PollingDuplexBinding Sample Implementation

Part 2 of post, the client implementation.

I’ve been playing around with PollingDuplexBinding a bit lately. The application that I’m building communicates with a number of external web services from partners of the organization I work for. The partner companies require a good deal of configuration in order to enable and authenticate the services and very often these configurations get changed and break the web service calls. The result is a lot of unexpected errors and people who are upset the application “isn’t working”. In order to improve my ability to debug issues I created a Silverlight client that can display all of the log messages generated by the application so rather than logging onto the server and trying to pull apart the Console log messages I created the client which I can use to sort and search different types of log messages. I chose PollingDuplexBinding to “push” messages from the server application to any clients who are listening. A database is probably a better place for these log messages but I want to see what’s happening in real time without logging onto the server, I don’t want to run Sql queries and this fits better with my long term goal to create a Windows Phone 7 app in which I can monitor the state of the server application as well.

I have to admit I received a lot of help from a blog post I found at the time I originally implemented this solution. I changed the implementation a bit to fit my needs better but I wish I could cite the original source who laid the foundation of what can be found below. Unfortunately I can’t find the original blog post. Thanks to the mysterious blogger out there.

This implementation makes heavy use of dependency injection (via Unity though that’s not included here). Also note the lack of an ILogger being injected into any of these services. Logging the log publishing service with the same logger which is creating the messages being published here would create an infinite loop. Another potential refactor would be to create a special logger that would send the messages else where. I’m using the EnterpriseLibrary logging block and this would probably be pretty easy to create the logger but don’t set a listener to this log publishing service. But that’s for another post. Also note this is implemented on .Net 3.5 and requires the addition of the System.ServiceModel.PollingDuplex assembly. Lets jump into the code.

[ServiceContract(CallbackContract=typeof(ILogServiceCallback))]
public interface ILogService
{
    /// <summary>
    /// Publishes the message.
    /// </summary>
    /// <param name="message">The message.</param>
    [OperationContract(IsOneWay=true)]
    void PublishMessage(LogMessage message);

    /// <summary>
    /// Subscribes this instance.
    /// </summary>
    [OperationContract] 
    void Subscribe();

    /// <summary>
    /// Keeps the alive.
    /// </summary>
    [OperationContract]
    bool KeepAlive();
}

Quick thing to notice on this ServiceContact. The callback type is specified, the implementation of which is used to push messages to the client. For reference, the implementation is created on the client side. The interface can be found lower on this page.

