Overriding Deserialization of Persisted Workflow Instances

This article shows how to override the default deserialization of persisted .NET 3.5 workflow instances to change assembly or type names, and update the definition of non-workflow types.

Introduction

I wanted to know if it is possible to upgrade a workflow instance by overriding the default deserialization mechanism of the SQL workflow persistence service. I searched for ways on how to upgrade persisted workflow instances but most of what I’ve found is about running different workflow versions side-by-side or using a feature called dynamic update. If I only want a simple change like renaming the name or property of a non-workflow type that I use in a workflow, I don’t want to run different workflow versions at the same time (side by side versioning). For example, I have an Order object that I use in a state machine workflow. A workflow instance is started, became idle, and is persisted to the database. Then, you realize that the Order class should have another property to address some defect. I believe that implementing side by side versioning is too tedious just for this, and the persisted workflow instance could be loaded using the latest version.

Sample Application

I’ve created a sample application to demonstrate some problems in workflow deserialization that occurs when you try to make some changes on a type definition, and some possible solutions or workarounds. The following figure shows the main window of the sample WPF application.



Figure 1. Sample Application

We have a state machine workflow that is hosted as a WCF service. This workflow contains 4 states: Initial, New, Processed, and Completed. The service interface provides 3 operations: CreateOrder, ProcessOrder, and CloseOrder. Each operation is implemented by a ReceiveActivity in the workflow. When we click on the New Order button, the workflow is started and an Order object is assigned to a property of the workflow. The current state is then set to the New state. In our WPF application, the current state is distinguished by the thick black border. When the workflow becomes idle, the instance is persisted to a SQL Server database using a SqlWorkflowPersistenceService object that is added earlier as a service of the workflow runtime. You can confirm this by looking at the InstanceState table.



Figure 2. Persisted Workflow Instance
As you can see in the preceding figure, the state column corresponds to the serialized data. This data is in binary form because the SqlWorkflowPersistenceService uses the BinaryFormatter class for serialization and deserialization.

Type and Assembly Changes

Making changes to an assembly name or type definition that is used by a persisted object will cause deserialization problems. Some changes that could cause these problems are signing an assembly, changing the name or version of the assembly, and changing the namespace or name of a type. In our example, the workflow uses an Order class, which is shown below.

namespace DataTransferObjects
{
[Serializable]
[DataContract]
public class Order
{
[DataMember]
public int Id { get; set; }

[DataMember]
public string Customer { get; set; }
}
}

Listing 1. Order Class

This class is defined in the DataTransferObjects assembly. Let’s say we decided to sign the assembly. It is necessary to sign assemblies that contain workflow types or types that are used by a workflow, in case you want to implement workflow versioning. Going back to our example, the Order is currently in the New state. Clicking on the Process Order button produces the following exception:

System.IO.FileLoadException: Could not load file or assembly 'DataTransferObjects, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The located assembly's manifest definition does not match the assembly reference.

The problem here is that the persisted instance references the old assembly which is unsigned (no public key token). When the workflow runtime tries to load the workflow from persistence store, it will deserialize the Order object according to the stored type and assembly names. Since the application can’t find the assembly, it throws the said exception. To prevent this exception we can override how the workflow persistence service loads a workflow. The following code listing shows a class that overrides the LoadWorkflowInstanceState method of the SqlWorkflowPersistenceService class.

public class CustomLoadPersistenceService : SqlWorkflowPersistenceService
{
NameValueCollection parameters;

public CustomLoadPersistenceService(NameValueCollection parameters)
: base(parameters)
{
this.parameters = parameters;
}

protected override Activity LoadWorkflowInstanceState(Guid id)
{
using (WorkflowPersistenceDataContext db = new WorkflowPersistenceDataContext(parameters["ConnectionString"]))
{
Activity activity;

InstanceState instanceState = db.InstanceStates.Where(i => i.uidInstanceID == id).Single();
byte[] activityBytes = instanceState.state.ToArray();

using (MemoryStream stream = new MemoryStream(activityBytes))
{
stream.Position = 0L;

using (GZipStream zipStream = new GZipStream(stream, CompressionMode.Decompress, true))
{
BinaryFormatter binaryFormatter = new BinaryFormatter();
binaryFormatter.SurrogateSelector = ActivitySurrogateSelector.Default;
binaryFormatter.Binder = new DeserializationBinder();

activity = Activity.Load(zipStream, null, binaryFormatter);
}
}

return activity;
}
}
}

