On Channel9 last week Amiga forever! wanted to know how to pass multiple parameters to a WCF callback; so as I covered callbacks at DDD Scotland briefly this seemed like a perfect excuse to illustrate how to create a callback in WCF. So why do we want callbacks? One of remoting's more useful features was the ability for a server to trigger events on a client; WCF callbacks provide the same sort of facility.

To start lets create a new WCF project by choosing the WCF Service Library in VS2008;

newWCFProject

You can see I'm targeting .NET 3.0; just to prove it will work in Visual Studio 2005 as well.

Next we need to define our service contract. To illustrate callbacks I am going to have three command line applications; one to host the service, one to send messages to the service and one to listen for events from the service. We're going to have a very simple service that has a single method which accepts a string value. This string value will then be send as part of the callback, along with a timestamp of when it was sent.

So we add a new interface, IMessage, which will become our service contract. This simply consists of

namespace WcfCallbacks
{
    using System.ServiceModel;

    [ServiceContract]
    interface IMessage
    {
        [OperationContract]
        void AddMessage(string message);
    }
}

This would be enough for a simple server scenario; but because we want to support callbacks we need to do a couple more things. Firstly we define our callback contract; creating a new interface IMessageCallback

namespace WCFCallbacks
{
    using System;
    using System.ServiceModel;

    interface IMessageCallback
    {
        [OperationContract(IsOneWay = true)]
        void OnMessageAdded(string message, DateTime timestamp);
    }
}

You will notice that the callback interface is not marked as a ServiceContract. Now we just need to add a subscribe mechanism to our original service contract, so we modify IMessage to have suitable methods;

namespace WCFCallbacks
{
    using System.ServiceModel;

    [ServiceContract(CallbackContract = typeof(IMessageCallback))]
    public interface IMessage
    {
        [OperationContract]
        void AddMessage(string message);

        [OperationContract]
        bool Subscribe();

        [OperationContract]
        bool Unsubscribe();
    }
}

We change the service contract in two ways; we specify a callback contract as part of the ServiceContract attribute and we add a subscribe and unsubscribe mechanism as part of the operation contract. So now we just need to implement. So let's cover the subscribe mechanism; first we need somewhere to keep a record of subscribers, these can be stored in a generic list

private static readonly List<IMessageCallback> subscribers = new List<IMessageCallback>();
And to provide the subscribe and unsubscribe mechanisms we use the following implementation;
public bool Subscribe()
{
    try
    {
        IMessageCallback callback = OperationContext.Current.GetCallbackChannel<IMessageCallback>();
        if (!subscribers.Contains(callback))
            subscribers.Add(callback);
        return true;
    }
    catch
    {
        return false;
    }
}

public bool Unsubscribe()
{
    try
    {
        IMessageCallback callback = OperationContext.Current.GetCallbackChannel<IMessageCallback>();
        if (subscribers.Contains(callback))
            subscribers.Remove(callback);
        return true;
    }
    catch
    {
        return false;
    }
}

So now we have a list of subscribers which we can issue our callbacks to; we just need to do it as and when necessary; which in our simple example is when a message is received. So for the AddMessage implementation we have the following

public void AddMessage(string message)
{
    subscribers.ForEach(delegate(IMessageCallback callback)
    {
        if (((ICommunicationObject)callback).State == CommunicationState.Opened)
        {
            callback.OnMessageAdded(message, DateTime.Now);
        }
        else
        {
            subscribers.Remove(callback);
        }
    });
}

As you can see we simply iterate through the subscribers list, check if the connection is still open, send the message if it is, otherwise remove it from the subscribers list if it isn't. Obviously you would want more error handling and do all this in a background thread.

