WCF Extensibility Guidance - Extending the WCF Channel Model

Jesus Rodriguez
Pablo Cibraro
Tellago, Inc

Published: October 2009

Articles in this series

Extending the WCF Channel Model

The WCF channel model represents the main entry point for implementing new transports, message encoding mechanisms or protocols not supported out of the box in the WCF infrastructure.  In other words, it provides a friendly mechanism to extend what happens “on the wire”. It works at a much deeper level in the stack than the service model discussed in the next chapters.

The three main components of the channel model are protocols, encodings and transports, and any of these can be customized or extended to support new functionality.

These three main components are used to create a communication stack, also called a channel stack, which is mainly composed of different abstraction layers or channels. As any architecture based on layers, each channel in the stack provides some abstraction about the implementation of a protocol or transport, and exposes this abstraction to the channel directly below it.

Messages flow through this communication stack as Message objects, and depending on the functionality of each channel, they can modify or include metadata in the messages, provide a message encoder to transform the messages into a stream of bytes, or simply send or receive the messages through a transport.

The bottom channel is called the transport channel.  The transport channel knows how to deal with the underlying details of a specific transport implementation, which could be for instance HTTP, TCP, or MSMQ to name a few. It is basically responsible for sending/receiving messages (already encoded) to/from other parties.

The encoding channel is responsible for providing a Message Encoder implementation, which will encode or decode the message into a format expected or supported by the transport channel.

Above the encoding channel there can be any number of protocol channels that focus on different communication functions such as security or reliability among others. Protocol channels operate on messages flowing through them, typically applying transformations like adding headers, encrypting or signing some message parts or perhaps sending or receiving their own protocol messages.

Channels

In order to support different communication patterns, channels in WCF come in different “shapes”. The shape of a channel used depends upon the communication pattern being implemented:

  • Datagram:There is a unidirectional stream of messages between two parties. One party sends messages, and the other party receives them. This is typical scenario for a queuing system, one party that puts messages into a queue, and other party that takes and processes messages from the same queue.

  • Send-Reply:Both parties can send or receive messages, but there is a semantic restriction on when messages can be sent. One party, the sender, can send request messages to another party, the receiver. However, the receiver can only reply back to the sender after having received a request. An http service is common example of this pattern.

  • Duplex:Each party can send or receive messages independently. Once a bidirectional communication has been established between the two parties, both sides of the connection can read and write messages at any time.

  • Session:An additional variation of these three patterns that uses a session. Sessions in the context of WCF are always explicitly initiated and terminated by the client, and represent a way to correlate a group of messages into a conversation.The nature in which the messages are correlated is tied to the channel implementation. For instance, some session-based channels might correlate messages based on a shared network connection while other channels might use some information available in the messages. Some parts of the WCF infrastructure rely on sessions in order to work properly - “Callback contracts” for example.A session can also be associated to a specific service implementation instance, so the service can maintain state in member variables in behalf of the client.

In addition to channels and their “shapes”, a developer also needs to deal with channel managers. A channel manager is responsible for creating channels and closing them when they are no longer used.

There are two kinds of channel managers:   channel listeners and channel factories.

  • A channel listener creates channels to receive messages from the layer below or from the network.Therefore, all the received messages flow between the channels created by the configured channel listeners. From the point of view of a developer, every channel is associated with an internal queue. The channel puts messages in that queue as they receive them from the layer below or the network. Afterwards, these messages are passed to the layer above when the channel is asked to do so.Channel listeners are also responsible for closing any channel they created.

  • A channel factory creates channels to send messages to the layer below or the transport. It is responsible for getting messages from the layer above, performing some processing on those messages, and finally passing them to the layer below. Similar to channel listeners, channel factories must also close any channel they created.

Scenarios

