NIntegrate SOA Practice – E-Appointments Sample Application

Background

E-Appointments is a Microsoft ASAP (Aspiring Software Architects Program) Case Study Implementation using the latest .NET 3.0 Technologies and proven Microsoft patterns and practices like Software Factories and Enterprise Library.

The E-Appointments application has been designed to demonstrate how you can develop a distributed application using several new Microsoft technologies. The sample application utilizes several technologies including: Windows Communication Foundation, Windows Workflow Foundation, Windows Presentation Foundation, ASP .NET Ajax, Live Maps.

In this article, I’m going to enhance this application using NIntegrate, to demonstrate how to enhance WCF configuration management and how to make a WCF based distributed application support dynamic query easier and more maintainable in real enterprise distributed application. I’ll give all the source code of the enhanced sample application and the comparable official source code before the enhancement. The following enhancements benefited from NIntegrate will be demonstrated:

  • How to decouple WCF configuration from Web.config?
  • How to safely transfer dynamic query conditions through WCF?

Preparation

Before the practice, I’ll do some simplification  on the official source code of E-Appointments sample application downloaded from its codeplex site.

  • Upgrade the official source code to be VS2008 projects from VS2005 projects;
  • Remove the WPF UI project (because our enhancements will not change any WPF function);
  • Remove the certification based security validation (because NIntegrate doesn’t touch security and without this part, the sample application is easier to run locally);

After all these changes, we got a set of layered projects, which could be run and debug without any special configuration locally except a one-click SQL Server database installation.

There will be 13 projects categorized by 3 groups in the simplified solution:

E-Appointments Official Projects

Projects in UI folder consist of the main E-Appointments client website and 2 helper projects. The Modules project implements the MVP pattern benefited from the Microsoft Software Factory. The ServiceAgents project is the WCF client and proxy.

Projects in Hosts folder consist of the WCF host application and 1 WF helper project.

Projects in the BMS folder consist of the Biz & Data Access projects and WF & WCF service implementation projects.

Download NIntegrate

Now our game could start. Let’s download the binary of NIntegrate and the source code of the Enhanced E-Appointments sample application from http://nintegrate.codeplex.com first.

In the downloaded binary ZIP file, only the NIntegrate.dll is to be referenced by the sample application.

Click the StartCodeGeneratorWebsite.bat file could start the CodeGenerator tool to validate WCF configuration and generate NIntegrate query classes.

Decouple WCF configuration

By default, all WCF configuration are stored in Web.config for WCF host and client web applications. Although this is easy to use and dependency is injected instead of hardcoded, which works fine for small applications, for enterprise distributed applications, the cost to manage the configuration becomes heavy and unacceptable.

The target of this chapter is to decouple the WCF configuration of the E-Appointments host & client web applications from their Web.config files, so that all the WCF configuration could be stored  anywhere you want. Generally, in practice, the most suitable place to store them is database. But in this sample application, we just store them in a custom XML file as an embedded resource of an assembly.

Step 1 Create a new helper project named NIntegrateExtensions

Firstly, let’s create a NIntegrate folder in EAppointments solution and a NIntegrateExtensions project inside this new folder. This project will contain all the NIntegrate WCF helper classes and the custom XML file to contain WCF configuration.

Step 2 Create a custom XML file to store all the WCF configuration

We just easily copy all the WCF configuration from the Web.config files, and arrange them into some custom XML element groups as below:

WcfConfiguration.xml
<?xml version="1.0" encoding="utf-8" ?>
<WcfConfiguration>
  
<Bindings>
    
<binding name="wsHttp" closeTimeout="00:01:00" openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00" bypassProxyOnLocal="false" transactionFlow="false" hostNameComparisonMode="StrongWildcard" maxBufferPoolSize="524288" maxReceivedMessageSize="5000000" messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true" allowCookies="false">
      
<readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384" maxBytesPerRead="4096" maxNameTableCharCount="16384"/>
      
<reliableSession ordered="true" inactivityTimeout="00:10:00" enabled="false"/>
    
</binding>
  
</Bindings>
  
