Versioning long running workfows part 3

Part 1
Part 2
Part 3
Part 4

In the first article of this series I demonstrated how to get multiple versions of a workflow running side by side in the same  workflow runtime. The most important thing was that you need to keep every version of the assembly around and use the assemblyBinding element in the app.config to let the runtime know where each version was on disk. Once done life was good [:)]

In the second part I demonstrated how a HandleExternalEventActivity was version dependent and you needed to use the version specific service to send a message to the workflow. It worked but the as the code was not exactly pretty life was just ok [:(].

What is wrong with the HandleExternalEventActivity?

Well there is nothing really wrong with the HandleExternalEventActivity but it is a very thin layer over the actual workflow structures it tries to hide. And these structures are the workflow queuing mechanism! Internally everything is turned into a message and send through a queue. So if this is only a thin abstraction layer why not use the original API in the first place.

That is exactly what I advise, leave the external data exchange mechanism for what it is and just create a custom workflow activity.

What does it take to implement the same behavior using a custom activity? Not a whole lot actually so lets take a look.

image

Above is the new workflow with the custom activity.

Because the custom activity is used as the first child in one of the branches of a ListenActivity it must implement the IEventActivity interface and we must override the Execute method to do something once a message is found in the queue.

using System;


using System.Workflow.Activities;


using System.Workflow.ComponentModel;


using System.Workflow.Runtime;


 


namespace WorkflowLibrary1


{


    public partial class MyActivity : Activity, IEventActivity


    {


        public static string TheQueueName = "MyActivityQueueName";


 


        protected override ActivityExecutionStatus Execute(


            ActivityExecutionContext executionContext)


        {


            WorkflowQueuingService wqs = executionContext.GetService<WorkflowQueuingService>();


            WorkflowQueue queue = wqs.GetWorkflowQueue(QueueName);


            object data = queue.Dequeue();


            Console.WriteLine("Received {0} in {1}", data, GetType().Assembly.FullName);


            return base.Execute(executionContext);


        }


 


        public IComparable QueueName


        {


            get { return TheQueueName; }


        }


 


        public void Subscribe(


            ActivityExecutionContext parentContext, 


            IActivityEventListener<QueueEventArgs> parentEventHandler)


        {


            WorkflowQueuingService wqs = parentContext.GetService<WorkflowQueuingService>();


            WorkflowQueue queue = null;


            if (wqs.Exists(TheQueueName))


                queue = wqs.GetWorkflowQueue(TheQueueName);


            else


                queue = wqs.CreateWorkflowQueue(QueueName, true);


            queue.RegisterForQueueItemAvailable(parentEventHandler);


        }


 


        public void Unsubscribe(


            ActivityExecutionContext parentContext, 


            IActivityEventListener<QueueEventArgs> parentEventHandler)


        {


            WorkflowQueuingService wqs = parentContext.GetService<WorkflowQueuingService>();


            WorkflowQueue queue = wqs.GetWorkflowQueue(TheQueueName);


            queue.UnregisterForQueueItemAvailable(parentEventHandler);


        }


    }


}

 

I am not going to explain the details except that the message is read in the Execute and is printed as is along with the assembly version. And guess what, If I create multiple versions of the workflow and run them side by side life is good [:)].

image

Sending the data was easy too with only the following code:

static void SendEvent1(WorkflowRuntime workflowRuntime, Guid instanceId)


{


    WorkflowInstance instance = workflowRuntime.GetWorkflow(instanceId);


    instance.EnqueueItem(MyActivity.TheQueueName, 1, null, null);


}

However the data send in this simple example is only an integer. Lets see what happens when we use a custom object instead of the single integer.

The case of the custom message type

