Duplex communication with .Net 3.5 Workflow Services

Workflow Services was introduced as part of .Net 3.5 and enables us to use WF workflows as implementation of our WCF services. Workflows are long running by nature so the usual WCF duplex messaging constructs (where correlation between client & server is done based on actual physical connection/socket) are not suitable for most cases. Primarily because it is simply not possible or practical to keep a socket open until workflow finishes its processing (which could potentially take days to produce any meaningful response).

So channel based WCF duplex messaging is not good for long running scenarios. So how can we achieve duplex messaging in a long running world?

The general guidance is to use an explicit callback channel (sometime known as “back channel”) for notifications. You can argue that addressability of the client (who may be behind NAT/Firewall) is a real blocker for explicit back channel & I totally agree. However with Relay technologies like .Net Service Bus, reaching to the client endpoints is not an issue anymore hence explicit back channel is much more practical/feasible option now.

Anyways in this post I will show you, how we can achieve durable duplex messaging using Workflow Services. Here is a basic scenario:

 

Front end service (orange box) is a long running workflow service – actual service implementation is a WF workflow so it can go idle, persist, sit in the database & brought back into memory if and when needed (all the usual stuff).

Now with above setup if we do usual request-reply messaging – Send activity will simply wait for the reply, workflow will stay in memory & ultimately underlying WCF call will timeout and bad things will happen (workflow might terminate etc).

So first change we have to do is to move the “wait for reply logic” out of the workflow into the host. With this change workflow can go idle, persist and ultimately gets unloaded while host is still listening for the response. On the response, host can bring workflow back into memory and resume the execution.  Important takeaway is that we don’t have to do all this ourselves. Workflow Services & associated “context-exchange protocol” can do most of this for us.

Following is the implementation of front end service...

 

Dowork is a one-way call. As part of Dowork I’m also passing instanceId of the current workflow along with a callback address (using the standard WS-Addressing ReplyTo header - for protocol purists: this might not be the correct use of ReplyTo header but I’m using it here to keep things simple). So essentially I’m saying “please call me back on this number, when you are done.” J

Dowork call returns immediately by triggering long running work in the Backend service. Workflow will become idle & can potentially persist.

To override the value of default replyTo header, I have created a simple message inspector.

class ReplyToInspector : IClientMessageInspector

{

    EndpointAddress replyTo;

    public ReplyToInspector(EndpointAddress replyTo)

    {

        this.replyTo = replyTo;

 

    }

    #region IClientMessageInspector Members

    public void AfterReceiveReply(ref System.ServiceModel.Channels.Message reply, object correlationState)

    {

       

    }

    public object BeforeSendRequest(ref System.ServiceModel.Channels.Message request, IClientChannel channel)

    {

        request.Headers.ReplyTo = replyTo;

        return null;

    }

    #endregion

}

And this can be configured in config file as:

        <endpointBehaviors>

          <behavior name="reply">

            <replyTo address="https://localhost:43211/Callback/"/>

          </behavior>

        </endpointBehaviors>

Backend service is standard WCF service – doing callbacks using the usual ChannelFactory stuff. The important bit (highlighted) is the explicit context management.

public void Dowork(Guid instanceId)

{

    Console.WriteLine("Doing long running work...");

    for (int i = 0; i < 10; i++)

    {

        Console.Write("...");

        Thread.Sleep(1000);

    }

    Console.WriteLine();

    try

    {

        Console.WriteLine("Sending response...");

        SendResults(instanceId, OperationContext.Current.IncomingMessageHeaders.ReplyTo);

    }

    catch (Exception exp)

    {

        Console.WriteLine("Failed: " + exp.ToString());

    }

}

private void SendResults(Guid instanceId, EndpointAddress remoteAddress)

{

    var binding = new BasicHttpContextBinding();

    var cf = new ChannelFactory<IBackendCallback>(binding, remoteAddress);

    var proxy = cf.CreateChannel();

    var cc = proxy as IContextChannel;

    using (new OperationContextScope(cc))

    {

        var cmp = new ContextMessageProperty();

        var cm = cc.GetProperty<IContextManager>();

        cm.Enabled = false;

        cmp.Context["instanceId"] = instanceId.ToString();

        OperationContext.Current.OutgoingMessageProperties.Add(ContextMessageProperty.Name, cmp);

        proxy.LongRunningOperationCompleted(new OperationOutput { Id = 22, Status = "Done.." });

    }

}

In Dowork, I’m passing instanceId as method’s signature however you can easily pass this data as part of context header (remmeber context header is property bag -- IDictionary<string,string>)

In the SendResults – I’m creating approparite context header – so that WorflowServiceHost can connect this callback to the correct instance & correct activity inside that instance. WorkflowServiceHost will also load the workflow if it got persisted.

I’m attaching complete solution with this. In next post, I will talk about durable messaging enhancements added in .Net 4.0 Beta 1.

Stay tuned...

Originally posted by Zulfiqar on the 5th of June 2009 here.