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.
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
.
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'
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
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!
www.TheProblemSolver.nl
Wiki.WindowsWorkflowFoundation.eu