Forwarding Context-ful Messages
Workflow Services (introduced in .NET 3.5) are based on a simple convention for passing the workflow instance identifier from the client to the workflow and from the workflow to any services it invokes. This convention revolves around the use of context-ful bindings (BasicHttpContextBinding, WSHttpContextBinding, NetTcpContextBinding and others) and a simple dictionary which contains a key called "instanceId" and a value that contains the workflow instance identifier.
This information is passed out-of-band to facilitate cleaner interfaces - it's passed in a SOAP header called ContextMessageHeader (which is an internal WCF class), and can be accessed from the channel's context manager or a message property called ContextMessageProperty. (For completeness it's also worth stating that the context information can be passed in an HTTP cookie - which is the approach taken by the BasicHttpContextBinding - but we will focus on the SOAP header approach.)
If a client wants to communicate with a specific workflow instance, then the following plumbing code will ensure that the right instance will service the request:
//Extension for applying instance id to context
public static void ApplyInstanceId(this IContextChannel proxy, Guid id)
{
IContextManager ctxManager = proxy.GetProperty<IContextManager>();
IDictionary<string,string> ctx = ctxManager.GetContext();
ctx.Add("instanceId", id.ToString());
ctxManager.SetContext(ctx);
}
This extension method can be used on the proxy to the workflow endpoint:
IWorkflow workflowProxy = ChannelFactory<IWorkflow>.CreateChannel(
Common.Binding, new EndpointAddress(Common.WorkflowAddress));
((IContextChannel)workflowProxy).ApplyInstanceId(workflow.InstanceId);
This causes the message to include a context message header with the workflow instance identifier embedded in it. The message will appear similar to the following (note the Context element in bold italic):
<s:Envelope
xmlns:s="http://www.w3.org/2003/05/soap-envelope"
xmlns:a="http://www.w3.org/2005/08/addressing">
<s:Header>
<a:Action s:mustUnderstand="1">
http://tempuri.org/IWorkflow/Echo
</a:Action>
<a:MessageID>
urn:uuid:610c88d9-3365-4aa5-ac8c-3e7949621c80
</a:MessageID>
<a:ReplyTo>
<a:Address>
http://www.w3.org/2005/08/addressing/anonymous
</a:Address>
</a:ReplyTo>
<Context xmlns="http://schemas.microsoft.com/ws/2006/05/context">
<Property name="instanceId">
dec52f54-c51b-4025-888d-58f29507f572
</Property>
</Context>
<a:To s:mustUnderstand="1">
http://localhost:9092/Intermediary/RR
</a:To>
</s:Header>
<s:Body>
<Echo xmlns="http://tempuri.org/">
<message>Hello</message>
</Echo>
</s:Body>
</s:Envelope>
On the other hand, when receiving a callback from a workflow, a service has to use the following plumbing code to determine which specific workflow instance is responsible for the call:
//Extension for extracting instance id from message properties
public static string GetInstanceId(this MessageProperties properties)
{
ContextMessageProperty property;
if (!ContextMessageProperty.TryGet(properties, out property))
{
throw new InvalidOperationException("No ContextMessageProperty");
}
return property.Context["instanceId"];
}
This extension method can be used on the incoming message properties:
string workflowInstanceId =
OperationContext.Current.IncomingMessageProperties.GetInstanceId();
This is all fairly straightforward and well-covered by existing technology samples. The challenge is to forward context-ful messages through an intermediary (such as the intermediary covered in previous posts). The naive approach is not going to work for a two primary reasons.
To begin with, the IContextManager approach is incompatible with the ContextMessageProperty. This means that if the outgoing channel created within the router has context management enabled and the outgoing message properties contain the context message property, an exception will be thrown. This means that we have to disable context management on the outgoing channel or remove the context message property before forwarding the message. Either option is feasible, so here's how to disable context management (the alternative is left as an exercise for the reader):
//Extension for disabling context management on the channel
public static void DisableContextManagement(this IContextChannel channel)
{
IContextManager ctxManager = channel.GetProperty<IContextManager>();
ctxManager.Enabled = false;
}
This can be used on any outgoing channel. For example, in the sample project featured in this post, the router code for request-reply messages now becomes:
public Message ActionRR(Message request)
{
//Forward to workflow
IGenericRR proxy = ChannelFactory<IGenericRR>.CreateChannel(
Common.Binding, new EndpointAddress(Common.WorkflowAddress));
((IContextChannel)proxy).DisableContextManagement();
return proxy.ActionRR(request);
}
Unfortunately, this still isn't going to work. In fact, if you try this code out, you'll find that there's no exception thrown, but the message doesn't reach the workflow. It's silently swallowed and there's absolutely no indication that anything went wrong in the process.
(Fast-forward countless hours of frustrating debugging.) It appears that the context header present in the incoming message prevents the message from being successfully forwarded. The context header is added again when the message is dispatched by the outgoing channel, and the header's presence somehow causes the message to be lost. Therefore, we must remove the context header explicitly:
//Extension for removing the context header
public static void RemoveContext(this MessageHeaders headers)
{
headers.RemoveAll(ContextHeaderName, ContextHeaderNamespace);
}
const string ContextHeaderName = "Context";
const string ContextHeaderNamespace = "http://schemas.microsoft.com/ws/2006/05/context";
This code can now be incorporated into the message-forwarding logic outlined above:
public Message ActionRR(Message request)
{
//Forward to workflow
request.Headers.RemoveContext();
IGenericRR proxy = ChannelFactory<IGenericRR>.CreateChannel(
Common.Binding, new EndpointAddress(Common.WorkflowAddress));
((IContextChannel)proxy).DisableContextManagement();
return proxy.ActionRR(request);
}
This must be done regardless of whether the intermediary is processing a message directed at a workflow or a message originating from a workflow. Without removing the context header, the message will fail to be processed.
To connect all these seemingly disconnected pieces of code, you can download a sample project demonstrating a scenario where a client communicates to a workflow through an intermediary and the workflow proceeds to send a one-way notification to the client through the same intermediary.