Listing 2. Override Loading of Persisted Workflow Instance

I used Reflector to figure out how SqlWorkflowPersistenceService loads a workflow into memory. I copied some of the implementation, used LINQ to SQL to connect to the persistence database and specified a BinaryFormatter object as a parameter of the static Load method of the Activity class. The important part here is to specify a SerializationBinder object to the Binder property of the BinaryFormatter. According to MSDN Library, the SerializationBinder allows users to control class loading and mandate what class to load. The following code listing shows our SerializationBinder.

class DeserializationBinder : SerializationBinder
{
public override Type BindToType(string assemblyName, string typeName)
{
if (assemblyName == "DataTransferObjects, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")
{
assemblyName = "DataTransferObjects, Version=1.0.0.0, Culture=neutral, PublicKeyToken=894ebf3d610d51e5";
}

return Type.GetType(String.Format("{0}, {1}", typeName, assemblyName));
}
}

Listing 3. Change the Type to Load

Before the BinaryFormatter loads an object, it calls the BindToType method and gives you a way to change what type to load. Using the assembly name and type name parameters, we can check if the type the BinaryFormatter will load is an older version, and then return the correct type. The example shown here is a simple and you can make changes to it like use a configuration file to map old versions to new versions. You also have to handle generic types differently as the type name parameter will be complex. Afterwards, we have to add the CustomLoadPersistenceService object to the workflow runtime’s services instead of using SqlWorkflowPersistenceService. Going back to our application, clicking on the Process Order button moves the Order to the Processed state without any exception.

We can also make changes like renaming, adding or removing serializable members. Let’s say we want to rename the Customer property of the Order class to CustomerName. We can use a serialization surrogate for this purpose. A serialization surrogate lets you control how an object is serialized or deserialized without implementing ISerializable. Here is the serialization surrogate for the Order class.

class OrderSerializationSurrogate : ISerializationSurrogate
{
#region ISerializationSurrogate Members

public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
throw new NotImplementedException();
}

public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
{
Order order = obj as Order;
if (order != null)
{
order.Id = info.GetInt32("<Id>k__BackingField");
try
{
order.CustomerName = info.GetString("<CustomerName>k__BackingField");
}
catch (SerializationException)
{
order.CustomerName = info.GetString("<Customer>k__BackingField");
}
}

return null;
}

#endregion
}

Listing 4. Making Changes on Serializable Members

We only need to implement SetObjectData since we are only interested in controlling deserialization. I have to use a try-catch statement here because there is no way currently to check if a specific field exists in the SerializationInfo object. Currently, the return value of the SetObjectData is ignored by the formatter. To use the surrogate, we need to make some changes in the LoadWorkflowInstanceState method.

BinaryFormatter binaryFormatter = new BinaryFormatter();
ActivitySurrogateSelector surrogateSelector = ActivitySurrogateSelector.Default;
surrogateSelector.AddSurrogate(typeof(Order), new StreamingContext(StreamingContextStates.All),
new OrderSerializationSurrogate());
binaryFormatter.SurrogateSelector = surrogateSelector;
binaryFormatter.Binder = new DeserializationBinder();

AppDomain appDomain = AppDomain.CurrentDomain;
appDomain.AssemblyResolve += AppDomain_AssemblyResolve;

activity = Activity.Load(zipStream, null, binaryFormatter);

appDomain.AssemblyResolve -= AppDomain_AssemblyResolve;

surrogateSelector.RemoveSurrogate(typeof(Order), new StreamingContext(StreamingContextStates.All));

Listing 5. Adding a Serialization Surrogate

