DCSIMG
Forwarding Context-ful Messages - All Your Base Are Belong To Us

All Your Base Are Belong To Us

Mostly .NET internals and other kinds of gory details

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.

image

Comments

Kay said:

Thank you for a very useful sample!

Would the same also work when the service was hosted in IIS?

I'd like to offer a service endpoint for all non WCF clients, which only support context less bindings/protocols.

The endpoint would have the context information as part of the interface.

# July 28, 2008 5:07 AM

Sasha Goldshtein said:

I haven't tried hosting it in IIS, but I don't see why it wouldn't work, off the top of my head.  What do you mean by "have the context information as part of the interface"?

# July 30, 2008 2:12 PM

Kay said:

Thanks for your answer.

As I described, I'd like to have a service endpoint for all non WCF clients and all web service clients which don't support the WF context within the SOAP header or HTTP cookie.

The interface for this endpoint would contain the context (such as the instance ID) as a parameter, and then forward it to the context aware endpoint.

Is this feasible or the way to go?

May I email you about that topic?

Thanks and best regards

-Kay

# August 4, 2008 9:48 AM

Sasha Goldshtein said:

Yes, I think it's feasible.  And yes, feel free to email me about this (through the contact form).

# August 9, 2008 6:04 AM

Curt Peterson said:

You need to include more than the instanceId to support the entire context.  Specifically, you need to include the conversationId (this is optional and will be there if you set the ContextToken on an activity).

In general you should always operate on all elements in the IDictionary of the context and use them all, as eventually you might add user entries as well.

# February 27, 2009 10:09 AM
Leave a Comment

(required) 

(required) 

(optional)

(required) 


Enter the numbers above: