Versioning long running workflows part 2

Part 1
Part 2
Part 3
Part 4

In my previous post I demonstrated how to keep multiple versions of an assembly around and how to use the assemblyBinding element in the app.config to let the runtime load multiple versions of a worklfow. In the end we had both workflows, the first in assembly 1.0.0.0 and the second in assembly 2.0.0.0, running and life seemed to be good [:)]

So is there more to write on the subject? Yes unfortunately there are still some potential problems that need to be addressed [:(].

 

The pitfalls of External Data Exchange

Lets take a look at what happens if we add a HandleExternalEventActivity to the mix. This HandleExternalEventActivity can be used to have a workflow react to input from an external data exchange service, sometimes called local communication.

Lets change the workflow to reflect the following:

image

In this workflow I am waiting for either a DelayActivity to fire or an event to be raised from an external service. Like before the activities are emended in a permanent loop so the workflow is never finished.

I have kept the external data exchange service real simple. The interface looks like this:

using System;


using System.Workflow.Activities;


 


namespace WorkflowLibrary1


{


    [ExternalDataExchange]


    public interface IMyService


    {


        event EventHandler<MyEventArgs> TheEvent;


    }


}

The implementation like this:

using System;


 


namespace WorkflowLibrary1


{


    public class MyService : IMyService


    {


        public event EventHandler<MyEventArgs> TheEvent;


 


        public void OnTheEvent(Guid instanceId)


        {


            if (TheEvent != null && instanceId != Guid.Empty)


            {


                MyEventArgs args = new MyEventArgs(instanceId, DateTime.Now);


                TheEvent(null, args);


            }


        }


    }


}

And the event parameter looks like this:

using System;


using System.Workflow.Activities;


 


namespace WorkflowLibrary1


{


    [Serializable]


    public class MyEventArgs : ExternalDataEventArgs


    {


        public DateTime FiredAt { get; set; }


        public MyEventArgs(Guid instanceId, DateTime firedAt)


            : base(instanceId)


        {


            FiredAt = firedAt;


        }


    }


}

Not much complexity there [:)].

In the main function the workflow runtime is created and configured again. This time we also need to add the MyService as follows:

ExternalDataExchangeService edes = new ExternalDataExchangeService();


workflowRuntime.AddService(edes);


MyService myService = new MyService();


edes.AddService(myService);


 

The complete main function is at the bottom of the post but the most important things are the two Guids. The variable instanceId1 holds the WorkflowInstanceId from a workflow version 1.0.0.0 while the variable instanceId2 holds a WorkflowInstanceId from a workflow version 2.0.0.0. Both of these have been created during two previous runs and are saved by the SqlWorkflowPersistenceService added to the runtime. So lets see what happens when I run the application.

image

As we can see from the screenshot both workflows, the first in assembly 1.0.0.0 and the second in assembly 2.0.0.0, are running together  just fine. So lets see what happens when we raise the event TheEvent as declared in the external data exchange interface.

image

As we can see from the screenshot above the second version of the workflow receives the event just fine but version 1.0.0.0 doesn’t and instead we receive the following exception:

Event "TheEvent" on interface type "WorkflowLibrary1.IMyService" for instance id "c9592a1b-e703-4726-b9bb-16410a7aaaad" cannot be delivered.

Now the workflow did manage to receive the event when it was just created, in fact it could up until the moment we recompiled the application and deployed version 2.0.0.0. Neither the workflow nor the service has changed so what gives?

 

Underneath the covers of the HandleExternalEventActivity

To understand the problem we must first understand a bit more about the internals of the HandleExternalEventActivity. When we created the ExternalDataExchange interface we declared an event taking a parameter derived from ExternalDataEventArgs. And when configuring the HandleExternalEventActivity we specified an event name so everything works using .NET events right? Well no, wrong!

In fact pretty much everything in Windows Workflow Foundation works based on queues. In fact due to the long running nature and the fact you don’t really know when thing will execute it must do so. In fact that is the reason why the event parameter must be marked with the Serializable attribute. In fact the ExternalDataExchangeService watches every for every possible event and converts every event into a queued message.

Okay nice to know but how does that help with this problem?

Well the thing is it needs to be able to find the correct queue to send the message and that is where things get interesting. When we look at WF queues we see that queue names are of type IComparable. Now most of the time when creating a queue the easiest thing to do is use a string or a guid as the queue name. And as both implement IComparable this is perfectly legal. But in the case of the ExternalDataExchangeService  and the HandleExternalEventActivity both need to be able to construct the same queue name based upon the interface and event name. And this is where the EventQueueName enters.