Note that we have to remove the surrogate after we used it so that it won’t be used for serializing Order objects. Note that the default ActivitySurrogateSelector is static. We also have to remove the OrderSerializationSurrogate object so as to prevent an exception when we try to add it anew on the next call to LoadWorkflowInstanceState.

Workflow and Queue Changes

Now, let’s try to sign the assembly containing the workflow type. It is logical to think that we need to modify our DeserializationBinder class, same as what we did with the DataTransferObjects assembly. However, we will still get a FileLoadException even if we try this. The problem is that our workflows and activities are not serialized directly. The SqlWorkflowPersistenceService uses a serialization surrogate called ActivitySurrogate to control serialization of workflows and activities. The ActivitySurrogate class serializes types that implement IObjectReference and only reference a workflow or activity. These types are ActivityRef, DanglingActivityRef, and ActivitySerializedRef. If you try to list the types that go through the BindToType method, you will see these types instead of workflow or activity types. On deserialization, the GetRealObject method of these IObjectReference objects will be called to get the real object, which is the workflow in our case.

One workaround that I thought of is to create a custom serialization surrogate that will control how the ActivityRef, DanglingActivityRef, and ActivitySerializedRef are deserialized. During deserialization, references to a type’s old version will be replaced with the latest version. However, this proves to be difficult as these 3 types are defined as internal classes. This would require also using a lot of reflection on private members. One simple workaround is to use the AssemblyResolve event of the current AppDomain. We can subscribe to this event before calling the Activity.Load method, where the exception occurs.

AppDomain appDomain = AppDomain.CurrentDomain;
appDomain.AssemblyResolve += AppDomain_AssemblyResolve;

activity = Activity.Load(zipStream, null, binaryFormatter);

appDomain.AssemblyResolve -= AppDomain_AssemblyResolve;

Listing 6. Subscribing to the AssemblyResolve Event

The following code shows the event handler for the AssemblyResolve event. In this method, we can check the name of the assembly that the application failed to resolve, and then return the correct assembly.

private Assembly AppDomain_AssemblyResolve(object sender, ResolveEventArgs e)
{
if (e.Name == "WorkflowServiceLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")
{
return Assembly.Load("WorkflowServiceLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f65aaaea031e2f5b");
}

return null;
}

Listing 7. Resolve Assembly

Now, let’s say we want to change the namespace of the OrderWorkflow from WorkflowServiceLibrary to WorkflowServices. If we try to load a persisted workflow instance referencing the old namespace, we will get the following exception.

System.TypeLoadException: Could not load type 'WorkflowServiceLibrary.OrderWorkflow' from assembly 'WorkflowServiceLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f65aaaea031e2f5b'.

You might think of using the TypeResolve event of the AppDomain class to resolve this problem. Unfortunately, this event won’t be raised when a type is not found in a static assembly. So I recommend not changing the type name of a workflow or activity.

Moving on, let’s say we want to change an operation contract that is used by a ReceiveActivity. In our application, the OrderWorkflow implements the following interface.

[ServiceContract]
public interface IOrderWorkflow
{
[OperationContract]
Guid CreateOrder(Order order);

[OperationContract]
void ProcessOrder();

[OperationContract]
void CloseOrder();
}

Listing 8. Order Workflow Service Contract

Supposed we changed the CloseOrder to CompleteOrder operation contract. Trying to call the CompleteOrder on a persisted workflow instance gives us the following exception: Operation is not implemented by the service. Usually, we get this kind of exception in a state machine workflow when we try to call an operation but not accessible in the current state the workflow instance is in. However, the problem we have here is a bit different. In our workflow, we are using event-driven activities and we can add ReceiveActivity or HandleExternalEvent activities inside them. When an event happens, it is put in a queue that is processed by the workflow. A queue has a specific name, and this gets serialized when a workflow gets persisted. Here is the queue name for the CloseOrder operation.

WorkflowServiceLibrary.IOrderWorkflow, WorkflowServiceLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f65aaaea031e2f5b|CloseOrder