Developers should consider implementing custom channels in scenarios that require custom encoding mechanisms that can not be addressed by the default encoders, custom protocols or any transport not supported out of the box in WCF for sending or receiving messages. The following list includes some of these possible scenarios.

  • Message chunking:In scenarios where large messages must be transferred between a client and the service, it is often desirable to limit the amount of memory used to buffer those messages. Since some protocols require buffering the entire messages, reliable messaging and security are two examples, message streaming is not always a feasible option. For those scenarios, a large message can be spit out into smaller pieces called chunks.These chunks are sent across the transport one at a time and the large message is recreated on the other side as the chunks are received. A custom channel can be used to implement this protocol to perform chunking and de-chunking of large messages.
  • “In-Process” or UDP transports:An application might need to use UDP to communicate with the other end, or perhaps it only needs to communicate with a service living in the same process and machine, with no need to cross a process boundary.These are good examples of transports not shipped as part of WCF. We can create a custom channel that provides an implementation for these transports.
  • .NET Service bus:The channels included as part of the new Microsoft .NET Service bus can also be considered a good example of an implementation that uses a custom transport to communicate with the Service Bus hosted in the cloud. All the underlying details of the transport, or the protocol handshaking required to open a connection with the bus, are completely implemented in the channel, and therefore hidden from the application that will consume the service bus.
  • Other custom message protocol implementations:A company might decide to implement any of the WS-* protocols not included as part of the WCF, such as WS-Eventing or WS-Pooling.All the details of these protocols can be implemented in a custom channel.

Implementation

As explained in the previous sections, several steps are required for building a new channel, and plugging it into the WCF infrastructure. These steps are summarized below:

  1. Implement a channel listener by deriving the base class System.ServiceModel.Channels.ChannelListenerBase<T>, where T represents the kind of inner channel that this listener will use to receive or process the messages.Possible types for the inner channel are System.ServiceModel.Channels.IInputChannel for communications based on datagrams,System.ServiceModel.Channels.IReplyChannel for using a send and reply communication pattern, System.ServiceModel.Channels.IDuplexChannel for duplex communications, or finally all their variations for session enabled communications, System.ServiceModel.Channels.IInputSessionChannel, System.ServiceModel.Channels.IReplySessionChannel and System.ServiceModel.Channels.IDuplexSessionChannel.
  2. Implement a channel factory by deriving the class System.ServiceModel.Channels.ChannelFactoryBase<T>, where T represents the kind of inner channel that the factory will use to send or process the messages. Possible types for the inner channels are System.ServiceModel.Channels.IOutputChannel for communications based on datagrams, System.ServiceModel.Channels.IRequestChannel for using a send and reply communication pattern, System.ServiceModel.Channels.IDuplexChannel for duplex communication, or finally all their variations for session enabled communications, System.ServiceModel.Channels.IOutputSessionChannel, System.ServiceModel.Channels.IRequestSessionChannel and System.ServiceModel.Channels.IDuplexSessionChannel.
  3. Implement custom binding elements that wrap these channel managers, and give developers an interface to interact with the custom channels.You can implement a custom binding element by deriving the abstract class System.ServiceModel.Channels.BindingElement, and implementing the abstract methods to return the channel manager implementations.

The following code shows the implementation of a custom ChannelListener class that internally creates inner channels that follow a duplex communication pattern with sessions.

internal class CustomChannelListener : ChannelListenerBase<IDuplexSessionChannel>

{

    IChannelListener<IDuplexSessionChannel> innerListener;

    public CustomChannelListener(IChannelListener<IDuplexSessionChannel> innerListener)

        : base()

    {

        this.innerListener = innerListener;

    }

    public override Uri Uri

    {

        get { return innerListener.Uri; }

    }

    public override T GetProperty<T>()

    {

        return innerListener.GetProperty<T>();

    }

    protected override void OnOpen(TimeSpan timeout)

    {

        innerListener.Open(timeout);

    }

    protected override void OnAbort()

    {

        innerListener.Abort();

    }

    protected override void OnClose(TimeSpan timeout)

    {

        innerListener.Close(timeout);

    }

    protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state)

    {

        return innerListener.BeginOpen(timeout, callback, state);

    }

    protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state)

    {

        return innerListener.BeginClose(timeout, callback, state);

    }

    protected override void OnEndClose(IAsyncResult result)

    {

        innerListener.EndClose(result);

    }

    protected override void OnEndOpen(IAsyncResult result)

    {

        innerListener.EndOpen(result);

    }

    protected override IDuplexSessionChannel OnAcceptChannel(TimeSpan timeout)

    {

        IDuplexSessionChannel innerChannel = innerListener.AcceptChannel();

        return WrapChannel(innerChannel);

    }

    protected override IAsyncResult OnBeginAcceptChannel(TimeSpan timeout, AsyncCallback callback, object state)

    {

        return innerListener.BeginAcceptChannel(timeout, callback, state);

    }

    protected override IDuplexSessionChannel OnEndAcceptChannel(IAsyncResult result)

    {

        IDuplexSessionChannel innerChannel = innerListener.EndAcceptChannel(result);

        return WrapChannel(innerChannel);

    }

    IDuplexSessionChannel WrapChannel(IDuplexSessionChannel innerChannel)

    {

        if (innerChannel == null)

        {

            return null;

        }

        else

        {

            return new CustomDuplexSessionChannel(this, innerChannel);

        }

    }

    protected override IAsyncResult OnBeginWaitForChannel(TimeSpan timeout, AsyncCallback callback, object state)

    {

        return innerListener.BeginWaitForChannel(timeout, callback, state);

    }

    protected override bool OnEndWaitForChannel(IAsyncResult result)

    {

        return innerListener.EndWaitForChannel(result);

    }

    protected override bool OnWaitForChannel(TimeSpan timeout)

    {

        return innerListener.WaitForChannel(timeout);

    }

}

Sample WCF ChannelListener

Similar to channel listener, the corresponding channel factory can be implemented as illustrated in the following code.

internal class CustomChannelFactory : ChannelFactoryBase<IDuplexSessionChannel>

{

    IChannelFactory<IDuplexSessionChannel> innerChannelFactory;

    internal CustomChannelFactory(IChannelFactory<IDuplexSessionChannel> innerChannelFactory)

    {

        this.innerChannelFactory = innerChannelFactory;

    }

    protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state)

    {

        return innerChannelFactory.BeginOpen(timeout, callback, state);

    }

    protected override void OnEndOpen(IAsyncResult result)

    {

        innerChannelFactory.EndOpen(result);

    }

    protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state)

    {

        return innerChannelFactory.BeginClose(timeout, callback, state);

    }

    protected override void OnEndClose(IAsyncResult result)

    {

        innerChannelFactory.EndClose(result);

    }

    protected override void OnOpen(TimeSpan timeout)

    {

        innerChannelFactory.Open(timeout);

    }

    protected override void OnAbort()

    {

        innerChannelFactory.Abort();

    }

    protected override void OnClose(TimeSpan timeout)

    {

        innerChannelFactory.Close(timeout);

    }

    protected override IDuplexSessionChannel OnCreateChannel(EndpointAddress address, Uri via)

    {

        IDuplexSessionChannel innerChannel = this.innerChannelFactory.CreateChannel(address, via) as IDuplexSessionChannel;

        CustomDuplexSessionChannel channel = new CustomDuplexSessionChannel(this, innerChannel);

        return channel;

    }

    public override T GetProperty<T>()

    {

        return innerChannelFactory.GetProperty<T>();

    }

}

Sample WCF Channel Factory

The custom channel created by channel managers in the code above is an implementation of a System.ServiceModel.Channels.IDuplexSessionChannel.  There is a base interface Sytem.ServiceModel.Channels.IChannel that all channels must implement to manage a state machine with a set of states, state transition methods and state transition events that abstract the underlying implementation of the channel communication. All the interfaces for implementing a channel discussed previously derive from this main interface. WCF already provides a basic implementation of this interface in the abstract class System.ServiceModel.ChannelBase

You can start developing a channel by either implementing the IChannel interface or deriving your implementation from the base class ChannelBase.

The following code illustrates a basic implementation of the channel used by the ChannelFactory and ChannelListener discussed previously,

internal class CustomDuplexSessionChannel : ChannelBase, IDuplexSessionChannel

{

    IDuplexSessionChannel innerChannel;

    ManualResetEvent receiveStopped = new ManualResetEvent(true);

    ManualResetEvent currentMessageCompleted = new ManualResetEvent(true);

    ManualResetEvent sendingDone = new ManualResetEvent(true);

    internal CustomDuplexSessionChannel(ChannelManagerBase channelManager,

        IDuplexSessionChannel innerChannel)

        : base(channelManager)

    {

        this.innerChannel = innerChannel;

    }

    #region CommunicationObject overrides