[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
public class LogDuplexService : ILogService
{
    private readonly IOperationContextManager contextManager;
    private LogMessage currentMessageText;

    /// <summary>
    /// Initializes a new instance of the <see cref="LogDuplexService"/> class.
    /// </summary>
    public LogDuplexService(IOperationContextManager contextManager)
    {
        this.contextManager = contextManager;
    }

    /// <summary>
    /// Service to call to keep the channel open.
    /// </summary>
    /// <returns></returns>
    public bool KeepAlive()
    {
        return true;
    }

    /// <summary>
    /// Called by client to indicate it wishes to subscribe to events from this service.
    /// </summary>
    public void Subscribe()
    {
        contextManager.AddCurrentContext();
    }

    /// <summary>
    /// Publishes the message.
    /// </summary>
    /// <param name="message">The message.</param>
    public void PublishMessage(LogMessage message)
    {
        currentMessageText = message;
        contextManager.ForEachContextCallbackChannel<ILogServiceCallback>(PublishMessage);
    }

    private bool PublishMessage(ILogServiceCallback callback)
    {
        try
        {
            callback.PushMessage(currentMessageText);
        }
        catch(CommunicationObjectAbortedException)
        {
            return false;
        }
        catch(CommunicationException)
        {
            return false;
        }

        return true;
    }

The above interface and implementation describes the main service that is exposed. It has functionality to maintain the connection (KeepAlive), register a client as a listener of log messages (Subscribe) and to publish messages. The publish probably shouldn’t be exposed as it is only used internally but it’s a convenient place for it. I think if I get some free time to refactor, the publish method won’t be exposed as a service though it does provide some interesting functionality if clients could also publish messages.

The IOperationContextManager is responsible for maintaining connections. We have to know who is subscribed and how to notify them when a log message is available and this is the purpose of that class. Implementation below. I didn’t include the interface but all of the public methods are exposed on the interface.

public class OperationContextManager : IOperationContextManager
{
    private readonly IOperationContextHelper contextHelper;
    private readonly LinkedList<OperationContext> nodes;

    /// <summary>
    /// Initializes a new instance of the <see cref="OperationContextManager"/> class.
    /// </summary>
    public OperationContextManager(IOperationContextHelper contextHelper)
    {
        this.contextHelper = contextHelper;
        nodes = new LinkedList<OperationContext>();
    }

    /// <summary>
    /// Adds the current context.
    /// </summary>
    public void AddCurrentContext()
    {
        if (nodes.Count > 0)
        {
            nodes.AddLast(OperationContext.Current);
        }
        else
        {
            nodes.AddFirst(OperationContext.Current);
        }
    }

    /// <summary>
    /// Iterates through each context and executes the action delegate.
    /// </summary>
    /// <param name="action">The action.</param>
    public void ForEachContextCallbackChannel<T>(Func<T, bool> action) where T : class
    {
        if (nodes == null || nodes.Count <= 0)
        {
            return;
        }
         ExecuteAction(nodes.First, action);
    }

    private void ExecuteAction<T>(LinkedListNode<OperationContext> node, Func<T, bool> action) where T : class
    {
        if (node == null || node.Value == null)
        {
            return;  
        }

        T callbackProxy = contextHelper.GetCallbackChannel<T>(node.Value);
        bool removeNode = callbackProxy == null ? true : !action(callbackProxy);

        // Recursively iterate through link list executing the proveded delegate passing in the callback channel of type T from the current context.
        ExecuteAction(node.Next, action);

        if (removeNode)
        {
            nodes.Remove(node);
        }
    }
}

The ContextManager uses a linked list to keep track of all the connections. I used a linked list because connections are constantly added and removed and this seemed to make the most sense. The AddCurrentContext method gets the current OperationContext which holds the key information about the client who is calling the service. We’ll hold on to that context and use it later to get the callback channel used to notify the client.

The ForEachContextCallbackChannel will execute the specified delegate on each available callback. It recursively iterates through all of the linked list nodes containing the OperationContexts of the clients who are listening and executes the callback to notify them of the available message. Also included is a check on whether the client is actually still listening. If an exception is thrown when executing the callback then the OperationContext is removed from the linked list as the client is likely no longer listening and will need to re-subscribe in order to get the log messages again.

public class OperationContextHelper : IOperationContextHelper
{
    /// <summary>
    /// Gets the callback channel of the specified type for the provided OperationContext.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="context">The context.</param>
    /// <returns></returns>
    public T GetCallbackChannel<T>(OperationContext context) where T : class
    {
        if (context == null)
        {
            return null;
        }

        return context.GetCallbackChannel<T>();
    }
}

This may seem like an unnecessary class but it was abstracted to improve the overall testability and provide a means to mock out the services. The sole method of this class will take in an OperationContext and get the callback channel, specified generically, on that context.

/// <summary>
/// Callback for duplex service to push log messages to client.
/// </summary>
public interface ILogServiceCallback
{
    /// <summary>
    /// Pushes the message to the client via the duplex service.
    /// </summary>
    /// <param name="message">The message.</param>
    [OperationContract(IsOneWay=true)] 
    void PushMessage(LogMessage message);
}

Above is the call back contract. Notice the IsOneWay attribute on the sole operation of the contract. The implementation of this can be found on the client and will be provided in another post.

Part 2 of post, the client implementation.

Advertisements

One thought on “PollingDuplexBinding Sample Implementation

  1. Pingback: Shelby Township MI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s