Adapter as a WCF Binding - In Depth

WCF LOB Adapter SDK surfaces an adapter as a custom WCF Binding A WCF Bindingcorresponds to the “How” of the WCF Message transfer from one Endpoint to another Endpoint.   It produces a set of binding elements that correspond to particular design settings and describe how the endpoint communicates.  The WCF Binding roughly equals Channel Stack and each binding element is a rough equivalent to one channel in the Channel Stack.   An example of a WCF Binding is BasicHttpBinding that is suitable for Basic Profile Web Services. 
Read more about WCF Bindings in depth here.
A binding supports various channel shapes and message exchange patterns.
The “outbound / send” binding is used to build a ChannelFactory, which in turn builds a channel stack and returns a reference to the top channel in the stack. The application can then use this channel to send messages.  The “inbound / receive” binding is used to build aIChannelListener, which listens for incoming messages.  The IChannelListener provides messages to the listening application by creating channel stacks and handing the application reference to the top channel. The application then uses this channel to receive incoming messages.  The following table shows key interfaces that a developer is expected to implement to support inbound/outbound WCF bindings – either created using WCF LOB Adapter SDK or WCF Channel Model.

IMPORTANT: Irrespective of how the binding is developed, WCF clients access these bindings in the same way – configuration or code.
The developer is working with a WCF Message when either creating a custom channel or developing an adapter using the WCF LOB Adapter SDK.  A WCF Message is represented by an envelope with headers and a body.  The header section consists of properties independent of transport such as addressing, security and reliable messaging.  The body contains the payload.  The rules for reading and writing the body and the headers are different: for example, the headers are always buffered in memory and may be accessed in any order any number of times, while the body may be read only once and may be streamed. WCF Message information model is closely aligned with SOAP.  Normally, when using SOAP, the Message body is mapped to the SOAP body and the Message headers are mapped to the SOAP headers.
If you are writing an adapter using WCF LOB Adapter SDK or writing custom channels, you should become intimately familiar with System.ServiceModel.Channels.Message class.  Here are a few useful links:

  • ·         Using the message class
  • ·         Service Station: WCF Messaging Fundamentals
    Consider a WCF Service with the following endpoint information and interface.  I used this sample implementation from WCF samples in path /basic/client/TypedClient.
    • ·         Contract = ICalculator
    • ·         Binding = BasicHttpBinding
    • ·         Address = http://sonua/servicemodelsamples/TypedClient/service.svc
    • ·         ServiceHost = IIS

 

using System;
using System.ServiceModel;
 
namespace Microsoft.ServiceModel.Samples
{
    // Define a service contract.
    [ServiceContract(Namespace="http://Microsoft.ServiceModel.Samples")]
    public interface ICalculator
    {
        [OperationContract]
        double Add(double n1, double n2);
        [OperationContract]
        double Subtract(double n1, double n2);
        [OperationContract]
        double Multiply(double n1, double n2);
        [OperationContract]
        double Divide(double n1, double n2);
    }
 
    // Service class which implements the service contract.
    public class CalculatorService : ICalculator
    {
              . . .
    }
}

 

Once the service is hosted in IIS, the WCF client can generate a proxy using Svcutil.Exe or [ASR].   The client application can use this proxy to communicate with the service endpoint.  The app.config on the client side defines the following client endpoint.

<configuration>
  <system.serviceModel>
    <client>
      <endpoint name=""
                address="http://localhost/servicemodelsamples/TypedClient/service.svc"
                binding="basicHttpBinding"
                contract="Microsoft.ServiceModel.Samples.ICalculator" />
    </client>
  </system.serviceModel>
</configuration>

Here is a portion of the client program –

using System;
 
using System.ServiceModel.Channels;
using System.ServiceModel;
 
namespace Microsoft.ServiceModel.Samples
{
    //The service contract is defined in generatedClient.cs, generated from the service by the svcutil tool.
 
