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

NullTransport for WCF

4.91/5 (48 votes)
1 Oct 200712 min read 1   1.7K  
This article describes design, implementation and the usage of the custom in-process transport for Microsoft Windows Communication Foundation (WCF) model.

Contents

Note: The Workflow test case requires installation of the .NET Framework 3.5 (Orcas) Beta 2 version.

Features

  • Input/Output (One way)
  • Request/Reply
  • Duplex
  • Session
  • Transactional
  • Sync and Async connectivity fashions
  • No Encoder

Introduction

The Microsoft Windows Communication Foundation (WCF) represents a logical model of connectivity, where the connectivity between the service and its consumer is abstracted to the stack of the channels on top of the physical transport layer. Based on the binding stack elements and physical transport on both ends of the connectivity, we can define how the message can flow between the business layers in terms of the message exchange pattern (MEP). Note, the business layers do not need to know about each other, such as where they are hosted, located and connected, therefore we can say, the business layers are logical connected.

The business connected layers represented by the client (proxy) and service can live in the same appDomain or on the different appDomains in the same process, out of the process or across the machine. Having the logical connectivity model in the application architecture enables it to encapsulate the business layers from the connectivity driven by the metadata.

The WCF model introduces several common transports and bindings to create a sync and async connectivity with a variant of the message exchange patterns such as Input/Output, Request/Reply, Session, Duplex, etc. over the different physical transports such as TCP, HTTP, MSMQ, Pipe, etc. It is a great open connectivity paradigm with the ability to customize and extend all elements based on the business needs.

Based on the physical transport, the business can live in the same tier or can be decoupled across an Enterprise Network using the principle of the Service Oriented Architecture (SOA) tenets.

This article is focused on the WCF connectivity model within the same appDomain. The current version of the .netfx 3.0 and also upcoming version 3.5 (Orcas) uses the named pipe for in-process communication between the service and its consumer.

The following picture shows a netNamedPipeBinding WCF connectivity:

Image 1

The logical connectivity between the business layers is mapped to the WCF model described by their service contract. This description represents the metadata of the connectivity such as Address, Binding and Contract (ABC). Basically, the client and service using the same channel stack banded by Protocol, Encoder and Transport layers. The client channel is connected to the business layer by Proxy (using the transparent proxy pattern) and on the service side, the Dispatch will invoke a service operation method. More details about this great communication paradigm can be found in the MSDN documentation and .NET forum. I assume that you already read the documentation and have some working knowledge and experience with WCF programming.

As I mentioned earlier, this article focuses on the in-process communication, therefore let's continue with this focus. When the client invokes the service operation on the transparent CLR proxy, the operation is mapped into the message object by the Protocol layer (depends on the message exchange pattern) and passing the message to the Encoder for its serialization.

The Transport layer will take this binary stream or formatted text over the appDomain using a specific physical media, in our case it is a local Named Pipe running in the kernel mode, which can be reachable by any process on the same machine. The service's Encoder must decode the incoming stream/text into the message object and pass it to the Protocol channels. At the end, the Dispatch's Operation Invoker will invoke a method on the service instance.

So far so good, the netNamedPipeBinding allows to connect and transport binary message object using the local named pipe resource via a kernel mode between the service and client in a fast and reliable sync/async manner. So, what is the point?

The point is described in the following picture; if the client and service are located in the same appDomain. Do we need to go out and back into the same appDomain to handle the internal domain communications via inter-process kernel resources?

The following picture can answer for in-process connectivity:

Image 2

As you can see, the physical layer of the connectivity represented by NULL Transport will take a clone message object and pass it directly to the service protocol layer. This logic is similar to the router, when the output channel is routing a message to the correct Input channel. Note, this NULL connectivity is fully transparent between the service and client and there is no restriction on the service to have multiple endpoints with a different ABC for binding.

For example: The service with two endpoints, let's say the first one is netTcpBinding and the other one a custom binding with the Null Transport can handle the public and private connectivity very efficiently.

Another example of the NullTransport usage is the incoming netfx 3.5 version (currently in beta2), where WCF and WF models are integrated into one common WorkflowService model. This model via new Receive and Send Activities enables to communicate with workflow based on the service contract in the context manner. The business encapsulated into the type/XOML workflows can be orchestrated in the logical model based on the connectivity metadata without the knowledge where they are physically hosted.

The following picture shows the connectivity within the appDomain for WCF and WorkflowServices. In this case, the services and clients are logically connected to the appDomain bus represented by the NULL Transport layer:

Image 3

The WorkflowServices are a great step into the distributed workflow model, where a workflow can invoke another workflow in the context message exchange pattern on the internal or public Enterprise Service Bus.