The EventQueueName also implements IComparable and is used internally to uniquely identify a queue name. And when the EventQueueName checks if two queues are the same it doesn’t just use the interface name and the event name but it also compares the assemblies both are defined in. So in this case the workflow version 1.0.0.0 is creating a queue that contains the fact that it is from version 1.0.0.0 as part of the contract while the runtime only uses the last version this creating a queue that contains version 2.0.0.0 as part of the queue name.

We can use the following code to pint the queue information from each workflow:

static void PrintQueues(WorkflowRuntime workflowRuntime, Guid instanceId)


{


    WorkflowInstance instance = workflowRuntime.GetWorkflow(instanceId);


    ReadOnlyCollection<WorkflowQueueInfo> queues = instance.GetWorkflowQueueData();


    foreach (var queue in queues)


    {


        EventQueueName queueName = queue.QueueName as EventQueueName;


        if (queueName != null)


        {


            Assembly assembly = queueName.InterfaceType.Assembly;


            Console.WriteLine(queueName);


            Console.WriteLine(queueName.InterfaceType.Assembly.FullName);


            Console.WriteLine();


        }


    }


}


 

With this as the result:

image

 

The solution

The original function used raise the event looked like this:

static void SendEvent1(WorkflowRuntime workflowRuntime, Guid instanceId)


{


    MyService myService = workflowRuntime.GetService<MyService>();


    myService.OnTheEvent(instanceId);


}

The reason this is failing should now be apparent. After all the main program binds to the version of WorkflowLibrary1 it was build against or version 2.0.0.0. So the GetService() call returns a service object that created a queue name containing version 2.0.0.0 as part of its name and cannot call a HandleExternalEventActivity that creates a queue version 1.0.0.0.

So the solution is to create the correct service object. Doing so isn’t complicated but does unfortunately requires some reflection because we need to work with the same type from an older assembly.

static void SendEvent2(WorkflowRuntime workflowRuntime, Guid instanceId)


{


    WorkflowInstance instance = workflowRuntime.GetWorkflow(instanceId);


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


    ExternalDataExchangeService edes = workflowRuntime.GetService<ExternalDataExchangeService>();


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


    object myService = edes.GetService(type);


 


    if (myService == null)


    {


        myService = Activator.CreateInstance(type);


        edes.AddService(myService);


    }


 


    MethodInfo mi = type.GetMethod("OnTheEvent");


    mi.Invoke(myService, new object[] { instanceId });


}

This code first gets an assembly reference to the assembly the actual workflow was defines in. Depending on the workflow this is either going to return assembly version 1.0.0.0 or version 2.0.0.0. Once we have this reference we retrieve the correct service type from the assembly. This type is different for each assembly so when we ask the ExternalDataExchangeService for the service it will try to return one with the correct type information. If this isn’t found yet it will return null and we can use the Activator.CreateInstance() to create the correct type adding it to the ExternalDataExchangeService for next time.

Next we use reflection to execute the OnTheEvent we defined to fire the event for the workflow which now uses the correct version information when creating the queue name and everything works just fine.

image

The screenshot above shows that both workflow versions are able to receive events again.

Conclusion

The code above works and solves the problem but is not very nice. It assumes that the ExternalDataExchange is defined in the same assembly as the workflow, something that doesn’t need to be the case. So is there a better solution? Yes but that is the subject of another blog post.

Enjoy!

[f1]
[f2]

 

The complete main program:

using System;


using System.Workflow.Activities;


using System.Workflow.Runtime;


using System.Workflow.Runtime.Hosting;


using WorkflowLibrary1;


using System.Reflection;


using System.Collections.ObjectModel;


 


namespace WorkflowConsoleApplication1


{


    class Program


    {


        static void Main(string[] args)


        {


            using (WorkflowRuntime workflowRuntime = new WorkflowRuntime())


            {


                ExternalDataExchangeService edes = new ExternalDataExchangeService();


                workflowRuntime.AddService(edes);


                MyService myService = new MyService();


                edes.AddService(myService);


 


                string connStr = @"Data Source=.\sqlexpress;Initial Catalog=WorkflowPersistence;Integrated Security=True";


                SqlWorkflowPersistenceService persistence = new SqlWorkflowPersistenceService(connStr,


                    true, TimeSpan.FromSeconds(15), TimeSpan.FromMinutes(1));


                workflowRuntime.AddService(persistence);


 


                workflowRuntime.ServicesExceptionNotHandled += (sender, e) =>


                    {


                        Console.WriteLine(e.Exception.Message);


                    };


 


                workflowRuntime.StartRuntime();


 


                Guid instanceId1 = new Guid("c9592a1b-e703-4726-b9bb-16410a7aaaad");


                Guid instanceId2 = new Guid("482e2742-e7c7-45e2-bcd3-8f894d200733");


 


 


                //WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(Workflow1));


                //instanceId2 = instance.InstanceId;


                //instance.Start();


 


 


                bool done = false;


                while (!done)


                {


                    try


                    {


                        ConsoleKeyInfo key = Console.ReadKey(true);


                        switch (key.KeyChar)


                        {


                            case '1':


                                SendEvent2(workflowRuntime, instanceId1);


                                break;


 


                            case '2':


                                SendEvent2(workflowRuntime, instanceId2);


                                break;


 


                            case '5':


                                PrintQueues(workflowRuntime, instanceId1);


                                break;


 


                            case '6':


                                PrintQueues(workflowRuntime, instanceId2);


                                break;


 


                            case '0':


                                done = true;


                                break;


                        }


                    }


                    catch (Exception ex)


                    {


                        Console.WriteLine(ex.Message);


                    }


                }


 


                workflowRuntime.StopRuntime();


            }


        }


 