Finally we tweak the app.config to choose a protocol which supports callbacks; in this case wsDualHttpBinding because we want to use a standard protocol;

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.web>
    <compilation debug="true" />
  </system.web>
  <system.serviceModel>
    <services>
      <service name="WCFCallbacks.MessageService" behaviorConfiguration="WCFCallbacks.MessageBehavior">
        <host>
          <baseAddresses>
            <add baseAddress = "http://localhost:8731/Design_Time_Addresses/WCFCallbacks/Message/" />
          </baseAddresses>
        </host>
        <endpoint address ="" binding="wsDualHttpBinding" contract="WCFCallbacks.IMessage">
          <identity>
            <dns value="localhost"/>
          </identity>
        </endpoint>
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
      </service>
    </services>
    <behaviors>
      <serviceBehaviors>
        <behavior name="WCFCallbacks.MessageBehavior">
          <serviceMetadata httpGetEnabled="True"/>
          <serviceDebug includeExceptionDetailInFaults="False" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
  </system.serviceModel>
</configuration>

If at this stage you press start debugging you will see that the VS2008 WCF Test Client does not support callback contracts;

wcfTestClient

so we're going to have to implement our own hosts and test clients. First we need to create our service host; by creating a new command line application;

serviceHost

First we copy the app.config from our service library project into our server application and add a reference System.ServiceModel and our service library project. Then we edit our main method to contain the typical service initialisation (and shutdown) code;

ServiceHost myService = new ServiceHost(typeof(WCFCallbacks.MessageService));

myService.Open();
Console.WriteLine("Press any key to stop.");
Console.ReadKey();

if (myService.State != CommunicationState.Closed)
    myService.Close();

Now we just need to implement code to call the service and receive callbacks. We create another service host and this time we add a service reference;

addServiceReference

Visual Studio will discover services in a WCF service library provided there is an app.config inside the project. Once we have added the reference we can rename the app.config in the service library project to stop the test client appearing when we press run.

When we add a service reference to a callback service we need to implement the callback even if we do not want to subscribe or respond to the messages (for this reason it would be a good idea to offer two versions of your service; a contract with a callback and a contract without). To implement the callback we need to implement the callback interface we declared at the very start; so we need to create classes which wrap and control the service proxy Visual Studio generates for us.

For the demonstration we want to have a separate sending and receiving classes; our sending class looks like

[CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)]
class Sender : IMessageCallback, IDisposable
{
    private MessageClient messageClient;

    public void Go()
    {
        InstanceContext context = new InstanceContext(this);
        messageClient = new MessageClient(context, "WSDualHttpBinding_IMessage");

        for (int i = 0; i < 5; i++)
        {
            string message = string.Format("message #{0}", i);
            Console.WriteLine(">>> Sending "+message);
            messageClient.AddMessage(message);
        }

    }

    public void OnMessageAdded(string message, DateTime timestamp)
    {                
    }

    public void Dispose()
    {
        messageClient.Close();
    }
}

You can see we have implemented IMessageCallBack, which in turn requires to implement IDisposable. We can change the Callback behaviour using the CallbackBehaviourAttribute. In the Go method we create an InstanceContext for the current object and pass that into the MessageClient constructor. Then for test purposes we're just firing message down the wire. Notice that we don't call the subscribe method on the service, so the implementation of OnMessageAdded in this class will never get called.

For the receiver we obviously want to subscribe to the callbacks and react to them;

[CallbackBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)]
class Listener : IMessageCallback, IDisposable
{
    private MessageClient messageClient;

    public void Open()
    {
        InstanceContext context = new InstanceContext(this);
        messageClient = new MessageClient(context, "WSDualHttpBinding_IMessage");

        messageClient.Subscribe();
    }

    public void OnMessageAdded(string message, DateTime timestamp)
    {
        Console.WriteLine("<<< Recieved {0} with a timestamp of {1}", message, timestamp);
    }

    public void Dispose()
    {
        messageClient.Unsubscribe();
        messageClient.Close();
    }
}

Once we hit run and service initialisation finishes we will see 5 messages go off and 5 messages come back to our callback method which has multiple parameters as the original C9 post requested.

You can download the full project; as with all demonstration code it works here, there's little to no error checking and don't blame me if the code sleeps with your sister.

Technorati Tags: ,,