That's all for the introduction of the Null Transport, I hope you captured having this transport in-process communication model when the correct business layers in the tier can talk to each other using the same communication model as between the tiers.

I assume you have knowledge of WCF and its extensibility; therefore I will focus more on the concept and implementation of the transport layer than on the hierarchy of the custom built channels. Ok, let's start with the concept and follow up its implementation.

Concept and Implementation

The concept of the NullTransport is based on the routing (dispatching) the cloned output message object to the specific listener based on the EndpointAddressMessageFilter data. Each opened listener will subscribe its EndpointAddressMessageFilter into the MessageFilterTable stored in the process data slot. Based on this key, the MessageFilterTable will return a reference to the listener and its private InputQueueChannel for enqueueing the message object. Once the message arrives into the input async queue, the channel will process it like the message generated by Encoder layer.

The following picture shows the concept of the NullTransport with four listeners related to the MEP:

Image 4

When the Listener is constructed, the EndpointAddressMessageFilter is created for full local endpoint address and later it will be used in the AcceptChannel process.

The following code snippet shows these parts of the NullInputChannelListener implementation:

C#
public NullInputChannelListener(NullTransportBindingElement element, 
    BindingContext context): base(context.Binding)
{
    _element = element;
    _context = context;
  
    Uri listenUri = new Uri(context.ListenUriBaseAddress, 
        context.ListenUriRelativeAddress);
    _localAddress = new EndpointAddress(listenUri);
    _filter = new EndpointAddressMessageFilter(_localAddress);
}

protected override IInputChannel OnAcceptChannel(TimeSpan timeout)
{
    if (base.State == CommunicationState.Opened)
    {
        if (_currentChannel != null)
        { 
            // we are supporting only one channel in the listener
            _waitChannel.WaitOne(int.MaxValue, true);

            lock (ThisLock)
            {
                // re-open channel
                if (_currentChannel.State == CommunicationState.Closed &&
                base.State == CommunicationState.Opened)
                {
                    _currentChannel = 
                        new NullInputChannel(this, _localAddress);
                    _currentChannel.Closed += 
                        new EventHandler(OnCurrentChannelClosed);
                }
            }
        }
        else
        {
            lock (ThisLock)
            {
                // open channel at first time
                _currentChannel = new NullInputChannel(this, _localAddress);
                _currentChannel.Closed += 
                    new EventHandler(OnCurrentChannelClosed);
                NullListeners.Current.Add(_filter, this);
            }
        }
    }
    return _currentChannel;
}

The NullListener.Current is a static method to subscribe/unsubscribe a specific Listener to the MessageFilterTable and dispatch a message to the Listener's InputQueueChannel.

Once the Listener has been subscribed, the message can be dispatched. The following code snippet shows sending a message from the OutputChannel:

C#
public void Send(Message message, TimeSpan timeout)
{
    // double check
    base.ThrowIfDisposedOrNotOpen();

    // add remote address to the message header
    base.RemoteAddress.ApplyTo(message);

    // create buffered copy
    MessageBuffer buffer = message.CreateBufferedCopy(int.MaxValue);
    Message message2 = buffer.CreateMessage();

    // dispatch to the listener
    ThreadPool.QueueUserWorkItem
        (new WaitCallback(this.DispatchToListener), message2); 
}

protected virtual void DispatchToListener(object state)
{
    try
    {
        NullListeners.Current.Dispatch<NullInputChannelListener>
            (state as Message);
    }
    catch (Exception ex)
    {
        (state as Message).Close();
    }
}

As you can see, the cloned message is sent to the dispatcher in an async manner without waiting for its response. This is the design for fire & forget message exchange pattern represented by the Input and Output channel.

Building the custom WCF channel/transport is a straightforward task divided into the several layers with the interface plumbing patterns and some common behavior located in the CommunicationObject base class.

The following picture shows a basic boilerplate for custom NullChannels for message exchange patterns such as Input/Output, InputSession/OutputSession, Request/Reply and RequestSession/ReplySession:

Image 5

The first part of this boilerplate is related to the plug-in of the custom transport to the binding collection. The NullTransportElement must be added into the <bindingElementExtensions> element in the config file or programmatically done prior of the usage of this custom binding. Once we have our custom binding in the extensions, it can be recognized by endpoint and can call the plumbing pattern like is shown in the above picture.

The NullTransportBindingElement handles building the channels for Listener (Service) and Factory (Proxy) Channels based on the service contract. For instance: if the service contract has a one way operation, the Input/Output message exchange pattern will be applied for building the channels.