    //Client implementation code.
    class Client
    {
        static void Main()
        {
            // Create a client
            CalculatorClient client = new CalculatorClient("CustomBinding");
 
            // Call the Add service operation.
            double value1 = 100.00D;
            double value2 = 15.99D;
            double result = client.Add(value1, value2);
            Console.WriteLine("Add({0},{1}) = {2}", value1, value2, result);
 
            // Call the Subtract service operation.
                 . . .
    }
}

The following figure shows how the message flows over the WCF bindings from client to the service, where

  • ·         Contract = ICalculator
  • ·         User Code = Client
  • ·         Typed Proxy = CalculatorClient
  • ·         Service = CalculatorService

Here is the WCF message that flows between the client and the service.  I captured the WCF Request Message and WCF Response Message by enabling Message Logging.   There are many other ways to “peek” at the WCF message going from client endpoint to service endpoint and vice-versa.

WCF Request Message

 

<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing">
  <s:Header>
    <a:Action s:mustUnderstand="1">http://Microsoft.ServiceModel.Samples/ICalculator/Add</a:Action>
    <a:MessageID>urn:uuid:cf15e5e2-03fd-4c54-9ead-c859ec56c91d</a:MessageID>
    <ActivityId CorrelationId="73c930d3-6585-4a60-95a5-f7d640ca2c67" xmlns="http://schemas.microsoft.com/2004/09/ServiceModel/Diagnostics">00000000-0000-0000-0000-000000000000</ActivityId>
    <a:ReplyTo>
      <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
    </a:ReplyTo>
  </s:Header>
  <s:Body>
    <Add xmlns="http://Microsoft.ServiceModel.Samples">
      <n1>5</n1>
      <n2>7</n2>
    </Add>
  </s:Body>
</s:Envelope>

 

WCF Response Message

 

<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
  <s:Header>
    <a:Action s:mustUnderstand="1" u:Id="_2" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">http://Microsoft.ServiceModel.Samples/ICalculator/AddResponse</a:Action>
    <a:RelatesTo u:Id="_3" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">urn:uuid:ba888253-b832-4ce6-92a4-0b8a8424371e</a:RelatesTo>
  </s:Header>
  <s:Body u:Id="_0" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
    <AddResponse xmlns="http://Microsoft.ServiceModel.Samples">
      <AddResult>12</AddResult>
    </AddResponse>
  </s:Body>
</s:Envelope>

 

Replace the BasicHttpBinding with a custom WCF binding creating using WCF LOB Adapter SDK
Let’s replace the “basicHttpBinding” to another WCF custom binding.  I will make this change in the configuration file by adding another client endpoint that uses a binding called “calcBinding”.  This binding will write the WCF request message (entire SOAP envelope) to a file and read the pre-canned responses from another file.   For simplicity sake, this binding doesn’t do any processing such as really adding two numbers.

<configuration>
  <system.serviceModel>
    <client>
      <endpoint name="CustomCalcBinding"
                address="calc:/"
                binding="calcBinding"
                contract="Microsoft.ServiceModel.Samples.ICalculator" />
      <endpoint name=""
                address="http://localhost/servicemodelsamples/TypedClient/Service.svc"
                binding="wsHttpBinding"
                contract="Microsoft.ServiceModel.Samples.ICalculator" />
    </client>
  </system.serviceModel>
</configuration>

In the client program, you can pass the configuration information to the proxy
CalculatorClient client = new CalculatorClient("CustomCalcBinding");
Then run the client program.  This time it will flow through the custom binding as shown in the diagram below and get the responses from the file system.


 

You can use WCF Channel Model to create the custom binding.  For this example, I have used WCF LOB Adapter SDK to create this binding.  Here is a step-by-step walkthrough:
1) Use the WCF LOB Adapter Code Generation Wizard to generate the classes needed to surface the code as a WCF Binding

 

  1. a.       Click on File > New > Project > Visual C# > WCF LOB Adapter
  2. b.      Enter “Calculator” as the name of the adapter
  3. c.       Use “calc” as the scheme and “Microsoft.Samples.Adapters” for project namespace. Click on Next.
  4. d.      Select Synchronous Outbound.  Ignore the other message flow patterns and metadata options. Click on Next.
  5. e.      Click Next on Adapter Properties page. 
  6. f.        Click Next on Connection Properties page.
  7. g.       Click on Finish

 