<Behaviors>
    
<behavior name="DefaultService_Behavior">
      
<serviceMetadata httpGetEnabled="true"/>
      
<serviceDebug includeExceptionDetailInFaults="false"/>
    
</behavior>
  
</Behaviors>
  
<Host>
    
<service name="EAppointments.BMS.ServiceImplementation.AppointmentService" behaviorConfiguration="DefaultService_Behavior">
      
<endpoint binding="wsHttpBinding" contract="EAppointments.BMS.ServiceContracts.IAppointmentService" bindingConfiguration="wsHttp" />
      
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
    
</service>
    
<service name="EAppointments.BMS.ServiceImplementation.ProviderService" behaviorConfiguration="DefaultService_Behavior">
      
<endpoint binding="wsHttpBinding" contract="EAppointments.BMS.ServiceContracts.IProviderService" bindingConfiguration="wsHttp" />
      
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
    
</service>
    
<service name="EAppointments.BMS.ServiceImplementation.DirectoryService" behaviorConfiguration="DefaultService_Behavior">
      
<endpoint binding="wsHttpBinding" contract="EAppointments.BMS.ServiceContracts.IDirectoryService" bindingConfiguration="wsHttp" />
      
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
    
</service>
  
</Host>
  
<Client>
    
<endpoint address="http://localhost:24444/EAppointments.BMS.Host/AppointmentService.svc" binding="wsHttpBinding" bindingConfiguration="wsHttp" contract="EAppointments.UI.ServiceAgents.AppointmentService.IAppointmentService" name="WSHttpBinding_IAppointmentService" />
    
<endpoint address="http://localhost:24444/EAppointments.BMS.Host/DirectoryService.svc" binding="wsHttpBinding" bindingConfiguration="wsHttp" contract="EAppointments.UI.ServiceAgents.DirectoryService.IDirectoryService" name="WSHttpBinding_IDirectoryService" />
    
<endpoint address="http://localhost:24444/EAppointments.BMS.Host/ProviderService.svc" binding="wsHttpBinding" bindingConfiguration="wsHttp" contract="EAppointments.UI.ServiceAgents.ProviderService.IProviderService" name="WSHttpBinding_IProviderService" />
  
</Client>
</WcfConfiguration>

Easily, I grouped them into Bindings, Behaviors, Host and Client. They should be straightforward enough to be understood.

In property of the XML file,  set the Build Action value to “Embedded Resource”.

Step 3 Remove the entire <system.serviceModel> elements from Web.config files

Since we are now storing the WCF configuration in custom place, we don’t need the <system.serviceModel> elements in Web.config files anymore. So we could completely remove them.

Step 4 Create a WCF configuration loader class

To load WCF configuration from the custom XML file, let’s create a class named WcfConfigurationLoader in the NIntegrateExtensions project with two methods:

  • WcfService LoadWcfServiceConfiguration(Type serviceType)
  • WcfClientEndpoint LoadWcfClientEndpointConfiguration(Type serviceContractType)

NIntegrate provides a set of serializable WCF configuration classes for storing and transferring WCF configuration easier. So we could easily implement this configuration loader class in less than 50 lines of code as below(it is just for demo, so there are not enough guard code to ensure code quality):

WcfConfigurationLoader.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NIntegrate.ServiceModel.Configuration;
using System.Xml;
using System.Reflection;
using System.Configuration;