    protected override void OnOpen(TimeSpan timeout)

    {

        innerChannel.Open(timeout);

    }

    protected override void OnClose(TimeSpan timeout)

    {

        //wait for receive to stop so we can have a clean shutdown

        TimeoutHelper timeoutHelper = new TimeoutHelper(timeout);

        if (this.receiveStopped.WaitOne(TimeoutHelper.ToMilliseconds(timeoutHelper.RemainingTime()), false))

        {

            innerChannel.Close(timeoutHelper.RemainingTime());

        }

        else

        {

            throw new TimeoutException("Close timeout exceeded");

        }

    }

    protected override void OnAbort()

    {

        innerChannel.Abort();

    }

    protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state)

    {

        return innerChannel.BeginOpen(timeout, callback, state);

    }

    protected override void OnEndOpen(IAsyncResult result)

    {

        innerChannel.EndOpen(result);

    }

    protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state)

    {

        return innerChannel.BeginClose(timeout, callback, state);

    }

    protected override void OnEndClose(IAsyncResult result)

    {

        innerChannel.EndClose(result);

    }

    public override T GetProperty<T>()

    {

        return innerChannel.GetProperty<T>();

    }

    #endregion

    #region IInputChannel Members

    public Message Receive(TimeSpan timeout)

    {

        ThrowIfDisposedOrNotOpen();

        //if we're receiving chunks, wait till that's done

        TimeoutHelper helper = new TimeoutHelper(timeout);

        if (!this.currentMessageCompleted.WaitOne(TimeoutHelper.ToMilliseconds(timeout),false))

        {

            throw new TimeoutException("Receive timed out waiting for previous message receive to complete");

        }

        //call receive on inner channel

        Message message = innerChannel.Receive(helper.RemainingTime());

        if (message != null)

        {

            MessageBuffer buffer = message.CreateBufferedCopy(int.MaxValue);

            Message cloned = buffer.CreateMessage();

            //For purposes of this example, the channel only writes the message

            //content in the console

            XmlDictionaryReader reader = cloned.GetReaderAtBodyContents();

            string content = reader.ReadOuterXml();

            Console.WriteLine("Message Received");

            Console.WriteLine(content);

            return buffer.CreateMessage();

        }

        return message;

    }

    public Message Receive()

    {

        return this.Receive(base.DefaultReceiveTimeout);

    }

    public bool TryReceive(TimeSpan timeout, out Message message)

    {

         ThrowIfDisposedOrNotOpen();

         message = null;

         bool timedOut = false;

         try

         {

             message = this.Receive(timeout);

         }

         catch (TimeoutException)

         {

             timedOut = true;

         }

         return (!timedOut);

    }

    public bool WaitForMessage(TimeSpan timeout)

    {

        return innerChannel.WaitForMessage(timeout);

    }

    #endregion

    #region IOutputChannel Members

    public void Send(Message message)

    {

        Send(message, this.DefaultSendTimeout);

    }

    public void Send(Message message, TimeSpan timeout)

    {

        ThrowIfDisposedOrNotOpen();

        if (!this.sendingDone.WaitOne(TimeoutHelper.ToMilliseconds(timeout), false))

        {

            throw new TimeoutException("Send timed out waiting for the previous message send to complete");

        }

        lock (base.ThisLock) // synchronized state transitions are not allowed while sending

        {

            ThrowIfDisposedOrNotOpen();

            innerChannel.Send(message, timeout);

        }

    }

    public IAsyncResult BeginSend(Message message, TimeSpan timeout, AsyncCallback callback, object state)

    {

        throw new Exception("The method or operation is not implemented.");

    }

    public IAsyncResult BeginSend(Message message, AsyncCallback callback, object state)

    {

        throw new Exception("The method or operation is not implemented.");

    }

    public void EndSend(IAsyncResult result)

    {

        throw new Exception("The method or operation is not implemented.");

    }

    public EndpointAddress RemoteAddress

    {

        get { return innerChannel.RemoteAddress; }

    }

    public Uri Via

    {

        get { return innerChannel.Via; }

    }

    #endregion

    #region ISessionChannel<IDuplexSession> Members

    public IDuplexSession Session

    {

        get { return innerChannel.Session; }

    }

    #endregion

    #region IInputChannel advanced members

    Implementation has been ommited for brevity…

    #endregion

}