        static void SendEvent1(WorkflowRuntime workflowRuntime, Guid instanceId)


        {


            MyService myService = workflowRuntime.GetService<MyService>();


            myService.OnTheEvent(instanceId);


        }


 


        static void SendEvent2(WorkflowRuntime workflowRuntime, Guid instanceId)


        {


            WorkflowInstance instance = workflowRuntime.GetWorkflow(instanceId);


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


            ExternalDataExchangeService edes = workflowRuntime.GetService<ExternalDataExchangeService>();


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


            object myService = edes.GetService(type);


 


            if (myService == null)


            {


                myService = Activator.CreateInstance(type);


                edes.AddService(myService);


            }


 


            MethodInfo mi = type.GetMethod("OnTheEvent");


            mi.Invoke(myService, new object[] { instanceId });


        }


 


        static void PrintQueues(WorkflowRuntime workflowRuntime, Guid instanceId)


        {


            WorkflowInstance instance = workflowRuntime.GetWorkflow(instanceId);


            ReadOnlyCollection<WorkflowQueueInfo> queues = instance.GetWorkflowQueueData();


            foreach (var queue in queues)


            {


                EventQueueName queueName = queue.QueueName as EventQueueName;


                if (queueName != null)


                {


                    Assembly assembly = queueName.InterfaceType.Assembly;


                    Console.WriteLine(queueName);


                    Console.WriteLine(queueName.InterfaceType.Assembly.FullName);


                    Console.WriteLine();


                }


            }


        }


    }


}



11 thoughts on “Versioning long running workflows part 2

  1. Hi, The link to part 4 of your tutorial [at the top] links back to part 3 – Is this because part 4 is not yet available?

    On a different note, thank you so much for the very easy to understand tutorial – I’ve had Workflows dropped on me from a great height and noone in the department hs used them before. I was worried about versioning and although it’s something to be aware of, it shouldn’t be a huge issue after reading this.

    Many thanks!

    Please do let us nkow about part 4 if it’s available :)

  2. I have been struggling with versioning workflows that contain HandleExternalEvent activities for a long time and this is by far the best article I have seen on the subect. I am still struggling with a few items that you might be able to help me with. First off, where do you put the versions folder in a web application and what would the config look like in the web.config (what would the path to the .dll look like)? Also, If you suggest replacing HandleExternalEvent activities with custom activities how do you add the handlers and parameters that would be in the HandleExternalEvent activity?

    Thanks!

  3. Ho Joe,

    Either using the GAC or side by side versioning using the config file and multiple folders should work just fine in an ASP.NET application.’

    If you go the route of creating custom activities you would create public methods on your activity which wou would call with the data needed as parameters. Then use dependency properties to expose the data as needed.

  4. Thanks Maurice, i’m getting much closer!

    Now i am stuck on my custom ExternalEventArgs that I am passing to the event. I am getting an error saying that “NurseUpdateEventArgs” cannot be converted to “NurseUpdateEventArgs”. I am pretty sure it is because I am getting the NurseUpdateEventArgs for the current version, not the version that is running. How would I use reflection to get a reference to the correct EventArgs class (defined in my workflow)?

    Thanks again for a great article!

  5. Thanks for your great series of articles !

    Still I am facing an issue with the method ‘SendEvent2′ at the line :
    Type type = assembly.GetType(typeof(wfOrderWorkflows.IOrderService).FullName);
    returns “The type ‘wfOrderWorkflows.IOrderService’ exists in both ‘wfOrderWorkflows.dll’ and ‘wfOrderWorkflows.dll'”, and IOrderService is in the same assembly than the workflow.
    Am I doing something wrong ?

  6. So I don’t have the issue with ‘The type … exists in both…’, but now I have another issue:
    I was getting the exception:
    ‘Order cannot be cast to Order’ at the line: ‘mi.Invoke(myService, new object[] { selectedOrder });’.
    So I used the Activator.CreateInstance method to create an Order object corresponding to the right assembly.
    But now, after raising the event, I call ManualWorkflowScheduler.RunWorkflow and it returns false, while it used to returns true.

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>