namespace NIntegrateExtensions
{
    
/// <summary>
    
/// Demo class loading Wcf configuration from an embbed xml file, not thread-safe
    
/// </summary>
    public static class WcfConfigurationLoader
    {
        
private static readonly XmlDocument _xmlDoc;

        
static WcfConfigurationLoader()
        {
            _xmlDoc 
= new XmlDocument();

            
using (var stream = typeof(WcfConfigurationLoader).Assembly.GetManifestResourceStream("NIntegrateExtensions.WcfConfiguration.xml"))
            {
                _xmlDoc.Load(stream);
                stream.Close();
            }
        }

        
public static WcfService LoadWcfServiceConfiguration(Type serviceType)
        {
            var serviceElement 
= _xmlDoc.SelectSingleNode("/WcfConfiguration/Host/service[@name='" + serviceType.ToString() + "']");
            var serviceConfig 
= new WcfService { ServiceType = serviceType.AssemblyQualifiedName };
            var behavirConfigName 
= serviceElement.Attributes["behaviorConfiguration"].Value;
            var behaviorConfig 
= _xmlDoc.SelectSingleNode("/WcfConfiguration/Behaviors/behavior[@name='" + behavirConfigName + "']");
            serviceConfig.ServiceBehaviorXml 
= new ServiceBehaviorXml(behaviorConfig.OuterXml);
            var endpointConfigs 
= new List<WcfServiceEndpoint>();
            
foreach (XmlNode endpointElement in serviceElement.SelectNodes("endpoint"))
            {
                var endpointConfig 
= new WcfServiceEndpoint
                {
                    ServiceContractType 
= endpointElement.Attributes["contract"].Value,
                };
                
if (endpointElement.Attributes["address"!= null)
                    endpointConfig.Address 
= endpointElement.Attributes["address"].Value;
                var bindingTypeCode 
= endpointElement.Attributes["binding"].Value;
                
if (endpointElement.Attributes["bindingConfiguration"== null)
                    endpointConfig.BindingXml 
= new BindingXml(bindingTypeCode, null);
                
else
                {
                    var bindingConfigName 
= endpointElement.Attributes["bindingConfiguration"].Value;
                    var bindingElement 
= _xmlDoc.SelectSingleNode("/WcfConfiguration/Bindings/binding[@name='" + bindingConfigName + "']");
                    endpointConfig.BindingXml 
= new BindingXml(bindingTypeCode, bindingElement.OuterXml);
                }
                endpointConfigs.Add(endpointConfig);
            }
            serviceConfig.Endpoints 
= endpointConfigs.ToArray();
            
return serviceConfig;
        }

        
public static WcfClientEndpoint LoadWcfClientEndpointConfiguration(Type serviceContractType)
        {
            var endpointElement 
= _xmlDoc.SelectSingleNode("/WcfConfiguration/Client/endpoint[@contract='" + serviceContractType.ToString() + "']");
            var endpointConfig 
= new WcfClientEndpoint
            {
                ServiceContractType 
= endpointElement.Attributes["contract"].Value,
            };
            
if (endpointElement.Attributes["address"!= null)
                endpointConfig.Address 
= endpointElement.Attributes["address"].Value;
            var bindingTypeCode 
= endpointElement.Attributes["binding"].Value;
            
if (endpointElement.Attributes["bindingConfiguration"== null)
                endpointConfig.BindingXml 
= new BindingXml(bindingTypeCode, null);
            
else
            {
                var bindingConfigName 
= endpointElement.Attributes["bindingConfiguration"].Value;
                var bindingElement 
= _xmlDoc.SelectSingleNode("/WcfConfiguration/Bindings/binding[@name='" + bindingConfigName + "']");
                endpointConfig.BindingXml 
= new BindingXml(bindingTypeCode, bindingElement.OuterXml);
            }
            
return endpointConfig;
        }
    }
}

An instance of WcfService class contains all the required configuration for hosting a WCF service; and an instance of WcfClientEndpoint class contains all the required configuration for a client to consume a WCF service.

Step 5 Create a custom WCF service host factory class

To make the WCF service host engine load WCF configuration from custom place, we need to create a custom WCF service host factory class. With the help of NIntegrate, we could easily implmenent this class in less than 3 lines of code as below:

EAWcfServiceHostFactory.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NIntegrate.ServiceModel.Activation;
using NIntegrate.ServiceModel.Configuration;

namespace NIntegrateExtensions
{
    
public class EAWcfServiceHostFactory : WcfServiceHostFactory
    {
        
public override WcfService LoadServiceConfiguration(Type serviceType)
        {
            
return WcfConfigurationLoader.LoadWcfServiceConfiguration(serviceType);
        }
    }
}

Step 6 Create a WCF client channel factory class

To make the WCF service client engine load WCF configuration from custom place, we need to create a custom WCF client channel factory class. With the help of NIntegrate, we could easily implmenent this class in less than 10 lines of code as below:

EAWcfClientChannelFactory.cs
using System;
using System.Collections.Generic;
using System.Configuration;
using System.ServiceModel;
using NIntegrate.ServiceModel;

namespace NIntegrateExtensions
{
    
public static class EAWcfClientChannelFactory
    {
        
private static readonly Dictionary<Type, ChannelFactory> _cfCache = new Dictionary<Type, ChannelFactory>();
        
private static readonly object _cfCacheLock = new object();

        
public static WcfChannelWrapper<T> CreateWcfChannel<T>()
            
where T : class 
        {
            ChannelFactory cf;

            
if (!_cfCache.TryGetValue(typeof(T), out cf))
            {
                
lock (_cfCacheLock)
                {
                    
if (!_cfCache.TryGetValue(typeof(T), out cf))
                    {
                        var endpoint 
= WcfConfigurationLoader.LoadWcfClientEndpointConfiguration(typeof (T));
                        cf 
= WcfChannelFactoryFactory.CreateChannelFactory<T>(endpoint);
                        _cfCache[
typeof (T)] = cf;
                    }
                }
            }

            
return new WcfChannelWrapper<T>((cf as ChannelFactory<T>).CreateChannel());
        }
    }
}

Step 7 Use the EAWcfServiceHostFactory class to host services

Now we have all the helper classes, let’s change the .svc files for hosting the WCF services.

In the host web application, we could see 3 .svc files: AppointmentService.svc, DirectoryService.svc and ProviderService.svc.

DirectoryService.svc and ProviderService.svc have no specified service host factory in the .svc files, which means they are using the .NET framework build-in WCF service host factory. For these 2 services, we could easily add the assembly qualified name of the EAWcfServiceHostFactory class as value of the Factory property as below:

<%@ ServiceHost Language="C#" Debug="true" Service="EAppointments.BMS.ServiceImplementation.DirectoryService" Factory="NIntegrateExtensions.EAWcfServiceHostFactory, NIntegrateExtensions" %>
<%@ ServiceHost Language="C#" Debug="true" Service="EAppointments.BMS.ServiceImplementation.ProviderService" Factory="NIntegrateExtensions.EAWcfServiceHostFactory, NIntegrateExtensions" %>

The AppointmentService.svc is alittle bit different. Because it requires WF integration, there is already a custom WFServiceHostFactory class specified in the .svc file to start the WF engine when the WCF service host started. So instead of set the Factory value to EAWcfServiceHostFactory class, we could make the EAWcfServiceHostFactory class as the base class of the existing WFServiceHostFactory class as below:

WFServiceHostFactory.cs
using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel.Activation;
using System.ServiceModel;
using NIntegrateExtensions;
using NIntegrate.ServiceModel.Configuration;

namespace WFExtensions
{
    
public class WFServiceHostFactory : EAWcfServiceHostFactory
    {
        
public override WcfService LoadServiceConfiguration(Type serviceType)
        {
            var serviceConfig 
= base.LoadServiceConfiguration(serviceType);
            serviceConfig.CustomServiceHostType 
= typeof(WFServiceHost).AssemblyQualifiedName;

            
return serviceConfig;
        }
    }
}

Because of the legacy implementation of WFServiceHostFactory class returns instance of WFServiceHost class instead of the .NET framework build-in ServiceHost class, we need to set the CustomServiceHostType property value of WcfService class to the assembly qualified name of the WFServiceHost class.

After all of the code change in this step, try browsing the 3 .svc files in your browser, you should be able to see they are hosted and ready to be called by clients.

Step 8 Use the EAWcfClientChannelFactory class to consume services

Now let’s focus on the client web application. To consume WCF services, the legacy implementation of the sample application defines a set of proxy classes in the ServiceAgent project named XXXServiceAgent. In each of the proxy classes, for each WCF service operation method, there is a corresponding wrapper method initialize and call the proxy method on the  WCF service client class which is generally be generated automatically by VS IDE or the svcutil.exe tool of .NET framework SDK.

What we need to change is instead of initializing and calling the proxy method of the auto-generated WCF service client class, we need to call the CreateWcfChannel() method on the EAWcfClientChannelFactory  class to get the NIntegrate WCF channel wrapper class instances and call proxy methods on it. There should be less than 10 lines of code change in this file and the changed code should be like below for DirectoryServiceAgent class:

DirectoryServiceAgent.cs
using System;
using System.Collections.Generic;
using System.Text;
using EAppointments.UI.ServiceAgents.DirectoryService;
using EAppointments.UI.ServiceAgents.Interfaces;
using NIntegrateExtensions;
using NIntegrate.ServiceModel;

namespace EAppointments.UI.ServiceAgents
{
    
public class DirectoryServiceAgent : IDirectoryServiceAgent
    {
        
private WcfChannelWrapper<IDirectoryService> GetProxy()
        {
            
return EAWcfClientChannelFactory.CreateWcfChannel<IDirectoryService>();
        }

        
#region IDirectoryServiceAgent Members

        
public Patient[] Find(PatientSearchCriteria criteria)
        {
            
using (var proxy = GetProxy())
            {
                
return proxy.Channel.FindPatient(criteria).ToArray();
            }
        }

        
public Referrer[] Find(ReferrerSearchCriteria criteria)
        {
            
using (var proxy = GetProxy())
            {
                
return proxy.Channel.FindReferrer(criteria).ToArray();
            }
        }

        
public Specialty[] FindSpecialty()
        {
            
using (var proxy = GetProxy())
            {
                
return proxy.Channel.FindSpecialty().ToArray();
            }
        }

        
public ClinicType[] FindClinicType(Guid specialtyId)
        {
            
using (var proxy = GetProxy())
            {
                
return proxy.Channel.FindClinicType(specialtyId).ToArray();
            }
        }

        
#endregion
    }
}

The WCF channel wrapper class provided by NIntegrate implements the IDisposable interface with the best practice to ensure the resource of a WCF client channel is released if it is used in a using block as above.

After all of the code change in this step, try browsing the Default.aspx page of the E-Appointments client web application, you should be able to see all the functions are working as before.

Summary

Let’s do a quick review for what we benefited from the enhancements we made to the sample application in this chapter.

We could see, now there is completely no WCF configuration in either host & client web application’s Web.config files, which reduces the cost to management the WCF configuration in Web.config files when deploying and distributing them. Since all the WCF configuration classes provided by NIntegrate are serializable, you could store them anywhere, you could  also construct them freely. For example, in real practice, we store all the WCF configuration in database so that we could centrally manage all the WCF configuration of any applications. We could even change the WCF configuration when the applications are running to add runtime logging or do runtime performance tuning.

Enhance Cross-WCF Dynamic Query

In a general database based application, if there are dynamic query requirement, and if you don’t want to join SQL clips, you should be familiar with SQL with spectacular “IS NULL OR” lines in where clause like below:

SELECT    A.*
FROM    Appointment A
WHERE    (@UBRN            IS NULL OR A.UBRN = @UBRN)
    
AND (@PatientId        IS NULL OR A.PatientId = @PatientId)
    
AND (@ReferrerId    IS NULL OR A.ReferrerId = @ReferrerId)
    
AND    (@ProviderId    IS NULL OR A.ProviderId = @ProviderId)
    
AND    (@Status        IS NULL OR A.Status & @Status > 0)
    
AND    (@StartDateTime    IS NULL OR ((DatePart(yy,A.StartDateTime) >= DatePart(yy,@StartDateTime)) AND (DatePart(dy,A.StartDateTime) >= DatePart(dy, @StartDateTime))))
    
AND    (@EndDateTime    IS NULL OR ((DatePart(yy,A.EndDateTime) <= DatePart(yy,@EndDateTime)) AND (DatePart(dy,A.EndDateTime) <= DatePart(dy, @EndDateTime))))
    
AND (@WorkflowId    IS NULL OR A.WorkflowId = @WorkflowId)
ORDER BY A.UBRN

This is the stored procedure in E-Appointments application for dynamic query the Appointment table. Although, to some extent, this kind of SQL really works, and we have got used to them for long time, they are not friendly to maintenance.

For this example in E-Appointments application, to implement this dynamic query function, there defines a AppointmentSearchCriteria class which contains all the possible dynamic query required conditional fields. And an instance of AppointmentSearchCriteria class is transferred from UI to WCF client, to WCF service host, to Biz, to DataAccess, and finally to the stored procedure.

Then let’s consider if you want to add one dynamic query conditional field to it, what you have to do?

You need to add one field to the AppointmentSearchCriteria  class first, add one field in Biz layer, add one field in DataAccess, add one parameter in the stored procedure, re-generate the WCF client proxy code to having this new field, and finally set value of this field in UI. You can see, you have to change code in all the layers. Although this really works, I bet you will go mad if you changed fields in AppointmentSearchCriteria more than 5 times a day because of requirement change. It is really possible and even common in our so agile software development.

Then what NIntegrate could help for this case?

Step 1 Add a NIntegrate dynamic query method in AppointmentService WCF service

In the AppointmentService.cs file in the ServiceImplementation project, let’s add a NIntegrate dyanmic query method like below:

private static readonly QueryCommandFactory _cmdFac = new QueryCommandFactory();
private static readonly MapperFactory _mapperFac = new MapperFactory();

[ClaimsPrincipalPermissionAttribute(SecurityAction.Demand, RequiredClaimType 
= AppointmentClaimTypes.Read, Resource = Resources.Appointment)]
public AppointmentCollection Query(QueryCriteria criteria)
{
    
if (criteria == null)
        
throw new ArgumentNullException("criteria");

    var cmd 
= _cmdFac.CreateCommand(criteria, false);
    
using (var conn = cmd.Connection)
    {
        
if (conn.State != ConnectionState.Open)
            conn.Open();
        
using (var rdr = cmd.ExecuteReader())
        {
            var mapper 
= _mapperFac.GetMapper<IDataReader, List<DataTypes.Appointment>>();
            var list 
= mapper(rdr);
            
return new AppointmentCollection(list);
        }
    }

The QueryCriteria class is a sealed class provided by NIntegrate which is a serializable dynamic query criteria. The QueryCommandFactory class is a class provided by NIntegrate for constructing DbCommand from a QueryCriteria instance. The MapperFactory class is a class provided by NIntegrate for dynamic object mapping. Here it automatically maps the IDataReader to a List<DataTypes.Appointment>. For details of the MapperFactory class, please take a look at NIntegrate documentation.

Of course, we also added the Query method in the IAppointmentService service contract as a new OperationContract like below:

public interface IAppointmentService
{
    [OperationContract(Action 
= "Query")]
    [FaultContract(
typeof(SystemFault))]
    [FaultContract(
typeof(AppointmentServiceFault))]
    [FaultContract(
typeof(SecurityAccessDeniedException))] 
    AppointmentCollection Query(QueryCriteria criteria);
    
//...
}

Step 2 Re-generate the WCF client code

Since we added a new operation contract, we need to re-generate the WCF client. We could do this by the following command at command-line console:

svcutl.exe http://localhost:24444/EAppointments.BMS.Host/AppointmentService.svc /r:NIntegrate.dll /n:*,EAppointments.UI.ServiceAgents.AppointmentService

 

Copy the generated AppointmentService.cs file to overwrite the file with same file name in ServiceAgents project’s “Service References” folder so that client could consume this new WCF service method.

Step 3 Generate the Appointment query helper class

In the Modules project in UI/Common folder, create a new folder named NIntegrateQuery.

Open the query class generator tool mentioned in the above “Download NIntegrate” chapter, set the connectionstring pointing to EAppointments database, set output directory to the new NIntegrateQuery folder, set the output namespace to “EAppointments.UI.Modules.Query”, set the default connection string name to “EAppointments”.

Click the “Connect” button, select the “Appointment” table in the list and click the “Generate Code” button to generate a Appointment.cs file in the NIntegrateQuery  folder. The generated file should contain code below:

Appointment.cs
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:2.0.50727.4200
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

namespace EAppointments.UI.Modules.Query
{
    
    
    
public partial class Appointment : NIntegrate.Data.QueryTable
    {
        
        
private NIntegrate.Data.Int32Column _UBRN = new NIntegrate.Data.Int32Column("UBRN");
        
        
private NIntegrate.Data.GuidColumn _PatientId = new NIntegrate.Data.GuidColumn("PatientId");
        
        
private NIntegrate.Data.GuidColumn _ReferrerId = new NIntegrate.Data.GuidColumn("ReferrerId");
        
        
private NIntegrate.Data.GuidColumn _ProviderId = new NIntegrate.Data.GuidColumn("ProviderId");
        
        
private NIntegrate.Data.GuidColumn _SlotId = new NIntegrate.Data.GuidColumn("SlotId");
        
        
private NIntegrate.Data.DateTimeColumn _CreatedDate = new NIntegrate.Data.DateTimeColumn("CreatedDate");
        
        
private NIntegrate.Data.DateTimeColumn _StartDateTime = new NIntegrate.Data.DateTimeColumn("StartDateTime");
        
        
private NIntegrate.Data.DateTimeColumn _EndDateTime = new NIntegrate.Data.DateTimeColumn("EndDateTime");
        
        
private NIntegrate.Data.DateTimeColumn _UpdatedDate = new NIntegrate.Data.DateTimeColumn("UpdatedDate");
        
        
private NIntegrate.Data.GuidColumn _CancelledBy = new NIntegrate.Data.GuidColumn("CancelledBy");
        
        
private NIntegrate.Data.DateTimeColumn _CancelledDate = new NIntegrate.Data.DateTimeColumn("CancelledDate");
        
        
private NIntegrate.Data.StringColumn _CancellationReason = new NIntegrate.Data.StringColumn("CancellationReason"true);
        
        
private NIntegrate.Data.Int32Column _Status = new NIntegrate.Data.Int32Column("Status");
        
        
private NIntegrate.Data.StringColumn _Comments = new NIntegrate.Data.StringColumn("Comments"true);
        
        
private NIntegrate.Data.DateTimeColumn _ReminderDate = new NIntegrate.Data.DateTimeColumn("ReminderDate");
        
        
private NIntegrate.Data.GuidColumn _WorkflowId = new NIntegrate.Data.GuidColumn("WorkflowId");
        
        
private NIntegrate.Data.GuidColumn _ClinicTypeId = new NIntegrate.Data.GuidColumn("ClinicTypeId");
        
        
public Appointment() :
            
base("Appointment""EAppointments"false)
        {
        }
        
        
public NIntegrate.Data.Int32Column UBRN
        {
            
get
            {
                
return this._UBRN;
            }
        }
        
        
public NIntegrate.Data.GuidColumn PatientId
        {
            
get
            {
                
return this._PatientId;
            }
        }
        
        
public NIntegrate.Data.GuidColumn ReferrerId
        {
            
get
            {
                
return this._ReferrerId;
            }
        }
        
        
public NIntegrate.Data.GuidColumn ProviderId
        {
            
get
            {
                
return this._ProviderId;
            }
        }
        
        
public NIntegrate.Data.GuidColumn SlotId
        {
            
get
            {
                
return this._SlotId;
            }
        }
        
        
public NIntegrate.Data.DateTimeColumn CreatedDate
        {
            
get
            {
                
return this._CreatedDate;
            }
        }
        
        
public NIntegrate.Data.DateTimeColumn StartDateTime
        {
            
get
            {
                
return this._StartDateTime;
            }
        }
        
        
public NIntegrate.Data.DateTimeColumn EndDateTime
        {
            
get
            {
                
return this._EndDateTime;
            }
        }
        
        
public NIntegrate.Data.DateTimeColumn UpdatedDate
        {
            
get
            {
                
return this._UpdatedDate;
            }
        }
        
        
public NIntegrate.Data.GuidColumn CancelledBy
        {
            
get
            {
                
return this._CancelledBy;
            }
        }
        
        
public NIntegrate.Data.DateTimeColumn CancelledDate
        {
            
get
            {
                
return this._CancelledDate;
            }
        }
        
        
public NIntegrate.Data.StringColumn CancellationReason
        {
            
get
            {
                
return this._CancellationReason;
            }
        }
        
        
public NIntegrate.Data.Int32Column Status
        {
            
get
            {
                
return this._Status;
            }
        }
        
        
public NIntegrate.Data.StringColumn Comments
        {
            
get
            {
                
return this._Comments;
            }
        }
        
        
public NIntegrate.Data.DateTimeColumn ReminderDate
        {
            
get
            {
                
return this._ReminderDate;
            }
        }
        
        
public NIntegrate.Data.GuidColumn WorkflowId
        {
            
get
            {
                
return this._WorkflowId;
            }
        }
        
        
public NIntegrate.Data.GuidColumn ClinicTypeId
        {
            
get
            {
                
return this._ClinicTypeId;
            }
        }
    }
}

This is a NIntegrate query table helper class to provide the ability to strong typed query the Appointment database table.

Step 4 Consume the dynamic query service method

Now we can consume the dynamic query method. In Modules project’s Appointments folder, let’s re-write the Find() method in the AppointmentController.cs file as below:


internal Appointment[] Find(Guid? patientId, DateTime? startDateTime, DateTime? endDateTime, Int32 status)
{
    var query 
= new Query.Appointment();
    var criteria 
= query.Select();
    
if (startDateTime.HasValue)
        criteria.And(query.StartDateTime 
>= startDateTime.Value);
    
if (endDateTime.HasValue)
        criteria.And(query.EndDateTime 
<= endDateTime.Value);
    criteria.And(query.Status.BitwiseAnd(status) 
> 0);
    
if (patientId.HasValue)
        criteria.And((query.PatientId 
== patientId.Value));
    var appointments 
= new List<Appointment>(_appointmentServiceAgent.Query(criteria));

    
return OutputValidationUtility.Encode<Appointment>(appointments).ToArray();

The implementation of this method is very simple, it construct a NIntegrate QueryCriteria instance from the Appointment dynamic query helper class, and then call the Query() method to get the results. For details of dynamic query language based on the query class, please refer to NIntegrate documentation.

OK, our enhancement is done. Run the E-Appointments client web application again, you should be able to see the same effect as when the legacy implementation works.

Summary

Again, let’s do a quick review to see what benefits we got from NIntegrate dynamic query in this case here. To be fair, let’s still consider if you want to add one dynamic query conditional field , what you have to do?

Now we don’t need the AppointmentSearchCriteria class with fixed dynamic query fields. We even don’t need the stored procedure to dynamically query the Appointment database table anymore. So no matter add/remove dynamic query fields, the only required code change is the UI and UI controller. The application layers from WCF client to WCF service host, to Biz, to DataAccess to database require no change at all. The effort to implement and maintain the dynamic query implementation becomes super cheap. Amazing, right?

Tips of the sample application installation and debugging

Set up database

The setup the E-Appointments database, just run the SetUpDatabase.bat file in the “Scripts” folder of the downloaded ZIP file. By default, the database will be installed to the (local) SQL Server instance. If you want to install it to other SQL Server instance, you have to change the target instance in SetUpDatabase.bat file, you should also have to change all the connection strings in each Web.config file of the WCF host & client web applications.

Create a new appointment in E-Appointments web application

When running the E-Appointments client web application, you can login by the following account:

User – sanjiv
Password – p@ssw0rd

If you want to create a new appointment after login, click the “Create an Appointment” button, then click the “Search” button, select one patient in the list and click next, in the “Select Provider” view, if you have difficulty to find a available provider, try the following:

In Specialty dropdown, select “Cardiology”, in Sub-Specialty dropdown, select “Hypertension”, and then in the list or in the Map, select the provider.

posted @ 2010-02-20 22:20  Teddy's Knowledge Base  Views(968)  Comments(0Edit  收藏  举报