The following picture shows a class diagram for NullChannels. These channels are initiated by Factory and Listener layers:

Image 6

The simple MEP, such as one directional sending a message to the input channel can be completed directly by enqueueing of the cloned message object into the input channel queue in the fire & forget manner - see the above code snippets. The plumbing layers are represented by IOuputChannel and IInputChannel patterns.

To receive a response on the request described in the Request/Reply MEP, the plumbing layers must be driven by IRequestChannel and IReplyChannel interfaces and RequestContext object.

The RequestContext object represents a "vehicle" for handling a message object between the request and reply tasks for their synchronization and message passing. The following picture shows an implementation of the RequestContext abstract class for NullTransport:

Image 7

The NullAsyncRequestContext object is designed for plumbing inner's channel layers such as NullRequestChannel and NullReplyChannel respectively NullRequestSessionChannel and NullReplySessionChannel into the WCF channel protocol. The RequestContext object can be handled by both sides asynchronously or synchronously using the blocking manner.

The following code snippet shows the Request implementation in the NullRequestChannel:

C#
public IAsyncResult BeginRequest(Message message, 
    AsyncCallback callback, object state)
{
    return this.BeginRequest
        (message, base.DefaultSendTimeout, callback, state);
}

public IAsyncResult BeginRequest(Message message, TimeSpan timeout, 
    AsyncCallback callback, object state)
{
    base.ThrowIfDisposedOrNotOpen();
    base.RemoteAddress.ApplyTo(message);

    NullAsyncRequestContext request = 
        new NullAsyncRequestContext(message,timeout,callback,state);
    this.DispatchToListener(request);
    base.PendingRequests.Add(request);
    return request;
}  
   
public Message EndRequest(IAsyncResult result)
{
    return NullAsyncRequestContext.End(result);
}

public Message Request(Message message)
{
    return this.Request(message, base.DefaultSendTimeout);
}

public virtual Message Request(Message message, TimeSpan timeout)
{
    base.ThrowIfDisposedOrNotOpen();
    base.RemoteAddress.ApplyTo(message);

    NullAsyncRequestContext request = 
        new NullAsyncRequestContext(message, timeout);
    base.PendingRequests.Add(request);

    try
    {
        ThreadPool.QueueUserWorkItem
            (new WaitCallback(this.DispatchToListener), request); 
        return request.WaitForReply();
    }
    finally
    {
        base.PendingRequests.Close(request);
    }
}

protected virtual void DispatchToListener(object state)
{
    try
    {
        NullListeners.Current.Dispatch<NullReplyChannelListener>
            (state as NullAsyncRequestContext);
    }
    catch (Exception ex)
    {
        (state as NullAsyncRequestContext).Abort(ex);
    }
}

In the case of BeginRequest, the method will create a RequestContext object with async arguments that are dispatched to the specific ReplyChannel without waiting for its reply. When the service operation is finished, the reply message is sent to the RequestContext by invoking the following method:

C#
public override void Reply(Message message, TimeSpan timeout)
{
    lock (ThisLock)
    {
        ThrowIfInvalidReply();

        if (message != null)
        {
            MessageBuffer buffer = message.CreateBufferedCopy(int.MaxValue);
            _response = buffer.CreateMessage();
        }
        else
        {
            _response = null;
        }
        _replySent = true;
    
        if (_waitForReply != null)
        {
            _waitForReply.Set();
        }
    }

    if (_callback != null)
    {
        // call callback for reply is done
        _callback(this);
    }
}

The responsibility of the above method is to clone the message object and invoke an async callback to signal that the operation is done and the response is ready in the RequestContext object. This process is completed by the EndRequest method, where the reply message is returned to the requester (NullRequestChannel with an anonymous ReplyTo address).

The following picture shows a request and reply message captured by MessageInspector for <reliableSession> over <nullTransport> binding:

Image 8

Test

The following picture shows the solutions for NULL Transport implementation including test projects for .netfx 3.0 and 3.5 versions. Note, the solution for .netfx 3.5 is only for the WorkflowService test case and it requires the installation of .netfx 3.5 version (currently in beta 2). This solution references to the NullChannelLib.dll assembly built under the netfx 3.0 version.

As you can see, the NullChannelLib project contains several files located in the folders based on communication layers and MEPs.

Image 9

For test purposes, each test console application has its own config file. The following picture shows an example of the configuration for nullTransport custom binding. Note, the bindingElementExtension is required to use this transport.

Image 10

The message exchange patterns via the custom NULL Transport can be tested individually using the test projects such as Test_Duplex, Test_Session, Test_Tx (Transaction) and Test Workflow. The solution also contains a Logger for displaying WCF messages on the console screen.