In the previous example everything worked just fine because we only send in a real simple data type, an integer. However when we switch to a custom type things are less perfect [:(].

For this example I am using the following data type:

namespace WorkflowLibrary1


{


    public class MyData


    {


        public MyData(int data)


        {


            TheData = data;


        }


 


        public int TheData { get; set; }


 


        public override string ToString()


        {


            return string.Format("Data = {0}", TheData);


        }


    }


}

Still real simple but non the less a custom type we can version along with the workflow and its activities. The code to send the message becomes as follows:

static void SendEvent1(WorkflowRuntime workflowRuntime, Guid instanceId)


{


    WorkflowInstance instance = workflowRuntime.GetWorkflow(instanceId);


    MyData data = new MyData(1);


    instance.EnqueueItem(MyActivity.TheQueueName, data, null, null);


}

Again not a spectacular change as we only substitute the integer for an object of type MyData. The activity execute changes to the following:

protected override ActivityExecutionStatus Execute(


    ActivityExecutionContext executionContext)


{


    WorkflowQueuingService wqs = executionContext.GetService<WorkflowQueuingService>();


    WorkflowQueue queue = wqs.GetWorkflowQueue(QueueName);


    MyData data = (MyData)queue.Dequeue();


    Console.WriteLine("Received {0} in {1}", data, GetType().Assembly.FullName);


    return base.Execute(executionContext);


}

Again no big change, all we are doing is casting the data from the queue to be of type MyData. When we run this with a workflow started using the latest version everything is just fine but when I send a message to a workflow version 1.0.0.0 we receive the following InvalidCastException message:

Unable to cast object of type ‘WorkflowLibrary1.MyData’ to type ‘WorkflowLibrary1.MyData’

image

That message seems kind of weird as it is claiming that we cannot cast MyData to MyData!. Weird as this may seem it is completely true!

The problem, and things would have been clearer of the message include this information is that we cannot cast between two different versions of the same type as they are really different types.

The solution

Just like the previous time the solution is to create an object of the same type as was used in the custom workflow activity. The concept is pretty much the same as last time with the ExternalDataExchangeService and requires a bit of reflection.

static void SendEvent2(WorkflowRuntime workflowRuntime, Guid instanceId)


{


    WorkflowInstance instance = workflowRuntime.GetWorkflow(instanceId);


    Assembly assembly = instance.GetWorkflowDefinition().GetType().Assembly;


    Type type = assembly.GetType(typeof(MyData).FullName);


    object data =Activator.CreateInstance(type, new object[] {1});


    instance.EnqueueItem(MyActivity.TheQueueName, data, null, null);


}



using this code both the workflow and the runtime are perfectly happy. That said, personally I don’t really like having to resort to reflection every time [:(]



image



So instead of using typed objects you might just want to resort to using basic framework objects which will remain the same version until a major .NET framework upgrade. One easy way to send data is just embed it in an XML document or, just as the workflow parameters, a Dictionary<string, object> and pass that along.



Enjoy!



[f1]
[f2]

10 thoughts on “Versioning long running workfows part 3

  1. This is a really good series on what is actually a massive problem with Workflow Foundation.

    I had to find my own solutions to these problems which basically come to the same point as yours (side-by-side execution from GAC, using reflection to call methods on different versions of the data exchange service).

    It works, but it ain’t pretty!

    To me, this story should be built into WF so that we don’t have to write this code ourselves (and have various aneurisms on the way) – we should be able to tell WF what versions we expect to run, and WF should do all the jiggery-pokery to do the actual execution, including whether to use reflection or not.

    To me it just shows the immaturity of the product and I really hope that Microsoft are working on this stuff for future releases.

  2. What would be really good, is this same series of articles re-written for .NET 4.0. It strikes me that all this ugly reflection code could be eliminated using .NET 4.0 dynamically typed objects.

    Plus perhaps there are better constructs available in WF .NET 4.0 to handle these scenarios.

  3. @Michael,

    The dynamic stuff in C#4 os going to help a lot here. But then WF4 is a completely different animal so we might not even need to.

  4. @Alistair,

    No sorry, however the exact same approach should work using an ASP.NET application. Otherwise you can add you assemblies to the GAC instead of using a private probing path.

  5. In relation to the custom message type, would the same runtime redirection that is utilized in part 4 of this series for the workflow assembly solve the issue for the custom message type if in fact that type is defined in a separate assembly?

  6. @MattK

    That help in finding the right assembly but you still need to make sure you create an object of exactly the right type using Activator.CreateInstance().

  7. So if you are going to replace a HandleExternalEvent activity with a custom activity how do you handle the Handlers and Parameters that are available in the HandleExternalEvent activity (IE, how do you get your custom activity to have the same handlers and parameters (e, sender) as the HandleExternalEvent activity?

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>