WF4&WCF and message correlation

Note: This blog post is written using the .NET framework 4.0 Beta 2

In the previous blog posts, here, here and here, I demonstrated how to  use WCF from WF4. This same some more about sending multiple messages to the same workflow, AKA Workflow Correlation.

 

One of the ugly parts of Windows Workflow Foundation 3 was the message correlation part when you used WCF to send multiple messages to the same workflow.

When using WF3 you where forced to use one of the context bindings like BasicHttpContextBinding or WSHttpContextBinding. Not only that but you also had to retrieve and set the correlation context using the IContextManager and set it to some arcane guid. This guid was the workflow instance ID but having to use that on the client means the client had to be very aware of the server technology used, something that goes against the principals of WCF. Using something in the message itself as to route the message to the correct workflow was just not possible with the build-in activities.

 

Fortunately the WF4 story is much better. No longer do we need to use the “context” bindings but the standard WCF bindings will do just fine. And the client doesn’t have to work with the workflow instance id but can pass some more logical data, like an invoice number, that will be mapped to the correct workflow. Much better!

 

To demonstrate this we first need to expand the workflow so it can receive multiple messages for the same workflow. The input for both Receive activities will still be the same person class from the previous example and we will use the persons ID to route the messages.

 

The new workflow definition looks like this:

private static WorkflowElement CreateWorkflow()


{


    var result = new Sequence();


    var input = new Variable<Person>();


    result.Variables.Add(input);


    XNamespace ns = "http://tempuri.org";


 


    var handle1 = new Variable<CorrelationHandle>();


    result.Variables.Add(handle1);


 


    var handle2 = new Variable<CorrelationHandle>();


    result.Variables.Add(handle2);


 


    var receive1 = new Receive()


    {


        OperationName = "Operation1",


        ServiceContractName = ns + "MyService",


        Value = new OutArgument<Person>(input),


        AdditionalCorrelations = {                   


           {"ChannelBasedCorrelation", new InArgument<CorrelationHandle>(handle1)}


        },


        CorrelationQuery = CreateCorrelationQuery(),


        CanCreateInstance = true


    };


    result.Activities.Add(receive1);


 


    var write1 = new WriteLine()


    {


        Text = new InArgument<string>(env => string.Format("\tThe workflow was called with '{0}'.", input.Get(env)))


    };


    result.Activities.Add(write1);


 


    var reply1 = new SendReply()


    {


        Request = receive1,


        Value = new InArgument<string>("The result")


    };


    result.Activities.Add(reply1);


 


 


    var receive2 = new Receive()


    {


        OperationName = "Operation2",


        ServiceContractName = ns + "MyService",


        Value = new OutArgument<Person>(input),


        AdditionalCorrelations = {


           {"ChannelBasedCorrelation", new InArgument<CorrelationHandle>(handle2)}


        },


        CorrelationQuery = CreateCorrelationQuery()


    };


    result.Activities.Add(receive2);


 


    var write2 = new WriteLine()


    {


        Text = new InArgument<string>(env => string.Format("\tThe workflow was called again with '{0}'.", input.Get(env)))


    };


    result.Activities.Add(write2);


 


    var reply2 = new SendReply()


    {


        Request = receive2,


        Value = new InArgument<string>("The second result")


    };


    result.Activities.Add(reply2);


 


    return result;


}

 

This is very similar to the previous workflow except we have two Receive activities and only the first is configured to allow the message to create a new instance.

 

The trick in getting the second message routed to the same workflow is by setting the CorrelationQuery on the two Receive activities. This is done in the CreateCorrelationQuery() function as follows:

private static CorrelationQuery CreateCorrelationQuery()


{


    var result = new CorrelationQuery();


    var xpath = new XPathMessageQuery()


    {


        Namespaces = new XmlNamespaceManager(new NameTable()),


        Expression = "//sample:Person/sample:Id"


    };


 


    xpath.Namespaces.AddNamespace("sample", "urn:WF4Sample:person");


 


    var messageQuerySet = new MessageQuerySet()


    {


        Name = "RequestCorrelation"


    };


 


    messageQuerySet.Add("PersonId", xpath);


    result.Select = messageQuerySet;


 


    return result;


}

The important part here is that the MessageQuerySet key value and the related Expression XPath must produce the same result for both request. Something that is easy here as I am sending in the same person object twice.

 