Sample WCF IDuplexSessionChannel

As you can see in the code above, this simple implementation only prints the received messages in the standard output console.

Finally, once the channel managers and the channels have been implemented, the last step is to implement a binding element that wraps all these together to be used by the WCF service model in an application. 

public class CustomChannelBindingElement : BindingElement

{

    public CustomChannelBindingElement()

    {

    }

    public override bool CanBuildChannelFactory<TChannel>(BindingContext context)

    {

        return (typeof(TChannel) == typeof(IDuplexSessionChannel) && context.CanBuildInnerChannelFactory<TChannel>());

    }

    public override bool CanBuildChannelListener<TChannel>(BindingContext context)

    {

        return (typeof(TChannel) == typeof(IDuplexSessionChannel) && context.CanBuildInnerChannelListener<TChannel>());

    }

    public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)

    {

        if (!this.CanBuildChannelFactory<TChannel>(context))

        {

            throw new InvalidOperationException("Unsupported channel type");

        }

        CustomChannelFactory factory =

            new CustomChannelFactory(context.BuildInnerChannelFactory<IDuplexSessionChannel>());

        return (IChannelFactory<TChannel>)factory;

    }

    public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context)

    {

        if (!this.CanBuildChannelListener<TChannel>(context))

        {

            throw new InvalidOperationException("Unsupported channel type");

        }

        CustomChannelListener listener = new CustomChannelListener(context.BuildInnerChannelListener<IDuplexSessionChannel>());

        return (IChannelListener<TChannel>)listener;

    }

    public override BindingElement Clone()

    {

        CustomChannelBindingElement clone = new CustomChannelBindingElement();

        return clone;

    }

    public override T GetProperty<T>(BindingContext context)

    {

        if (context == null)

        {

            throw new ArgumentNullException("context");

        }

        return context.GetInnerProperty<T>();

    }

}

Sample WCF BindingElement

As might notice, a binding element is nothing more than a factory for the channel managers that can be used at the moment of initializing a System.ServiceModel.Channels.CustomBinding instance, or in a custom implementation of the System.ServiceModel.Channels.Binding class.

About the Authors

Jesus Rodriguez: Jesus Rodriguez is the Chief Architect of Tellago, Inc. He is also a Microsoft BizTalk Server MVP, an Oracle ACE and one of a few Architects worldwide to be a member of the Microsoft Connected Systems Advisor team. As a member, Jesus has been selected to participate in a variety of Software Design Reviews with Microsoft's Product Teams including Windows Communication Foundation, Windows Workflow Foundation and BizTalk Server.

Jesus derived his extensive experience with business process integration and messaging through numerous implementations of disparate systems founded on the principles of SOA and BPM. Jesus is an active contributor to the .NET and J2EE communities and an internationally recognized speaker and author with contributions that include several articles for various publications including MSDN Magazine, Microsoft Architecture Journal, SOAWorld and Web Services Journal as well as speaking engagements at top industry conferences such as Microsoft TechEd, SOAWorld, Microsoft SOA and BPM Conference, Oracle Open World, software Architect Conference, Web Services Security Conference and the Microsoft MVP Summit. Additionally, Jesus has conducted a number of Web Casts on varying SOA technologies.

Jesus is a prolific blogger on all subjects related to integration and has a true passion for technology. You can gain valuable insight on leading edge technologies through his blog at https://weblogs.asp.net/gsusx.

Pablo Cibraro: Pablo is a senior architect in Tellago Inc, and an internationally reconigzed expert with over 10 years of experience in architecting and implementing large distributed systems with Microsoft Technnologies.

He has spent the past few years working directly with the Microsoft Patterns & Practices team on sample applications, patterns and guidance for building service-oriented applications with Web Services, Web Services Enhacements (WSE) and Windows Communication Foundation (WCF). The biggest contributions with this team were the Web Services Security patterns, and the implementation of a Secure Token Service quickstart for WSE 3.0 and WCF.

He now focuses on technologies that enable developers to build large scale systems such as WCF, WF, Dublin, OSLO and Windows Azure.

The community knows Pablo mainly for his weblog and active participation in conferences and forums.