Introduction
In this article, I have tried to put my thought process for implementing a lightweight ESB (Enterprise Service Bus), harnessing the content-based routing feature of WCF 4 and the message transformation capability of XSLT 2.0. Here we would deal with multiple contracts, multiple routing services, multiple input messages, and multiple output messages corresponding to the input messages. In the end, we will also see how to go about further refining this implementation, leveraging the WS-Discovery protocol support feature of WCF 4 and the new kid in the block, Windows Server AppFabric, for hosting and managing WCF services in an Enterprise SOA implementation scenario involving Microsoft technologies.
Background
This article requires prior knowledge of WCF, C#, and .NET 4.0. Knowledge of ESB Toolkit 2.0, BizTalk Server 2009, and Windows Server AppFabric is desirable, but not essential. Prior knowledge of SOA is also desirable.
An Enterprise SOA model has five horizontal layers and four vertical layers, making a total of nine layers. The layers and the technology involved therein are presented below:
Producer
- Layer 01: Operational Systems - They are discrete applications and operational systems running in an enterprise, forming silos and islands of data, knowledge, and transaction history.
- Layer 02: Business Components - They are grouping of implementations pertaining to distinct subject areas in a business. ADO.NET Entity Framework, custom-made components, (probably taking part in COM+ transactions) etc., might implement this layer.
- Layer 03: Enterprise Services - They are loosely coupled, autonomous, coarse-grained services exposing the functionality of the business components underneath. WCF Data Services, WCF Services, ASP.NET Web Services, RESTful services etc., constitute this layer. WCF is the technology used here.
Consumer
- Layer 04: Business Workflows - This layer implements the business choreography in conjunction with business rules. The workflows are implemented using Windows Workflow Foundation 4, which is BPEL compliant. Microsoft has XLANGs which is BPEL4WS compliant. WRE (Workflow Rule Engine) could be used for implementing a business rules store.
- Layer 05: Client Applications - This layer comprises of the various client applications. Silverlight 4.0 is the forefront technology here.
Common Layers
- Layer 06: Enterprise Service Bus - ESB provides a range of new capabilities focused on building robust, connected, service-oriented applications that incorporate itinerary-based service invocation for lightweight service composition, dynamic resolution of endpoints and maps, Web Service and WS-* integration, fault management and reporting, and integration with third-party SOA governance solutions. Neudesic ESB is the product for this layer. This product is endorsed by Microsoft.
- Layer 07: Service Management and Monitoring - This layer mainly manages and monitors the services. Windows Server AppFabric version 1.0 is the leading technology here.
- Layer 08: Information Architecture (Service Metadata, Registry, BI) - This layer integrates the services with the UDDI registry (UDDI 3.0), implements data architecture etc. SSIS (SQL Server Integration Services) is another technology used here.
- Layer 09: SOA Governance - This layer is used for SOA governance. Policies pertaining to services and other governance related aspects are implemented here. AmberPoint Inc.'s BizTalk Nano Agent is a product endorsed by Microsoft for SOA Governance. WCF 4 also supports WS-Policy and WS-PolicyAttachment.
Using the Code
The following things are done in the code presented here:
- Two product services are exposed.
- Two customer services are exposed.
- A routing service exposed for choosing any one of the two customer services to be called depending upon the customer message content.
- Another routing service exposed for choosing any one of the two product services to be called depending upon the product message content.
- Two XSLT transformations: one for transforming input customer message to output customer detail message, and the other for transforming input product message to output finished product message.
- The service contracts and corresponding data contracts for input and output messages for customers and products.
- The services implementing contracts for customer and product.
- Configuration files for implementing routing services and other standalone services.
From the abovementioned steps, it is clear that what we are going to achieve content based routing combined with message transformation.
The following steps are to be performed:
- Create a blank Visual Studio Solution and name it LightWeightESB.sln.
- Add a class library called CustomerProductContract.
- Add a code file called ICustomer.cs with the following contents:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Web;
using System.Runtime.Serialization;
namespace CustomerProductContract
{
[ServiceContract]
public interface ICustomer
{
[OperationContract]
CustomerDetail GetCustomerDetails(Customer cust);
}
[DataContract]
public class Customer
{
[DataMember]
public string CustomerID { get; set; }
[DataMember]
public string CustomerName { get; set; }
[DataMember]
public string CustomerCreditRating { get; set; }
}
[DataContract]
public class CustomerDetail
{
[DataMember]
public string CustomerID { get; set; }
[DataMember]
public string CustomerFirstName { get; set; }
[DataMember]
public string CustomerMiddleName { get; set; }
[DataMember]
public string CustomerLastName { get; set; }
[DataMember]
public string CustomerCreditRating { get; set; }
}
}
The Customer
class corresponds to the input message, and the CustomerDetail
class corresponds to the output message. A message map implemented using XSLT 2.0 maps the input customer message to the output customer detail message.
- Next, add a code file named IProduct.cs containing the following code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Web;
using System.Runtime.Serialization;
namespace CustomerProductContract
{
[ServiceContract]
public interface IProduct
{
[OperationContract]
FinishedProduct GetProductDetails(Product prod);
}
[DataContract]
public class Product
{
[DataMember]
public string ProductName { get; set; }
[DataMember]
public string ProductCategory { get; set; }
}
[DataContract]
public class FinishedProduct
{
[DataMember]
public string MfgDate { get; set; }
[DataMember]
public string ExpDate { get; set; }
[DataMember]
public string ProductName { get; set; }
[DataMember]
public string BatchID { get; set; }
[DataMember]
public string ProductCategory { get; set; }
}
}
Here, the Product
class corresponds to the input message and the FinishedProduct
class corresponds to the output message. Mapping between the input and output messages are done using XSLT 2.0. Thus, we see that we have multiple input messages being received by the lightweight service bus.
- Add references for System.ServiceModel.dll, System.ServiceModel.Description.dll, System.ServiceModel.Web.dll, and System.Runtime.Serialization.dll to the project.
- Add a code file TransformUtility.cs containing the following code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Xsl;
using System.Xml.XPath;
using System.Xml.Serialization;
using System.IO;
namespace CustomerProductContract
{
public static class TransformUtility
{
private static XmlSerializer GetSerializer(Type type)
{
return new XmlSerializer(type);
}
public static string ToXml<T>(T obj)
{
var xs = GetSerializer(typeof(T));
var sb = new StringBuilder();
using (var swriter = new StringWriter(sb))
{
xs.Serialize(swriter, obj);
swriter.Close();
}
return sb.ToString();
}
public static T FromXml<T>(string xml)
{
T axb;
var xs = GetSerializer(typeof(T));
using (var reader = new StringReader(xml))
{
axb = (T)xs.Deserialize(reader);
reader.Close();
}
return axb;
}
public static string TransformXml(TextReader reader, bool isCustomer)
{
string xslPath = isCustomer ? @"F:\Works\ProtocolBridge\Customer" +
@"Contract\MappingMapToCustomerDetails.xslt" :
@"F:\Works\ProtocolBridge\CustomerContract" +
@"\MappingMapToFinishedProducts.xslt";
XPathDocument xpathDoc = new XPathDocument(reader);
XslCompiledTransform xsl = new XslCompiledTransform();
xsl.Load(xslPath);
StringBuilder sb = new StringBuilder();
TextWriter tw = new StringWriter(sb);
xsl.Transform(xpathDoc, null, tw);
return sb.ToString();
}
}
}
Now, this requires some explanation. The ToXml<>()
method serializes a .NET object into its equivalent XML string, and the FromXml<>()
method deserializes an XML string to an equivalent .NET object. Generics are used here because we can deserialize and serialize any type using the aforementioned methods. Decorating classes with the DataContract
attribute automatically makes it serializable. The TransformXml()
method takes an input TextReader
equivalent of the XML message and transforms it using XslCompiledTransform
, harnessing the .xslt file containing the transformations.
- Now we need to add the .xslt files. First, we will add the MappingMapToCustomerDetails.xslt file. Right-click the project and use the Add->New Item option to add the XSLT file. Copy the following contents into it:
<stylesheet version="2.0" exclude-result-prefixes="xs fn"
xmlns:fn="http://www.w3.org/2005/xpath-functions"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<output method="xml" indent="yes" encoding="UTF-8"/>
<template match="/">
<customerdetail >
<attribute name="xsi:noNamespaceSchemaLocation"
namespace="http://www.w3.org/2001/XMLSchema-instance"/>
<for-each select="Customer">
<customerid>
<value-of select="CustomerID"/>
</customerid>
</for-each>
<for-each select="Customer">
<customermiddlename>
<value-of select="CustomerName"/>
</customermiddlename>
</for-each>
</customerdetail>
</template>
</stylesheet>
Here we provide a mapping between the Customer
and CustomerDetail
objects by first serializing the Customer
object into an XML string and then applying the transformation, followed by deserializing the resultant XML string into a CustomerDetail
object. Thus we achieve transformation of the input message to an output message, which is one of the prime features of ESB.
- Next, we add the .xslt file similarly, with the name MappingMapToFinishedProducts.xslt, with the following contents:
<stylesheet version="2.0" exclude-result-prefixes="xs fn"
xmlns:fn="http://www.w3.org/2005/xpath-functions"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<output method="xml" indent="yes" encoding="UTF-8"/>
<template match="/">
<finishedproduct>
<attribute name="xsi:noNamespaceSchemaLocation"
namespace="http://www.w3.org/2001/XMLSchema-instance"/>
<for-each select="Product">
<productname>
<value-of select="ProductName"/>
</productname>
</for-each>
<for-each select="Product">
<productcategory >
<value-of select="ProductCategory"/>
</productcategory>
</for-each>
</finishedproduct>
</template>
</stylesheet>
Here, we provide a mapping between the Product
and FinishedProduct
objects by first serializing the Product
object into an XML string and subsequently applying the transformation, followed by deserializing the resultant XML string into a FinishedProduct
object. We are doing simple transformations in both the above cases just to show the concept of transformation in ESB rather than delving deep into the idiosyncrasies of XSLT.
- Now, compile and build the solution.
- Next, add a WCF Service Application to the solution and name it ExclusiveProductService.
- Have the following contents in the ExclusiveProductService.svc file:
<%@ServiceHost Language="C#"
Debug="true"
Service="ExclusiveProductService.ExclusiveProductService"
CodeBehind="ExclusiveProductService.svc.cs"%>
- In the code-behind, the file ExclusiveProductService.svc.cs has the following code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
using System.IO;
namespace ExclusiveProductService
{
public class ExclusiveProductService : CustomerProductContract.IProduct
{
public CustomerProductContract.FinishedProduct
GetProductDetails(CustomerProductContract.Product prod)
{
string prodXml =
CustomerProductContract.TransformUtility.ToXml
<CustomerProductContract.Product>(prod);
StringReader reader = new StringReader(prodXml);
string finProdXml =
CustomerProductContract.TransformUtility.TransformXml(reader, false);
CustomerProductContract.FinishedProduct finProd =
CustomerProductContract.TransformUtility.FromXml
<CustomerProductContract.FinishedProduct>(finProdXml);
return finProd;
}
}
}
Here, the IProduct
interface is implemented. We first get the XML equivalent of the input Product
object and then take it inside a reader object, and subsequently call the TransformXml
method. Upon getting the deserialized output object, we return it.
- In the corresponding web.config file, put the following code:
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0"/>
</system.web>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_ICustomerProduct">
<security mode="None"/>
</binding>
</basicHttpBinding>
</bindings>
<services>
<service name="ExclusiveProductService">
<endpoint address="ExclusiveProductService.svc"
contract="CustomerProductContract.IProduct"
bindingConfiguration="BasicHttpBinding_ICustomerProduct"
binding="basicHttpBinding"/>
<endpoint address="mex"
contract="IMetadataExchange"
binding="mexHttpBinding"/>
<host>
<baseAddresses>
<add baseAddress="http://localhost/ExclusiveProductService/"/>
</baseAddresses>
</host>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInfaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true"/>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
- Next, we add another WCF Service Application to the solution named GeneralProductService.
- Next, in the GeneralProductService.svc file, we add the following code:
<%@ServiceHost Language="C#" Debug="true"
Service="GeneralProductService.GeneralProductService"
CodeBehind="GeneralProductService.svc.cs"%>
- In the code-behind, the file GeneralProductService.svc.cs has the following code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
using System.Xml;
using System.IO;
namespace GeneralProductService
{
public class GeneralProductService : CustomerProductContract.IProduct
{
public CustomerProductContract.FinishedProduct
GetProductDetails(CustomerProductContract.Product prod)
{
string prodXml =
CustomerProductContract.TransformUtility.ToXml
<CustomerProductContract.Product>(prod);
StringReader reader = new StringReader(prodXml);
string finProdXml =
CustomerProductContract.TransformUtility.TransformXml(reader, false);
CustomerProductContract.FinishedProduct finProd =
CustomerProductContract.TransformUtility.FromXml
<CustomerProductContract.FinishedProduct>(finProdXml);
return finProd;
}
}
}
Here, we implement the IProduct
interface. First, we obtain the Product
object via input and convert it into an XML string equivalent. Then, we transform that into output XML which, on deserializing, yields the FinishedProduct
object, which goes into the output message.
- The corresponding web.config file should have the following:
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0"/>
</system.web>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_ICustomerProduct"/>
<security mode="None"/>
</binding>
</basicHttpBinding>
</bindings>
<services>
<service name="GeneralProductService"/>
<endpoint address="GeneralProductService.svc"
contract="CustomerProductContract.IProduct"
bindingConfiguration="BasicHttpBinding_ICustomerProduct"
binding="basicHttpBinding"/>
<endpoint address="mex" contract="IMetadataExchange"
binding="mexHttpBinding"/>
<host>
<baseAddresses>
<add baseAddress="http://localhost/GeneralProductService/"/>
</baseAddresses>
</host>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true"/>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
- Now add another WCF Service Application named OrdinaryCustomerService to the solution.
- The corresponding OrdinaryCustomerService.svc file should have the following code:
<%@ ServiceHost Language="C#" Debug="true"
Service="OrdinaryCustomerService.OrdinaryCustomerService"
CodeBehind="OrdinaryCustomerService.svc.cs" %>
- And the code-behind file OrdinaryCustomerService.svc.cs should contain the following code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
using System.IO;
namespace OrdinaryCustomerService
{
public class OrdinaryCustomerService : CustomerProductContract.ICustomer
{
public CustomerProductContract.CustomerDetail
GetCustomerDetails(CustomerProductContract.Customer cust)
{
string custXml = CustomerProductContract.TransformUtility.ToXml
<CustomerProductContract.Customer>(cust);
StringReader reader = new StringReader(custXml);
string detCustXml =
CustomerProductContract.TransformUtility.TransformXml(reader, true);
CustomerProductContract.CustomerDetail detCust =
CustomerProductContract.TransformUtility.FromXml
<CustomerProductContract.CustomerDetail>(detCustXml);
return detCust;
}
}
}
Here, the ICustomer
contract is implemented. The GetCustomerDetails
method takes a Customer
object, gets the XML string equivalent of the same, and transforms that into an output CustomerDetail
object which is returned.
- The corresponding web.config file should contain the following code:
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0"/>
</system.web>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_ICustomer">
<security mode="None"/>
</binding>
</basicHttpBinding>
</bindings>
<services>
<service name="OrdinaryCustomerService">
<endpoint address="OrdinaryCustomerService.svc"
contract="CustomerProductContract.ICustomer"
bindingConfiguration="BasicHttpBinding_ICustomer"
binding="basicHttpBinding"/>
<endpoint address="mex"
contract="IMetadataExchange" binding="mexHttpBinding"/>
<host>
<baseAddresses>
<add baseAddress="http://localhost/OrdinaryCustomerService/"/>
</baseAddresses>
</host>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true"/>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
- Next, add another WCF Service Application to the solution and name it PremiumCustomerService.
- The corresponding PremiumCustomerService.svc file should contain the following code:
<%@ServiceHost Language="C#" Debug="true"
Service="PremiumCustomerService.PremiumCustomerService"
CodeBehind="PremiumCustomerService.svc.cs"%>
- And the code-behind file PremiumCustomerService.svc.cs file should contain:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
using System.IO;
namespace PremiumCustomerService
{
public class PremiumCustomerService : CustomerProductContract.ICustomer
{
public CustomerProductContract.CustomerDetail
GetCustomerDetails(CustomerProductContract.Customer cust)
{
string custXml = CustomerProductContract.TransformUtility.ToXml
<CustomerProductContract.Customer>(cust);
StringReader reader = new StringReader(custXml);
string detCustXml =
CustomerProductContract.TransformUtility.TransformXml(reader, true);
CustomerProductContract.CustomerDetail detCust =
CustomerProductContract.TransformUtility.FromXml
<CustomerProductContract.CustomerDetail>(detCustXml);
return detCust;
}
}
}
- Now the corresponding web.config file should contain the following code:
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0"/>
</system.web>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_ICustomer">
<security mode="None"/>
</binding>
</basicHttpBinding>
</bindings>
<services>
<service name="PremiumCustomerService">
<endpoint address="PremiumCustomerService.svc"
contract="CustomerProductContract.ICustomer"
bindingConfiguration="BasicHttpBinding_ICustomer"
binding="basicHttpBinding"/>
<endpoint address="mex"
contract="IMetadataExchange"
binding="mexHttpBinding"/>
<host>
<baseAddresses>
<add baseAddress="http://localhost/PremiumCustomerService/"/>
</baseAddresses>
</host>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true"/>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
- Now we shall add the routing services. First, we would add a WCF Service Application named ProductService. This service would route the product messages to one of the services, either GeneralProductService or ExsclusiveProductService, depending upon the contents of the
ProductCategory
property. - The ProductService.svc file should contain the following code:
<%@ ServiceHost Language="C#" Debug="true"
Service="System.ServiceModel.Routing.RoutingService,System.ServiceModel.Routing,
version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" %>
Here we are using System.ServiceModel.Routing.RoutingService
as the service which resides in System.ServiceModel.Routing.dll.
- Needless to say, now we need to add a reference to the project for System.ServiceModel.Routing.dll.
- The web.config file for this routing service should contain the following code:
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0"/>
</system.web>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_ICustomerProduct">
<security mode="None"/>
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint name="ProductServiceLibrary_GeneralProductService"
address="http://localhost/GeneralProductService/GeneralProductService.svc"
contract="*"
bindingConfiguration="BasicHttpBinding_ICustomerProduct"
binding="basicHttpBinding"/>
<endpoint name="ProductServiceLibrary_ExclusiveProductService"
address="http://localhost/ExclusiveProductService/ExclusiveProductService.svc"
contract="*" bindingConfiguration="BasicHttpBinding_ICustomerProduct"
binding="basicHttpBinding"/>
<endpoint address="mex" contract="IMetadataExchange"
binding="mexHttpBinding"/>
</client>
<services>
<service name="System.ServiceModel.Routing.RoutingService"
behaviorConfiguration="RoutingServiceBehavior"/>
<host>
<baseAddresses>
<add baseAddress="http://localhost/ProductService/"/>
</baseAddresses>
</host>
<endpoint name="RoutingServiceEndpoint"
contract="System.ServiceModel.Routing.IRequestReplyRouter"
bindingConfiguration="BasicHttpBinding_ICustomerProduct"
binding="basicHttpBinding"/>
<endpoint name="udpDiscovery" kind="udpDiscoveryEndpoint"/>
<endpoint address="mex" contract="IMetadataExchange"
binding="mexHttpBinding"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="RoutingServiceBehavior">
<serviceMetadata httpgetenabled="true" />
<serviceDebug includeExceptionDetailInFaults="true"/>
<routing routeOnHeadersOnly="False"
filterTableName="routingRules">
<serviceDiscovery/>
</behavior>
<behavior>
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<routing>
<namespaceTable>
<add namespace="http://schemas.datacontract.org/2004/07/CustomerProductContract"
prefix="pp"/>
</namespaceTable>
<filters>
<filter name="GeneralProductFilter"
filterData="//pp:ProductCategory = 'General'"
filterType="XPath"/>
<filter name="ExclusiveProductFilter"
filterData="//pp:ProductCategory = 'Exclusive'"
filterType="XPath"/>
</filters/>
<filterTables>
<filterTable name="routingRules">
<add priority="0"
endpointName="ProductServiceLibrary_GeneralProductService"
filterName="GeneralProductFilter"/>
<add priority="0"
endpointName="ProductServiceLibrary_ExclusiveProductService"
filterName="ExclusiveProductFilter"/>
</filterTable>
</filterTables>
<backupLists>
<backupList name="ProductBackupList">
<add endpointName="ProductServiceLibrary_GeneralProductService"/>
</backupList>
</backupLists>
</routing>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
Here, in the serviceBehaviors
section, we specify the filterTableName
attribute for the routing element. We also enable serviceDiscovery
here. In the routing section, we add a namespace prefix for using it in an XPath expression for content based routing. We add a filter in the filters
section. The filterType
we specify as XPath
, and in filterData
, we specify the actual XPath expression using the namespace prefix previously declared. The filters are nothing but values of public automatic properties of the class decorated with the DataContract
attribute. We also declare a backupLists
section for soft recovery from failures.
- Next, we add another WCF Service application to the solution, naming it as CustomerService.
- The CustomerService.svc file should contain the following code:
<%@ServiceHost Language="C#" Debug="true"
Service="System.ServiceModel.Routing.RoutingService,System.ServiceModel.Routing,
version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"%>
We can see that a routing service needs to be created per interface.
- The web.config file corresponding to this routing service should contain the following code:
<configuration>
<system.web>
<compilation debug="true" targetframework="4.0"/>
</system.web>
<system.servicemodel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_ICustomer">
<security mode="None"/>
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint name="CustomerServiceLibrary_PremiumCustomerService"
address="http://localhost/PremiumCustomerService/PremiumCustomerService.svc"
contract="*" bindingConfiguration="BasicHttpBinding_ICustomer"
binding="basicHttpBinding"/>
<endpoint name="CustomerServiceLibrary_OrdinaryCustomerService"
address="http://localhost/OrdinaryCustomerService/OrdinaryCustomerService.svc"
contract="*" bindingConfiguration="BasicHttpBinding_ICustomer"
binding="basicHttpBinding"/>
<endpoint address="mex"
contract="IMetadataExchange" binding="mexHttpBinding"/>
</client>
<services>
<service name="System.ServiceModel.Routing.RoutingService"
behaviorConfiguration="RoutingServiceBehavior">
<host>
<baseAddresses>
<add baseAddress="http://localhost/CustomerService/"/>
</baseAddresses>
</host>
<endpoint name="RoutingServiceEndpoint"
contract="System.ServiceModel.Routing.IRequestReplyRouter"
bindingConfiguration="BasicHttpBinding_ICustomer"
binding="basicHttpBinding"/>
<endpoint name="udpDiscovery" kind="udpDiscoveryEndpoint"/>
<endpoint address="mex"
contract="IMetadataExchange" binding="mexHttpBinding"/>
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="RoutingServiceBehavior">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true"/>
<routing routeOnHeadersOnly="False"
filterTableName="routingRules"/>
<serviceDiscovery/>
</behavior>
<behavior>
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<routing>
<namespaceTable>
<add namespace="http://schemas.datacontract.org/2004/07/CustomerProductContract"
prefix="cc"/>
</namespaceTable>
<filters>
<filter name="PremiumCustomerFilter"
filterData="//cc:CustomerCreditRating = 'Good'"
filterType="XPath"/>
<filter name="OrdinaryCustomerFilter"
filterData="//cc:CustomerCreditRating = 'Bad'"
filterType="XPath"/>
</filters>
<filterTables>
<filterTable name="routingRules">
<add priority="0"
endpointName="CustomerServiceLibrary_PremiumCustomerService"
filterName="PremiumCustomerFilter"/>
<add priority="0"
endpointName="CustomerServiceLibrary_OrdinaryCustomerService"
filterName="OrdinaryCustomerFilter"/>
</filterTable>
</filterTables>
<backupLists>
<backupList name="CustomerBackupList">
<add endpointName="CustomerServiceLibrary_OrdinaryCustomerService" />
</backupList>
</backupLists>
</routing>
</system.serviceModel>
<system.webserver>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
Here also, we declare the routing logic using a routing
section, implementing filterTable
and filters
therein containing an XPath expression against filterData
for routing based on the CustomerCreditRating
public automatic property of the Customer
class.
- Now we need to think about hosting. We can deploy all the services to Windows Server AppFabric. Actually, any service deployed to IIS 7.0 will be picked up by AppFabric in the management tools. We only need to ensure that Application Server Extensions for .NET 4 is pre-installed prior to deployment.
- We deploy all the services by creating the appropriate virtual directories corresponding to the physical directories, like CustomerService, ExclusiveProductService, GeneralProductService, OrdinaryCustomerService, PremiumCustomerService, and ProductService.
- Now we need to add the client project. Add a Windows Forms Application to the solution and name it ProtocolBridgingClient.
- The app.config file need not contain anything more than the following:
<configuration>
<system.serviceModel>
<client />
</system.serviceModel>
</configuration>
- Rename Form1.cs to ProtocolBridgingForm.cs, and the code for this Windows Form should contain the following:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.ServiceModel;
using System.ServiceModel.Description;
using System.ServiceModel.Discovery;
namespace ProtocolBridgingClient
{
public partial class ProtocolBridgingForm : Form
{
public ProtocolBridgingForm()
{
InitializeComponent();
}
private void ProtocolBridgingForm_Load(object sender, EventArgs e)
{
try
{
BasicHttpBinding binding = new BasicHttpBinding();
EndpointAddress custAddress = new EndpointAddress(
"http://localhost/CustomerService/CustomerService.svc");
CustomerProductContract.ICustomer custProxy =
ChannelFactory<CustomerProductContract.ICustomer>.CreateChannel(
binding, custAddress);
CustomerProductContract.Customer cust1 =
new CustomerProductContract.Customer
{
CustomerID = "ar0045855",
CustomerName = "Ambar Ray",
CustomerCreditRating = "Good"
};
CustomerProductContract.Customer cust2 =
new CustomerProductContract.Customer
{
CustomerID = "am0046067",
CustomerName = "Abhijit Mahato",
CustomerCreditRating = "Bad"
};
EndpointAddress prodAddress = new EndpointAddress(
"http://localhost/ProductService/ProductService.svc");
CustomerProductContract.IProduct prodProxy =
ChannelFactory<CustomerProductContract.IProduct>.CreateChannel(
binding, prodAddress);
CustomerProductContract.Product prod1 =
new CustomerProductContract.Product { ProductName = "LUX",
ProductCategory = "General" };
CustomerProductContract.Product prod2 =
new CustomerProductContract.Product { ProductName = "VIM",
ProductCategory = "Exclusive" };
CustomerProductContract.CustomerDetail custDet1 =
custProxy.GetCustomerDetails(cust1);
CustomerProductContract.CustomerDetail custDet2 =
custProxy.GetCustomerDetails(cust2);
CustomerProductContract.FinishedProduct finProd1 =
prodProxy.GetProductDetails(prod1);
CustomerProductContract.FinishedProduct finProd2 =
prodProxy.GetProductDetails(prod2);
string res1 = custDet1.CustomerID + " " +
custDet1.CustomerFirstName + " " +
custDet1.CustomerMiddleName +
" " + custDet1.CustomerLastName +
" " + custDet1.CustomerCreditRating;
string res2 = custDet2.CustomerID + " " +
custDet2.CustomerFirstName +
" " + custDet2.CustomerMiddleName + " " +
custDet2.CustomerLastName + " " +
custDet2.CustomerCreditRating;
string res3 = finProd1.BatchID + " " + finProd1.MfgDate + " " +
finProd1.ExpDate + " " + finProd1.ProductName +
" " + finProd1.ProductName;
string res4 = finProd2.BatchID + " " + finProd2.MfgDate +
" " + finProd2.ExpDate + " " +
finProd2.ProductName + " " + finProd2.ProductName;
txtCustomer.Text = res1 + "\n" + res2 +
"\n" + res3 + "\n" + res4;
}
catch (Exception ex)
{
txtCustomer.Text = (ex.InnerException != null) ?
ex.InnerException.Message + "\t" +
ex.Message : ex.Message;
}
}
private void ProtocolBridgingForm_FormClosed(object sender,
FormClosedEventArgs e)
{
GC.Collect();
}
}
}
Here, we create proxy classes for CustomerService (routing service) and ProductService (routing service) by using the ChannelFactory<>.CreateChannel()
method, passing the BasicHttpBinding
and EndpointAddress
objects as parameters. The EndpointAddress
object creation takes the hardcoded URI of the routing service. Finally, we call the proxy methods, which in turn invoke the services.
Conclusion
Finally, what we have done is, using a routing service, we are routing a Customer message to one of the two customer related services depending upon the content of the message and similarly, we use another routing service to route a Product message to one of the two product related services depending upon the content of the message. Also, after obtaining the message, we are transforming the message using an XSLT transformation and then sending back the output message. Thus, we are achieving both routing and transformation.
Points of Interest
In the client code, we have hardcoded the routing service URI for both the routing services. This can be avoided by using the WS-Discovery protocol support feature of WCF 4. We can say WS-Discovery is a UDP based multicast message exchange. The message receives the End Point information from the Service and uses this as the discovery information. The client uses discovery information to discover the available service on the network. Earlier, we enabled support for service discovery in the web.config files corresponding to the routing services by adding an extra endpoint to the service, like:
...
<endpoint name="udpDiscovery" kind="udpDiscoveryEndpoint"/>
...
Thus, we make this service a dynamic service. Now, at the client side, we need to add a reference to System.ServiceModel.Discovery.dll. At the client side, we can add code like:
...
DiscoveryClient discoverclient =
new DiscoveryClient(new UdpDiscoveryEndpoint());
FindResponse response = discoverclient.Find(
new FindCriteria(typeof(CustomerProductContract.ICustomer)));
EndpointAddress address = response.Endpoints[0].Address;
...
Also, we can use UDDI 3.0 APIs to integrate WCF services with the UDDI 3.0 Registry such that runtime determination of service endpoints (dynamic routing) can be done. I leave it as is in this article, as it would require a lot more enhancements in order to be truly presented as a low-cost, lightweight ESB alternative for the Microsoft platform. I expect a much improvised form of a custom lightweight ESB solution in the future. Right now, using the ESB Toolkit 2.0 mandates using BizTalk Server, which again mandates using SQL Server. An ESB alternative based on WCF 4.0 and XSLT 2.0 would provide a low cost alternative with fast return on investment, along with the flexibility for architects to use multiple database products and not just stick to SQL Server.
History
- 04 Aug. 2004 - Published.