Notice that the operation name is appended to the type and assembly names when the queue name is persisted. When we renamed our service operation from CloseOrder to CompleteOrder, the runtime will not be able to find the queue containing CompleteOrder when we call the operation. Thus, we should rename CloseOrder to CompleteOrder before a user can call the operation. Fortunately, we can do this by overriding the OnActivityExecutionLoad method of the workflow.

public sealed partial class OrderWorkflow : StateMachineWorkflowActivity
{
// Other parts removed for readability

protected override void OnActivityExecutionContextLoad(IServiceProvider provider)
{
QueueHelper.UpdateReferences(provider);
}
}

public class QueueHelper
{
public static void UpdateReferences(IServiceProvider provider)
{
WorkflowQueuingService queueService = provider.GetService(typeof(WorkflowQueuingService)) as WorkflowQueuingService;
FieldInfo persistedQueueStatesInfo = queueService.GetType().GetField("persistedQueueStates", BindingFlags.Instance | BindingFlags.NonPublic);
IDictionary persistedQueueStates = (IDictionary)persistedQueueStatesInfo.GetValue(queueService);

Dictionary<string, string> updatedQueueNamesMap = new Dictionary<string, string>();
IDictionaryEnumerator enumerator = persistedQueueStates.GetEnumerator();
while (enumerator.MoveNext())
{
string currentQueueName = enumerator.Key.ToString();
string updatedQueueName = null;
if (currentQueueName == "WorkflowServiceLibrary.IOrderWorkflow, WorkflowServiceLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f65aaaea031e2f5b|CloseOrder")
{
updatedQueueName = "WorkflowServiceLibrary.IOrderWorkflow, WorkflowServiceLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f65aaaea031e2f5b|CompleteOrder";
}

if (!string.IsNullOrEmpty(updatedQueueName))
{
// Store in dictionary since we cannot change dictionary while
// enumerating items.
updatedQueueNamesMap.Add(currentQueueName, updatedQueueName);
}
}

foreach (KeyValuePair<string, string> keyValue in updatedQueueNamesMap)
{
string currentQueueName = keyValue.Key;
string updatedQueueName = keyValue.Value;

object value = persistedQueueStates[currentQueueName];
persistedQueueStates.Remove(currentQueueName);
persistedQueueStates.Add(updatedQueueName, value);

if (queueService.Exists(currentQueueName))
{
WorkflowQueue queue = queueService.GetWorkflowQueue(currentQueueName);
FieldInfo queueNameFieldInfo = queue.GetType().GetField("queueName", BindingFlags.Instance | BindingFlags.NonPublic);
if (queueNameFieldInfo != null)
{
queueNameFieldInfo.SetValue(queue, updatedQueueName);
}
}
}
}
}

Listing 9. Update Queue Name

I’ve created a QueueHelper class where you just need to check the old queue name and update it accordingly. You can use a configuration file for the type mapping so you don’t need to edit the code every time you change a service interface. The UpdateReferences method is called inside the OnActivityExecutionContextLoad method because we can access the workflow queueing service there. This service contains the persisted queue states (a key-value pair of queue name and object state). We have to use reflection to access the persistedQueueStates variable because it is private. Then, we can remove the old queue name and add the new one. Aside from changing the queue names in persisted queue states, we also need to change the queue names in WorkflowQueue objects.

How about adding or removing activities to or from the workflow? Supposed we remove an activity from the workflow. When we load a persisted workflow instance, we will get the following exception.

System.Runtime.Serialization.SerializationException: The object with ID 33 implements the IObjectReference interface for which all dependencies cannot be resolved. The likely cause is two instances of IObjectReference that have a mutual dependency on each other.

Unfortunately, there is no easy way to convert persisted workflow instances to a new version if you added or removed activities in the type definition, at least for workflows with code (not pure XOML). I thought of using the feature called dynamic update but I have yet to try it. This article focused on WF 3.5 and I’ve yet to determine if the concepts or techniques presented here can be used with WF 4.0.


By Michael Detras   Popularity  (3629 Views)
Biography - Michael Detras
.NET developer. Interested in WPF, Silverlight, and XNA.
My blog