Note, the config file contains a diagnostics configuration for tracing the messages across the WCF model. Please find the patch for this tracing in the config file and use the \Microsoft SDKs\Windows\V6.1\Bin\SvcTraceViewer.exe utility to display the trace log messages.

Test_Workflow

I selected this test to describe the capability of the NULL Transport within the same AppDomain where the WCF model is used to connect the business objects located in the different layers. The console application represents a UI and host process for the business objects located in the workflows and service. Using the configuration file, the layers can be connected via the custom net.null transport or others, based on the application needs.

Image 11

The first ReceiveActivity in the WorfklowService enables the creation of the workflow instance and its instanceId will flow in the context during the valid session, therefore the second request message from the console program will be dispatched during the next ReceiveActivity.

The following picture shows a property of the first ReceiveActivity:

Image 12

The purpose of the following picture is shown in the message exchange with a first ReceiveActivity. The Request/Reply message exchange pattern is demonstrated by passing the context header with a workflow instance id back to the caller. In addition, the screen shows workflow runtime events such as created, started, idled and persisted:

Image 13

Note, the WorkflowService will automatically create a workflow instance id for you. There is no option in the current version of the netfx 3.5 to pass a context with a specific id in the first request call to the WorkflowService. However, I implemented a small workaround (CreateWorkflowAttribute.cs) for this issue based on the custom operation attribute [CreateWorkflow] - see the following code snippet:

C#
[ServiceContract(SessionMode = SessionMode.Required)]
public interface ITest
{
    [OperationContract]
    [TransactionFlow(TransactionFlowOption.Allowed)]
    void Ping(int value);

    [OperationContract(IsInitiating = true)]
    [TransactionFlow(TransactionFlowOption.Allowed)]
    [CreateWorkflow]
    string Ping2(int value);
}

Now, the client can create the instanceId and pass it via the request message to the WorkflowService. The following code snippet shows an example of creating a context header for an outgoing message:

C#
using (OperationContextScope scope = 
    new OperationContextScope((IContextChannel)channel))
{
    string instanceId = Guid.NewGuid().ToString();
  
    Dictionary<XmlQualifiedName, string> dic = 
        new Dictionary<XmlQualifiedName, string>();
  
    dic.Add(new XmlQualifiedName("InstanceId", 
        "http://schemas.microsoft.com/ws/2006/05/context"), instanceId);
  
    ContextMessageProperty mp = new ContextMessageProperty(dic);
    OperationContext.Current.OutgoingMessageProperties.Add
        ("ContextMessageProperty", mp);

    string response = channel.Ping2(loop);

    // ...
}

and the outgoing request message will look as it is shown in the following code snippet:

XML
<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">http://tempuri.org/ITest/Ping2</a:Action>
    <a:MessageID>urn:uuid:ce8e478c-ab6a-401d-958b-d1d3de2f972d</a:MessageID>
    <a:ReplyTo>
      <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
    </a:ReplyTo>
    <Context xmlns="http://schemas.microsoft.com/ws/2006/05/context">
      <InstanceId>044a3c30-3558-47de-8647-2c0d9b4f9359</InstanceId>
    </Context>
    <a:To s:mustUnderstand="1">net.null://localhost/gaga</a:To>
  </s:Header>
  <s:Body>
    <Ping2 xmlns="http://tempuri.org/">
      <value>0</value>
    </Ping2>
  </s:Body>
</s:Envelope>

Remoting Vs. namedPipeTransport Vs. nullTransport

The fast standard transport from WCF model is via namedPipeTransport using the local named pipe running in kernel mode. As I mentioned earlier, the message must be encoded via this transport. The Test_Session program can be used to compare the performance between the named pipe and null transport (after a small update in the config file related to the namedPipeTransport is completed).

On my Dell multiprocessors (4 cores) machine, the null transport is faster than pipe in the score of 2.462 ms to 3.062 ms. Also, I built a similar test for remoting object and the result showed me that the null transport is approximately 8% faster than TCP remoting channel. Note, the test was done for a very small message body - see the ITest contract. In the case of large messages, the null transport will perform better because there is no encoder layer in the channel.

Conclusion

This article described the in-process of the WCF Transport without using the Encoder layer. This efficient binding enables the usage of a common communication WCF paradigm for connectivity between the business layers located in the same appDomain. Using the config file; the connectivity can be easily changed administratively for crossing the boundary. The upcoming new .netfx 3.5 version related to the common model for connected systems with a workflow behind the service is a suitable candidate for NullTransport binding, where the business model can be logically run in one or more processes based on business needs.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here