WCF Extensibility – Channels (server side)
This post is part of a series about WCF extensibility points. For a list of all previous posts and planned future ones, go to the index page.
Last week I introduced the channel model in WCF, with an emphasis on the channels at the client side. But that’s usually just half of the story - although the example I provided didn’t require a server-side channel counterpart, in many cases there are two channels, on the client and the server, acting together to provide some functionality to the WCF pipeline. This post will go into a little more detail on the server aspects of channels, especially how they’re created and inserted into the WCF stack.
Unlike on the client, where a channel factory is used for creating the channel instances, on the server side WCF uses channel listeners for that purpose. A factory basically sits there waiting for a call to be made (IChannelFactory<T>.CreateChannel) to return a channel, and a client channel sits idle until some channel (or the service model proxy) makes a call to send a message along (e.g., IRequestChannel.Request or IOutputChannel.Send). A channel listener, on the other hand, will actively try to “pump” channels from the listener sitting below it in the stack (the transport channel will sit listening for sockets or something similar from the network), in an “accept loop” similar to the one shown below.
- IChannelListener<IReplyChannel> listener = CreateListener();
- while (true)
- {
- try
- {
- IReplyChannel channel = listener.AcceptChannel(TimeSpan.FromMinutes(5));
- ProcessChannel(channel);
- }
- catch (TimeoutException)
- {
- }
- }
Basically, the listener will be asked to “accept” a channel for a certain time, and it should return it whenever one is available. If no channels are available after the timeout, the listener will throw. In many cases, the channel will be available right away – for example, the listener created by the HttpTransportBindingElement returns a “channel acceptor” class which waits for incoming connections in the HTTP port it’s listening to. The “server” channels will then follow a similar pumping pattern, this time waiting for incoming messages to arrive. Notice that this is similar to the way the HttpListener class is often used for for creating a generic HTTP server (although HttpListener has an event-based option which makes this a little easier; no such luck for WCF channels, unfortunately).
- while (true)
- {
- try
- {
- RequestContext requestContext = channel.ReceiveRequest(TimeSpan.FromSeconds(10));
- ProcessRequest(requestContext);
- }
- catch (TimeoutException)
- {
- }
- }
Notice that those snippets show the synchronous versions of the pump messages, but the WCF runtime by default uses their asynchronous versions (and for channels, the Try(Timeout, out result) pattern): IChannelListener<TChannel>.BeginAcceptChannel / IChannelListener<TChannel>.EndAcceptChannel for the listener, IReplyChannel.BeginTryReceiveRequest / IReplyChannel.EndTryReceiveRequest for reply channels (or IInputChannel.BeginTryReceive / IInputChannel.EndTryReceive for input channels). The calls to the channels can be changed to use the synchronous versions (TryReceiveRequest / TryReceive) by using the SynchronousReceiveBehavior endpoint behavior.
Request Context and messages
There are essentially two kinds of server channels as far as receiving messages: input channels and reply channels – duplex channels are just a pair of input / output channel, and session channels are the same as the non-session ones with an added identifier for the session for the communication. On input channels (used in datagram-like communication), the client simply sends a message and “forgets”, not waiting for a response. The input channel implements this behavior by simply returning the message directly to the caller on the Receive (or TryReceive, or Begin/EndReceive, or Begin/EndTryReceive). After the channel returns the message, it will go back to the pump loop waiting for a new one, and that message is “done”.
Reply channels, on the other hand, need to send a response to the message which it received, so instead of simply delivering the message to the caller, it will return a RequestContext object. The request context is the object which can be used to send the reply which is correlated to the incoming request. The request context has a property which holds a reference to the incoming request, and some methods to send a reply to the caller – it’s the responsibility of the code which called IReplyChannel.ReceiveRequest (or its variants) to call RequestContext.Reply (or BeginReply / EndReply) on the context to return a response to the caller. If this is not done in a timely manner, the channel will abort the request. The
How to add custom channels (server side)
Just like the client, it all starts at the binding. So one needs to write a binding element, and create a binding containing this element. The binding element class would override CanBuildChannelListener<TChannel> (to determine whether the channel shape is supported) and BuildChannelListener<TChannel> to actually return a channel listener which is capable of accepting the requested channel type. The channel listener implementation needs to override the AcceptChannel methods (or OnAcceptChannel and its asynchronous / try variants), if the class inherits from ChannelListenerBase<TChannel>), besides a lot of boilerplate methods (Open, BeginOpen, EndOpen, Close, etc.) to finally return the channel which will be used in the pipeline. To illustrate the verboseness of all of this, this is I think the simplest code to write a server channel that I can think of (it just passes the message along, and supports only IReplyChannel, although supporting IInputChannel wouldn’t add a lot more code).
- public class WrappingBindingElement : BindingElement
- {
- public WrappingBindingElement()
- {
- }
- public override BindingElement Clone()
- {
- return new WrappingBindingElement();
- }
- public override T GetProperty<T>(BindingContext context)
- {
- return context.GetInnerProperty<T>();
- }
- public override bool CanBuildChannelListener<TChannel>(BindingContext context)
- {
- return typeof(TChannel) == typeof(IReplyChannel)
- && context.CanBuildInnerChannelListener<TChannel>();
- }
- public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context)
- {
- if (context == null)
- {
- throw new ArgumentNullException("context");
- }
- if (typeof(TChannel) != typeof(IReplyChannel))
- {
- throw new InvalidOperationException("Only IReplyChannel is supported");
- }
- var result = new WrappingListener(context.BuildInnerChannelListener<IReplyChannel>());
- return (IChannelListener<TChannel>)result;
- }
- class WrappingListener : ChannelListenerBase<IReplyChannel>
- {
- IChannelListener<IReplyChannel> innerListener;
- public WrappingListener(IChannelListener<IReplyChannel> innerListener)
- {
- this.innerListener = innerListener;
- }
- public override T GetProperty<T>()
- {
- T baseProperty = base.GetProperty<T>();
- if (baseProperty != null)
- {
- return baseProperty;
- }
- return this.innerListener.GetProperty<T>();
- }
- protected override IReplyChannel OnAcceptChannel(TimeSpan timeout)
- {
- return this.WrapChannel(this.innerListener.AcceptChannel(timeout));
- }
- protected override IAsyncResult OnBeginAcceptChannel(TimeSpan timeout, AsyncCallback callback, object state)
- {
- return this.innerListener.BeginAcceptChannel(timeout, callback, state);
- }
- protected override IReplyChannel OnEndAcceptChannel(IAsyncResult result)
- {
- return this.WrapChannel(this.innerListener.EndAcceptChannel(result));
- }
- protected override IAsyncResult OnBeginWaitForChannel(TimeSpan timeout, AsyncCallback callback, object state)
- {
- return this.innerListener.BeginWaitForChannel(timeout, callback, state);
- }
- protected override bool OnEndWaitForChannel(IAsyncResult result)
- {
- return this.innerListener.EndWaitForChannel(result);
- }
- protected override bool OnWaitForChannel(TimeSpan timeout)
- {
- return this.innerListener.WaitForChannel(timeout);
- }
- private IReplyChannel WrapChannel(IReplyChannel innerChannel)
- {
- if (innerChannel == null)
- {
- return null;
- }
- return new WrappingChannel(this, innerChannel);
- }
- #region Other properties / methods which are delegated to the inner listener
- public override Uri Uri
- {
- get { return this.innerListener.Uri; }
- }
- protected override void OnAbort()
- {
- this.innerListener.Abort();
- }
- protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state)
- {
- return this.innerListener.BeginClose(timeout, callback, state);
- }
- protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state)
- {
- return this.innerListener.BeginOpen(timeout, callback, state);
- }
- protected override void OnClose(TimeSpan timeout)
- {
- this.innerListener.Close(timeout);
- }
- protected override void OnEndClose(IAsyncResult result)
- {
- this.innerListener.EndClose(result);
- }
- protected override void OnEndOpen(IAsyncResult result)
- {
- this.innerListener.EndOpen(result);
- }
- protected override void OnOpen(TimeSpan timeout)
- {
- this.innerListener.Open(timeout);
- }
- #endregion
- }
- class WrappingChannel : ChannelBase, IReplyChannel
- {
- IReplyChannel innerChannel;
- public WrappingChannel(ChannelManagerBase channelManager, IReplyChannel innerChannel)
- : base(channelManager)
- {
- if (innerChannel == null)
- {
- throw new ArgumentNullException("innerChannel");
- }
- this.innerChannel = innerChannel;
- }
- public IAsyncResult BeginReceiveRequest(TimeSpan timeout, AsyncCallback callback, object state)
- {
- return this.innerChannel.BeginReceiveRequest(timeout, callback, state);
- }
- public IAsyncResult BeginReceiveRequest(AsyncCallback callback, object state)
- {
- return this.BeginReceiveRequest(this.DefaultReceiveTimeout, callback, state);
- }
- public IAsyncResult BeginTryReceiveRequest(TimeSpan timeout, AsyncCallback callback, object state)
- {
- return this.innerChannel.BeginTryReceiveRequest(timeout, callback, state);
- }
- public IAsyncResult BeginWaitForRequest(TimeSpan timeout, AsyncCallback callback, object state)
- {
- return this.innerChannel.BeginWaitForRequest(timeout, callback, state);
- }
- public RequestContext EndReceiveRequest(IAsyncResult result)
- {
- return this.WrapContext(this.innerChannel.EndReceiveRequest(result));
- }
- public bool EndTryReceiveRequest(IAsyncResult result, out RequestContext context)
- {
- bool endTryResult = this.innerChannel.EndTryReceiveRequest(result, out context);
- if (endTryResult)
- {
- context = this.WrapContext(context);
- }
- return endTryResult;
- }
- public bool EndWaitForRequest(IAsyncResult result)
- {
- return this.innerChannel.EndWaitForRequest(result);
- }
- public RequestContext ReceiveRequest(TimeSpan timeout)
- {
- RequestContext context = this.innerChannel.ReceiveRequest(timeout);
- return this.WrapContext(context);
- }
- public RequestContext ReceiveRequest()
- {
- return this.ReceiveRequest(this.DefaultReceiveTimeout);
- }
- public bool TryReceiveRequest(TimeSpan timeout, out RequestContext context)
- {
- bool result = this.innerChannel.TryReceiveRequest(timeout, out context);
- if (result)
- {
- context = this.WrapContext(context);
- }
- return result;
- }
- public bool WaitForRequest(TimeSpan timeout)
- {
- return this.innerChannel.WaitForRequest(timeout);
- }
- private RequestContext WrapContext(RequestContext innerContext)
- {
- if (innerContext == null)
- {
- return null;
- }
- else
- {
- return new WrappingRequestContext(this, innerContext);
- }
- }
- #region Other properties / methods which are delegated to the inner channel
- public override T GetProperty<T>()
- {
- return this.innerChannel.GetProperty<T>();
- }
- protected override void OnAbort()
- {
- this.innerChannel.Abort();
- }
- protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state)
- {
- return this.innerChannel.BeginClose(timeout, callback, state);
- }
- protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state)
- {
- return this.innerChannel.BeginOpen(timeout, callback, state);
- }
- protected override void OnClose(TimeSpan timeout)
- {
- this.innerChannel.Close(timeout);
- }
- protected override void OnEndClose(IAsyncResult result)
- {
- this.innerChannel.EndClose(result);
- }
- protected override void OnEndOpen(IAsyncResult result)
- {
- this.innerChannel.EndOpen(result);
- }
- protected override void OnOpen(TimeSpan timeout)
- {
- this.innerChannel.Open(timeout);
- }
- public EndpointAddress LocalAddress
- {
- get { return this.innerChannel.LocalAddress; }
- }
- #endregion
- class WrappingRequestContext : RequestContext
- {
- RequestContext innerContext;
- WrappingChannel channel;
- public WrappingRequestContext(WrappingChannel channel, RequestContext innerContext)
- {
- this.innerContext = innerContext;
- this.channel = channel;
- }
- public override void Abort()
- {
- this.innerContext.Abort();
- }
- public override IAsyncResult BeginReply(Message message, TimeSpan timeout, AsyncCallback callback, object state)
- {
- return this.innerContext.BeginReply(message, timeout, callback, state);
- }
- public override IAsyncResult BeginReply(Message message, AsyncCallback callback, object state)
- {
- return this.BeginReply(message, this.channel.DefaultSendTimeout, callback, state);
- }
- public override void Close(TimeSpan timeout)
- {
- this.innerContext.Close(timeout);
- }
- public override void Close()
- {
- this.innerContext.Close();
- }
- public override void EndReply(IAsyncResult result)
- {
- this.innerContext.EndReply(result);
- }
- public override void Reply(Message message, TimeSpan timeout)
- {
- this.innerContext.Reply(message, timeout);
- }
- public override void Reply(Message message)
- {
- this.Reply(message, this.channel.DefaultSendTimeout);
- }
- public override Message RequestMessage
- {
- get
- {
- return this.innerContext.RequestMessage;
- }
- }
- }
- }
- }
Again, that’s a lot of code. But that’s a good starting point if you want to do something simple with the message.
Real world scenarios
This has been a busy time at work, so I’ll skip the scenario code for this week’s post. There are some very interesting samples using channels (both server and client ones) at the WCF Channel Extensibility samples page. Also, there are some nice posts from Nicholas Allen about channels in WCF which are a great source of information for low-level WCF details.
Comments
- Anonymous
February 24, 2012
Please share the code for this. - Anonymous
February 27, 2012
Hi Hema, you can simply copy/paste the code in the example (this post doesn't have a full sample like other posts in this series). Copying on IE sometimes doesn't work, in Chrome works all the time for me. And if you want to see a full code sample for a server-side reply channel, you can look at the post at blogs.msdn.com/.../wcf-extensibility-transport-channels-reply-channel.aspx. - Anonymous
February 27, 2012
Hi Carlos, Thanks for the reply. I am new to WCF. I am trying to read the poison message in WCF service layer itself. So that I can handle it appropriately. Can you help me on this? - Anonymous
February 27, 2012
I haven't worked with queues and WCF, but the document at msdn.microsoft.com/.../ms789028.aspx should give you a good starting point on handling poison messages in WCF. - Anonymous
June 22, 2012
Hi Carlos;Been looking at your code and i wonder where the CreateListener() method gets defined? i don't see it listed anywhere. I have to say WCF is anything but intuitive. It's confusing the heck out of me. - Anonymous
June 22, 2012
Hi Shawn,Thanks for the comment. I partially agree with you - the WCF channel model is really, really, really complicated (especially with all the factory model / asynchronous methods - as I mentioned in another post, anytime you can avoid going down to the WCF channels, you definitely should. However, the "simple" WCF scenarios (where you define service and operation contracts and that's it) I found fairly simple.Regarding your question about the listener, in the post about transport channels (blogs.msdn.com/.../wcf-extensibility-transport-channels-reply-channel.aspx) there is a complete example for a reply channel. However, for completeness sake, I'll try to update this post with an example of a server-side protocol channel later today.Thanks,Carlos.