2)The following classes will be generated:

 

  1. a.       Calculator (derives from Adapter which in turn derives from TransportBindingElement)
  2. b.      CalculatorBinding (derives from AdapterBinding which in turn derives from Binding)
  3. c.       CalculatorBindingCollectionElement (derives from StandardBindingCollectionElement)
  4. d.      CalculatorBindingElement (derives from StandardBindingElement)
  5. e.      CalculatorBindingElementExtensionElement (derives from BindingElementExtensionElement)
  6. f.        CalculatorConnection
  7. g.       CalculatorConnectionFactory
  8. h.      CalcualtorConnectionUri
  9. i.         CalculatorHandlerBase
  10. j.        CalculatorOutboundHandler
  11. k.       CalculatorTrace

 

3)      Sign and build the project
4)      GAC the assembly
5)      Register the binding in machine.config (alternatively, you can also put this within app.config of your client)

 

 

 

  <system.serviceModel>
    <extensions>
      <bindingElementExtensions>
        <add name="calcTransport" type="Microsoft.Samples.Adapters.CalculatorBindingElementExtensionElement, Calculator, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b7727b35e1da7555"/>
      </bindingElementExtensions>
      <bindingExtensions>
        <add name="calcBinding" type="Microsoft.Samples.Adapters.CalculatorBindingCollectionElement, Calculator, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b7727b35e1da7555"/>
      </bindingExtensions>
    </extensions>
    <client>
      <endpoint binding="calcBinding" contract="IMetdataExchange" name="calc"/>
    </client>
  </system.serviceModel>

6)Implement the CalculatorConnection, CalculatorConnectionUri and CalculatorOutboundHandler class

In the connection-derived class, remove the not-implemented exceptions.

 

#region Using Directives
using System;
using System.IO;
using System.Collections.Generic;
using System.Text;
 
using Microsoft.ServiceModel.Channels.Common;
#endregion
 
namespace Microsoft.Samples.Adapters
{
    public class CalculatorConnection : IConnection
    {
        #region Private Fields
 
        private CalculatorConnectionFactory connectionFactory;
        private string connectionId;
 
        #endregion Private Fields
 
        /// <summary>
        /// Initializes a new instance of the CalculatorConnection class with the CalculatorConnectionFactory
        /// </summary>
        public CalculatorConnection(CalculatorConnectionFactory connectionFactory)
        {
            this.connectionFactory = connectionFactory;
            this.connectionId = Guid.NewGuid().ToString();
        }
 
        #region Public Properties
 
        /// <summary>
        /// Gets the ConnectionFactory
        /// </summary>
        public CalculatorConnectionFactory ConnectionFactory
        {
            get
            {
                return this.connectionFactory;
            }
        }
 
        #endregion Public Properties
 
        #region IConnection Members
 
        /// <summary>
        /// Closes the connection to the target system
        /// </summary>
        public void Close(TimeSpan timeout)
        {
        }
 
        /// <summary>
        /// Returns a value indicating whether the connection is still valid
        /// </summary>
        public bool IsValid(TimeSpan timeout)
        {
            return true;
        }
 
        /// <summary>
        /// Opens the connection to the target system.
        /// </summary>
        public void Open(TimeSpan timeout)
        {
        }
 
        /// <summary>
        /// Clears the context of the Connection. This method is called when the connection is set back to the connection pool
        /// </summary>
        public void ClearContext()
        {
        }
 
        /// <summary>
        /// Builds a new instance of the specified IConnectionHandler type
        /// </summary>
        public TConnectionHandler BuildHandler<TConnectionHandler>(MetadataLookup metadataLookup)
             where TConnectionHandler : class, IConnectionHandler
        {
 
            if (typeof(IOutboundHandler).IsAssignableFrom(typeof(TConnectionHandler)))
            {
                return new CalculatorOutboundHandler(this, metadataLookup) as TConnectionHandler;
            }
 
            return default(TConnectionHandler);
        }
 