So the regular client has hardly any changes. All that is needed is the addition of the second operation to the ServiceContract like this:

[ServiceContract]


interface MyService


{


    [OperationContract]


    Operation1Response Operation1(Person request);


    [OperationContract]


    Operation1Response Operation2(Person request);


}

And calling the second operation like this.

static void Main(string[] args)


{


    var binding = new BasicHttpBinding();


    var endpoint = new EndpointAddress("http://localhost:8090/Sequence1/Operation1");


    var factory = new ChannelFactory<MyService>(binding, endpoint);


    var proxy = factory.CreateChannel();


 


    var request = new Person()


    {


        Id = 23,


        FirstName = "Joe",


        LastName = "Regular",


        BirthDate = new DateTime(1975, 7, 1)


    };


    var result = proxy.Operation1(request);


    Console.WriteLine(result.Value);


 


    //request.Id = 99;


    request.FirstName = "Maurice";


    request.LastName = "Chevalier";


    request.BirthDate = new DateTime(1888, 9, 12);


 


    result = proxy.Operation2(request);


    Console.WriteLine(result.Value);


 


    Console.WriteLine("Regular client is done.");


    Console.ReadLine();


}

Note that I cam completely free to change the person object as long as I use the same Id. Should I change the Id the WorkflowServiceHost would try and find an existing workflow with the changed person Id, fail and throw a FaultException with message “The requested operation could not complete because instance key ‘4d7e998a-4cff-4743-3dd4-2afd5578383c’ could not be found.”. Not quite sure where the Guid comes from but there is no workflow with that instanceId.

The good thing here is that the client doesn’t need to be aware of the workflow implementation on the server and the server can use “regular” message data to determine the correct workflow to send the message to.

 

The Workflow client has about the same change. In this case the Send activity is duplicated but other that that there is very little change and certainly no code that makes the client workflow aware of the service workflow.

private static WorkflowElement CreateWorkflow()


{


    var result = new Sequence();


 


    XNamespace ns = "http://tempuri.org";


 


    var endpoint = new Endpoint()


    {


        Uri = new Uri("http://localhost:8090/Sequence1/Operation1"),


        Binding = new BasicHttpBinding()


    };


 


    var response = new Variable<string>();


    result.Variables.Add(response);


 


    var handle = new Variable<CorrelationHandle>();


    result.Variables.Add(handle);


 


    var person = new Person()


    {


        Id = 7,


        FirstName = "Maurice",


        LastName = "de Beijer",


        BirthDate = new DateTime(1962, 8, 24)


    };


 


    var request = new Send()


    {


        OperationName = "Operation1",


        Endpoint = endpoint,


        AdditionalCorrelations =


        {


           {"ChannelBasedCorrelation", new InArgument<CorrelationHandle>(handle)}


        },


        ServiceContractName = ns + "MyService",


        Value = new InArgument<Person>(person)


    };


    result.Activities.Add(request);


 


 


    var reply = new ReceiveReply()


    {


        Request = request,


        Value = new OutArgument<string>(response)


    };


    result.Activities.Add(reply);


 


    var write = new WriteLine()


    {


        Text = new InArgument<string>(response)


    };


    result.Activities.Add(write);


 


    var delay = new Delay()


    {


        Duration = TimeSpan.FromSeconds(5)


    };


    result.Activities.Add(delay);


 


    var request2 = new Send()


    {


        OperationName = "Operation2",


        Endpoint = endpoint,


        AdditionalCorrelations =


        {


           {"ChannelBasedCorrelation", new InArgument<CorrelationHandle>(handle)}


        },


        ServiceContractName = ns + "MyService",


        Value = new InArgument<Person>(person)


    };


    result.Activities.Add(request2);


 


 


    var reply2 = new ReceiveReply()


    {


        Request = request2,


        Value = new OutArgument<string>(response)


    };


    result.Activities.Add(reply2);


 


    var write2 = new WriteLine()


    {


        Text = new InArgument<string>(response)


    };


    result.Activities.Add(write2);


 


 


    return result;


}



 



Conclusion.



The WF3/WCF marriage in Wf3.5 left quite a bit to be desired and fortunately live has become a lot simpler with WF4.



 



Enjoy!



 



[f1]
[f2]

One thought on “WF4&WCF and message correlation

  1. let me tell you something, WF code conly -no desginer at all- it just sucks… doesn make any sense at all… I’d rather stick to plain c#…

Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>