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 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:

Frond 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="http://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...