        /// <summary>
        /// Aborts the connection to the target system
        /// </summary>
        public void Abort()
        {
        }
 
 
        /// <summary>
        /// Gets the Id of the Connection
        /// </summary>
        public String ConnectionId
        {
            get
            {
                return connectionId;
            }
        }
 
        #endregion IConnection Members
    }
}

 

In our sample, we are not really creating a complex connection string.

#region Using Directives
using System;
using Microsoft.ServiceModel.Channels.Common;
#endregion
 
namespace Microsoft.Samples.Adapters
{
    /// <summary>
    /// This is the class for building the CalculatorConnectionUri
    /// </summary>
    public class CalculatorConnectionUri : ConnectionUri
    {
        /// <summary>
        /// Initializes a new instance of the ConnectionUri class
        /// </summary>
        public CalculatorConnectionUri() { }
 
        /// <summary>
        /// Initializes a new instance of the ConnectionUri class with a Uri object
        /// </summary>
        public CalculatorConnectionUri(Uri uri) : base()
        {
        }
        /// <summary>
        /// Gets/Sets the Uri
        /// </summary>
        public override Uri Uri
        {
            get
            {
                return new Uri(Calculator.SCHEME + ":/");
            }
            set
            {
            }
        }
    }
}

I have pre-canned response WCF messages for Add, Subtract, Multiply and Divide operations.  I use the action field in the WCF message header to find the name of the incoming operation.

#region Using Directives
using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel.Channels;
 
using Microsoft.ServiceModel.Channels.Common;
using System.IO;
using System.Xml;
#endregion
 
namespace Microsoft.Samples.Adapters
{
    public class CalculatorOutboundHandler : CalculatorHandlerBase, IOutboundHandler
    {
        /// <summary>
        /// Initializes a new instance of the CalculatorOutboundHandler class
        /// </summary>
        public CalculatorOutboundHandler(CalculatorConnection connection
            , MetadataLookup metadataLookup)
            : base(connection, metadataLookup)
        {
        }
 
        #region IOutboundHandler Members
 
        /// <summary>
        /// Executes the request message on the target system and returns a response message.
        /// If there isn’t a response, this method should return null
        /// </summary>
        public Message Execute(Message message, TimeSpan timeout)
        {
            // Get the OperationMetadata definition for this incoming operation
            OperationMetadata om =
                this.MetadataLookup.GetOperationDefinitionFromInputMessageAction(message.Headers.Action, timeout);
            // If operation metadata is null, that means we don't have the knowledge
            // of the contract of this message. Just write request message to file
            // and read a "valid" response message from another file.
            if (om == null)
            {
                string action = message.Headers.Action;
                // Action is of format: http://Microsoft.Samples.WCF/ICalculator/{operationName}
                string requestFile = action.Substring(action.LastIndexOf('/'));
                string responseFile = requestFile + "Response";
 
                // Create a file output stream – this is where we will write the WCF request
 
                FileStream stream = new FileStream(@"c:\temp\" + requestFile + ".xml", FileMode.Create);
                XmlDictionaryWriter xdw = XmlDictionaryWriter.CreateTextWriter(stream);
                message.WriteMessage(xdw);
                xdw.Flush();
                stream.Close();
 
                // Create a file input stream – this is where we will read the WCF response from
                stream = new FileStream(@"c:\temp\" + responseFile + ".xml", FileMode.Open);
                XmlDictionaryReader xdr = XmlDictionaryReader.CreateTextReader(stream, new XmlDictionaryReaderQuotas());
                return Message.CreateMessage(xdr, 10000, MessageVersion.Default);
                // return Message.CreateMessage(version, outputMessageAction, xdr);
            }
 
            // If operation metadata is not null, this means the adapter cares to
            // know the metadata for this incoming operation, so it can invoke
            // a meaningful operation on the target system.
            // ...
 
            return null;
        }
 
        #endregion IOutboundHandler Members
    }
}

The above code snippet shows writing / reading the entire SOAP envelope.  What if we just want to write / read the SOAP body?
Sample Response Message

 

<?xml version="1.0" encoding="utf-16"?>
<AddResponse xmlns="http://Microsoft.ServiceModel.Samples">
  <AddResult>12</AddResult>
</AddResponse>

 

This is the code you can use to work with just the XML content in the body:

