Contents
Note: The Workflow test case requires installation of the .NET Framework 3.5 (Orcas) Beta 2 version.
- Input/Output (One way)
- Request/Reply
- Duplex
- Session
- Transactional
- Sync and Async connectivity fashions
- No Encoder
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:
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:
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 WorkflowService
s. In this case, the services and clients are logically connected to the appDomain bus represented by the NULL Transport layer:
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.
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:
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:
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)
{
_waitChannel.WaitOne(int.MaxValue, true);
lock (ThisLock)
{
if (_currentChannel.State == CommunicationState.Closed &&
base.State == CommunicationState.Opened)
{
_currentChannel =
new NullInputChannel(this, _localAddress);
_currentChannel.Closed +=
new EventHandler(OnCurrentChannelClosed);
}
}
}
else
{
lock (ThisLock)
{
_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
:
public void Send(Message message, TimeSpan timeout)
{
base.ThrowIfDisposedOrNotOpen();
base.RemoteAddress.ApplyTo(message);
MessageBuffer buffer = message.CreateBufferedCopy(int.MaxValue);
Message message2 = buffer.CreateMessage();
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 NullChannel
s for message exchange patterns such as Input/Output, InputSession/OutputSession, Request/Reply and RequestSession/ReplySession:
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 NullChannel
s. These channels are initiated by Factory
and Listener
layers:
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
:
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
:
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:
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)
{
_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:
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.
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.
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.
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
:
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:
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:
[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:
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:
<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.
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.