Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Implementing a Lightweight ESB Using Content Based Routing in WCF 4 and Transformation in XSLT 2.0

4.50/5 (10 votes)
5 Aug 2010CPOL11 min read 57.4K   809  
This article is an attempt to implement a lightweight Enterprise Service Bus using content-based routing in WCF 4 and message mapping transformation using XSLT 2.0.

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

  1. 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.
  2. 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.
  3. 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

  1. 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.
  2. Layer 05: Client Applications - This layer comprises of the various client applications. Silverlight 4.0 is the forefront technology here.

Common Layers

  1. 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.
  2. 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.
  3. 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.
  4. 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:

  1. Two product services are exposed.
  2. Two customer services are exposed.
  3. A routing service exposed for choosing any one of the two customer services to be called depending upon the customer message content.
  4. Another routing service exposed for choosing any one of the two product services to be called depending upon the product message content.
  5. 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.
  6. The service contracts and corresponding data contracts for input and output messages for customers and products.
  7. The services implementing contracts for customer and product.
  8. 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:

  1. Create a blank Visual Studio Solution and name it LightWeightESB.sln.
  2. Add a class library called CustomerProductContract.
  3. Add a code file called ICustomer.cs with the following contents:
  4. C#
    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.

  5. Next, add a code file named IProduct.cs containing the following code:
  6. C#
    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.

  7. Add references for System.ServiceModel.dll, System.ServiceModel.Description.dll, System.ServiceModel.Web.dll, and System.Runtime.Serialization.dll to the project.
  8. Add a code file TransformUtility.cs containing the following code:
  9. C#
    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)
            {
                //Change this path suitably as per location of your solution folder
                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.

  10. 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:
  11. XML
    <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.

  12. Next, we add the .xslt file similarly, with the name MappingMapToFinishedProducts.xslt, with the following contents:
  13. XML
    <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.

  14. Now, compile and build the solution.
  15. Next, add a WCF Service Application to the solution and name it ExclusiveProductService.
  16. Have the following contents in the ExclusiveProductService.svc file:
  17. XML
    <%@ServiceHost Language="C#" 
       Debug="true" 
       Service="ExclusiveProductService.ExclusiveProductService" 
       CodeBehind="ExclusiveProductService.svc.cs"%>
  18. In the code-behind, the file ExclusiveProductService.svc.cs has the following code:
  19. C#
    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
    {
        // NOTE: You can use the "Rename" command
        // on the "Refactor" menu to change the class name
        // "Service1" in code, svc and config file together.
        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.

  20. In the corresponding web.config file, put the following code:
  21. XML
    <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>
              <!-- To avoid disclosing metadata information, set the value 
                 below to false and remove the metadata endpoint above before deployment -->
              <serviceMetadata httpGetEnabled="true"/>
              <!-- To receive exception details in faults for debugging purposes, 
                 set the value below to true. Set to false before deployment 
                 to avoid disclosing exception information -->
              <serviceDebug includeExceptionDetailInfaults="false"/>
            </behavior>
          </serviceBehaviors>
        </behaviors>
        <serviceHostingEnvironment multipleSiteBindingsEnabled="true"/>
      </system.serviceModel>
      <system.webServer>
        <modules runAllManagedModulesForAllRequests="true"/>
      </system.webServer>
    </configuration>
  22. Next, we add another WCF Service Application to the solution named GeneralProductService.
  23. Next, in the GeneralProductService.svc file, we add the following code:
  24. XML
    <%@ServiceHost Language="C#" Debug="true" 
      Service="GeneralProductService.GeneralProductService" 
      CodeBehind="GeneralProductService.svc.cs"%>
  25. In the code-behind, the file GeneralProductService.svc.cs has the following code:
  26. C#
    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
    {
        // NOTE: You can use the "Rename" command
        // on the "Refactor" menu to change the class name
        // "Service1" in code, svc and config file together.
        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.

  27. The corresponding web.config file should have the following:
  28. XML
    <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>
              <!-- To avoid disclosing metadata information, set the value
                 below to false and remove the metadata endpoint above before deployment -->
              <serviceMetadata httpGetEnabled="true"/>
              <!-- To receive exception details in faults for debugging purposes, 
                set the value below to true. Set to false before deployment 
                to avoid disclosing exception information -->
              <serviceDebug includeExceptionDetailInFaults="false"/>
            </behavior>
          </serviceBehaviors>
        </behaviors>
        <serviceHostingEnvironment multipleSiteBindingsEnabled="true"/>
      </system.serviceModel>
      <system.webServer>
        <modules runAllManagedModulesForAllRequests="true"/>
      </system.webServer>
    </configuration>
  29. Now add another WCF Service Application named OrdinaryCustomerService to the solution.
  30. The corresponding OrdinaryCustomerService.svc file should have the following code:
  31. XML
    <%@ ServiceHost Language="C#" Debug="true" 
        Service="OrdinaryCustomerService.OrdinaryCustomerService" 
        CodeBehind="OrdinaryCustomerService.svc.cs" %>
  32. And the code-behind file OrdinaryCustomerService.svc.cs should contain the following code:
  33. C#
    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
    {
        // NOTE: You can use the "Rename" command on the "Refactor"
        // menu to change the class name "Service1"
        // in code, svc and config file together.
        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.

  34. The corresponding web.config file should contain the following code:
  35. XML
    <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>
              <!-- To avoid disclosing metadata information, set the value below 
                to false and remove the metadata endpoint above before deployment -->
              <serviceMetadata httpGetEnabled="true" />
              <!-- To receive exception details in faults for debugging purposes, 
                 set the value below to true. Set to false before 
                 deployment to avoid disclosing exception information -->
              <serviceDebug includeExceptionDetailInFaults="false"/>
            </behavior>
          </serviceBehaviors>
        </behaviors>
        <serviceHostingEnvironment multipleSiteBindingsEnabled="true"/>
      </system.serviceModel>
     <system.webServer>
        <modules runAllManagedModulesForAllRequests="true"/>
      </system.webServer>
    </configuration>
  36. Next, add another WCF Service Application to the solution and name it PremiumCustomerService.
  37. The corresponding PremiumCustomerService.svc file should contain the following code:
  38. XML
    <%@ServiceHost Language="C#" Debug="true" 
       Service="PremiumCustomerService.PremiumCustomerService" 
       CodeBehind="PremiumCustomerService.svc.cs"%>
  39. And the code-behind file PremiumCustomerService.svc.cs file should contain:
  40. C#
    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
    {
        // NOTE: You can use the "Rename" command on
        // the "Refactor" menu to change the class name
        // "Service1" in code, svc and config file together.
        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;
            }
        }
    }
  41. Now the corresponding web.config file should contain the following code:
  42. XML
    <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>
              <!-- To avoid disclosing metadata information, set the value 
                  below to false and remove the metadata endpoint 
                  above before deployment -->
              <serviceMetadata httpGetEnabled="true" />
              <!-- To receive exception details in faults for debugging purposes, 
                  set the value below to true. Set to false before deployment 
                  to avoid disclosing exception information -->
              <serviceDebug includeExceptionDetailInFaults="false"/>
            </behavior>
          </serviceBehaviors>
        </behaviors>
        <serviceHostingEnvironment multipleSiteBindingsEnabled="true"/>
      </system.serviceModel>
     <system.webServer>
        <modules runAllManagedModulesForAllRequests="true"/>
      </system.webServer>
    </configuration>
  43. 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.
  44. The ProductService.svc file should contain the following code:
  45. XML
    <%@ 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.

  46. Needless to say, now we need to add a reference to the project for System.ServiceModel.Routing.dll.
  47. The web.config file for this routing service should contain the following code:
  48. XML
    <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">
              <!-- To avoid disclosing metadata information, set the value below 
                 to false and remove the metadata endpoint above before deployment -->
              <serviceMetadata httpgetenabled="true" />
              <!-- To receive exception details in faults for debugging purposes, 
                 set the value below to true. Set to false before deployment 
                 to avoid disclosing exception information -->
              <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.

  49. Next, we add another WCF Service application to the solution, naming it as CustomerService.
  50. The CustomerService.svc file should contain the following code:
  51. XML
    <%@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.

  52. The web.config file corresponding to this routing service should contain the following code:
  53. XML
    <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">
              <!-- To avoid disclosing metadata information, set the value below 
                 to false and remove the metadata endpoint above before deployment -->
              <serviceMetadata httpGetEnabled="true" />
              <!-- To receive exception details in faults for debugging purposes, 
                 set the value below to true. Set to false before deployment 
                 to avoid disclosing exception information -->
              <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.

  54. 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.
  55. We deploy all the services by creating the appropriate virtual directories corresponding to the physical directories, like CustomerService, ExclusiveProductService, GeneralProductService, OrdinaryCustomerService, PremiumCustomerService, and ProductService.
  56. Now we need to add the client project. Add a Windows Forms Application to the solution and name it ProtocolBridgingClient.
  57. The app.config file need not contain anything more than the following:
  58. XML
    <configuration>
        <system.serviceModel>
            <client />
        </system.serviceModel>
    </configuration>
  59. Rename Form1.cs to ProtocolBridgingForm.cs, and the code for this Windows Form should contain the following:
  60. C#
    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:

XML
...
<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:

C#
...
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)