          if (om == null)
            {
                // Read request message
 
                string action = message.Headers.Action;
                // Input Action is of format: http://Microsoft.Samples.WCF/ICalculator/Add
                string requestName = action.Substring(action.LastIndexOf('/'));
                string responseName = requestName + "Response";
            
                FileStream stream = new FileStream(@"c:\temp\body\" + requestName + ".xml", FileMode.Create);
                XmlDictionaryWriter xdw = XmlDictionaryWriter.CreateTextWriter(stream);
                // Write just the XML message in the body
                message.WriteBodyContents(xdw);
                xdw.Flush();
                stream.Close();
 
                // Create response message
 
                StringBuilder outputString = new StringBuilder();
                XmlWriterSettings settings = new XmlWriterSettings();
                settings.OmitXmlDeclaration = true;
                // Create response message
                XmlWriter replywriter = XmlWriter.Create(outputString, settings);
                // - read the XML file and set it to the reply writer here
                stream = new FileStream(@"C:\temp\body\" + responseName + ".xml", FileMode.Open);
                XmlDictionaryReader xdr = XmlDictionaryReader.CreateTextReader(stream, new XmlDictionaryReaderQuotas());
                xdr.MoveToContent();
                string rawResponse = xdr.ReadOuterXml();
                replywriter.WriteRaw(rawResponse);
                replywriter.Close();
                XmlReader replyReader = XmlReader.Create(new StringReader(outputString.ToString()));
                // Output Action is of format: http://Microsoft.Samples.WCF/ICalculator/AddResponse
                string outputAction = message.Headers.Action + "Response";
                return Message.CreateMessage(MessageVersion.Default, outputAction, replyReader);
            }

This binding assumes that the metadata for the messages has already been defined through some means.  For example, in our scenario we already have the ICalculator contract generated from the CalculatorService hosted in IIS.  This adapter is metadata agnostic and doesn’t care much about the body of the message.  It provides a different “transport” or “protocol” implementation – like in above example it’s just getting a response from file system. 
Some of alternate (and realistic) scenarios can be -  

1)      The Calculator service is written using Java and hosted using Java Messaging Service (JMS) as a transport protocol.

2)      The Calculator service is provided by another back-end system that expects POX message instead of SOAP message.

3)   The Calculator service is a CORBA service hosted on a UNIX platform.

4)      The Calculator service is a WSE service hosted in IIS and the SOAP message needs to be ‘massaged’ a bit to/from WCF client.

This is assuming the ICalculator contract between the client and service remains unchanged.

What we have done so far is to create a pure transport adapter using the WCF LOB Adapter SDK!  But that’s not the best use of this Adapter SDK.  Adapter SDK is meant to be used in cases where the adapter can provide a “Contract” (WSDL/XML Schema) so that the client proxy can be generated from this contract.   (See diagram below)

 

And when the message flows from the proxy to the adapter, it can retrieve the operation metadata of incoming message from the adapter to retrieve more back-end details about this operation – e.g. backend function to call, the structure of the backend message, etc.   For example, if the calculator was a CICS program and the communication to this CICS program was through COMMAREA fixed length messages, then the execute() method of the CalculatorOutboundHandler can parse the incoming WCF message, convert to the format expected by the calculator CICS program, communicate with the CICS program using whatever protocol necessary, get the response and finally convert this response to the WCF response message.   When you realize that you need to parse the body of the message based on some notion of its metadata, please consider using the WCF LOB Adapter SDK to define the metadata for it, so that in the execute() method you can access the metadata and parse, build and transform WCF request/response accordingly.   

 

Click here for more information on generating metadata from the adapter.

 

posted @ 2016-03-07 10:35  吻上明天  阅读(303)  评论(0编辑  收藏  举报