WF Persistence - Where [DataContract] != [Serializable]

Published Sat, Nov 1 2008 3:27 | William

Let's say you were using Workflow Foundation (WF) and Windows Communication Foundation (WCF). Assume you wanted to use WF's Persistence mechanism (which requires types to be Serializable). And assume that all the objects you were using in the workflow were decorated with the [DataContract] attribute.  Would you expect any problems related to Serialization?  (I can but to the chase really quickly here if you don't want my belabored background information and explanation. If you're creating a type to use w/ Workflow Foundation that will be saved via the Workflow Foundation Persistence Service which makes use of the SqlWorkflowPersistenceService class, marking the class as a [DataContract] alone will not work for WF's persistence mechanism. It does make the class serializable in a literal sense and will suffice [provided you meet the other requirements of a [DataContract] for WCF's serialization needs, but it is insufficient for WF (see the References section at the end of this post for more details on WF Serialization) - so make sure you use the Serializable attribute on any type that'll be consumed by the Persistence Service).

MSDN describes the DataContract class the following way:

Specifies that the type defines or implements a data contract and is serializable by a serializer, such as   the DataContractSerializer. To make their type serializable, type authors must define a data contract for their type.

If you spend a lot of time in WCF, you'd likely assume that b/c types decorated with the DataContract attribute would satisfy any requirement for the type to be Serializable.  But you'd likely be wrong. Yes, a type that is a [DataContract]  is serializable in the literal sense, but it's serialized by the DataContractSerializer (see the References section at the end of this post for more details on WF Serialization)

So why is this an issue and what do you do about it?  Often, non-trivial workflows span an amount of time that exceeds any given user's session.  In fact, it's not uncommon for a non-trivial workflow to span as much as 30 days.  Using a simple example, let's say that you are writing a Workflow to manage new employees.  Further assume that there are several document requirements and that any deficiencies would result in severe penalties or liability risks.  Just to make it clear, assume that each of the following must be procured before a new employee can start working.  Further, assume that b/c of fees associated with each, if any previous one fails, that the whole process terminates (if someone's reference checks don't pass, there's no need to spend the money for a full background investigation as you know you're not going to hire them). [Also, this isn't a post on workplace hiring guidelines and is solely used for illustration - no need to point out why 'its bad business to drug test or whatever else].

  1. Employment Application
  2. Proof of Citizenship or ability to work in the U.S.
  3. State withholding form
  4. Federal withholding form
  5. Reference check
  6. License check
  7. Completed Drug Screen
  8. Full Background check

Implementation policies will vary from company to company but assume that all of these tasks wouldn't be completed within the user session that first created the entity. If you couldn't persist the workflow somehow and come back to it later, it would be of very little use.   Workflow foundation provides an out-of-the-box solution named not surprisingly -  Windows Workflow Persistence Service to help with long running workflows

Since Workflow Foundation is still in its infancy in terms of user adoption, many shops don't have well established design rules in place specifically for Workflow items.  This can have some profound (although easily addressed in most cases) effects on Workflow development. It's common for a workflow to start out small in terms of steps and happen relatively quickly.  When this is the case, there's little need for a persistence mechanism for the workflow.  But as people get comfortable with WF and are willing to try more involved solutions, steps get added that often increase the time span the workflow could run to a point that's longer than the user session.  For people new to WF, adding that first activity that'll possibly span a large amount of time is the first time they actually think seriously about the persistence service. And that's the point at which they often go back and try to retrofit things.  In many cases, the number of "things" will be fairly substantial so it's quite easy to overlook something.  Once you implement the persistence service, you'll typically go through the testing phase and make sure everything persist and can be retrieved correctly.  And this is the time serialization problems with show themselves - often in a manner that's hard to isolate b/c the number of items involved. 

In some cases, if there's a serialization problem, a fault will result.  Unhandled faults in WF cause behavior that deviates from what you'd typically expect. So depending on your logging and tracing, you may or may not find the issue quickly. in many cases, you'll find the fault is related to serialization and you'll address it on the specific type, then go back and try to find every other type and make it serializable as well.  This is likely to be overkill though - b/c you're likely to have some types that aren't involved in anything that's persisted and consequently don't require persistence.  So you try to only make the items that will be persisted serializable. Until you get to the point where you're addressing everything in the planning and design stages, you're likely to run into situations like this and the only way out typically involves a lot of trial-and-error.  That's why this post is relevant. If you go back and define each type that will be persisted as a [DataContract] , it'll take you some time to do, may necessitate you adding new references to some projects and ultimately, after all that's done, won't fix the problem. The following type may work all day long when just dealing with WCF's serialization requirements...

namespace Ger911.HCStandard.Core.Shared
{
    [DataContract]
    public class MatrixItem
    {
        [DataMember]
        public String EntityName { get; set; }
        [DataMember]
        public String ContactName { get; set; }
        [DataMember]
        public Int32 BedCount { get; set; }
        [DataMember]
        public String HospitalStatus { get; set; }
        [DataMember]
        public DateTime ModifiedDateTime { get; set; }
    }
}

 

But unless you modify it like this, you'll be in for some unpleasant surprises in WF:

namespace Ger911.HCStandard.Core.Shared
{

    [Serializable]
    [DataContract]
    public class MatrixItem
    {
        [DataMember]
        public String EntityName { get; set; }
        [DataMember]
        public String ContactName { get; set; }
        [DataMember]
        public Int32 BedCount { get; set; }
        [DataMember]
        public String HospitalStatus { get; set; }
        [DataMember]
        public DateTime ModifiedDateTime { get; set; }
    }
}

References:

Using Red Gate's Reflector, here's what you'll see under the hood of the WorkflowPesistenceService class's RestoreFromDefaultSerializedForm and GetDefaultSerializedForm.  For further insight into this, use Reflector and examine the SqlWorkflowPersistenceService:

 

RestoreFromDefaultSerializedForm

protected static Activity RestoreFromDefaultSerializedForm(byte[] activityBytes, Activity outerActivity)
{
            Activity activity;
            DateTime now = DateTime.Now;
            MemoryStream stream = new MemoryStream(activityBytes);
            stream.Position = 0L;
            using (GZipStream stream2 = new GZipStream(stream, CompressionMode.Decompress, true))
            {
                activity = Activity.Load(stream2, outerActivity);
            }
            TimeSpan span = (TimeSpan)(DateTime.Now - now);
            WorkflowTrace.Host.TraceEvent(TraceEventType.Information, 0, "Deserialized a {0} to length {1}. Took {2}.",
                new object[] { activity, stream.Length, span }); return activity;
}

 

GetDefaultSerializedForm

protected static byte[] GetDefaultSerializedForm(Activity activity)
{
          DateTime now = DateTime.Now;
          using (MemoryStream stream = new MemoryStream(0x2800))
          {
              stream.Position = 0L;
              activity.Save(stream);
              using (MemoryStream stream2 = new MemoryStream((int)stream.Length))
              {
                  using (GZipStream stream3 = new GZipStream(stream2, CompressionMode.Compress, true))
                  {
                      stream3.Write(stream.GetBuffer(), 0, (int)stream.Length);
                  }
                  ActivityExecutionContextInfo info = (ActivityExecutionContextInfo)activity.GetValue(Activity.ActivityExecutionContextInfoProperty);
                  TimeSpan span = (TimeSpan)(DateTime.Now - now);
                  WorkflowTrace.Host.TraceEvent(TraceEventType.Information, 0, "Serialized a {0} with id {1} to length {2}. Took {3}.", new object[] { info, info.ContextGuid, stream2.Length, span });
                  byte[] array = stream2.GetBuffer();
                  Array.Resize<byte>(ref array, Convert.ToInt32(stream2.Length));
                  return array;
              }
          }
}

 

DataContractSerializer

public sealed class DataContractSerializer : XmlObjectSerializer
{
    // Fields
    private IDataContractSurrogate dataContractSurrogate;
    private bool ignoreExtensionDataObject;
    internal Dictionary<XmlQualifiedName, DataContract> knownDataContracts;
    private ReadOnlyCollection<Type> knownTypeCollection;
    internal IList<Type> knownTypeList;
    private int maxItemsInObjectGraph;
    private bool needsContractNsAtRoot;
    private bool preserveObjectReferences;
    private DataContract rootContract;
    private XmlDictionaryString rootName;
    private XmlDictionaryString rootNamespace;
    private Type rootType;

    // Methods
    public DataContractSerializer(Type type);
    public DataContractSerializer(Type type, IEnumerable<Type> knownTypes);
    public DataContractSerializer(Type type, string rootName, string rootNamespace);
    public DataContractSerializer(Type type, XmlDictionaryString rootName, XmlDictionaryString rootNamespace);
    public DataContractSerializer(Type type, string rootName, string rootNamespace, IEnumerable<Type> knownTypes);
    public DataContractSerializer(Type type, XmlDictionaryString rootName, XmlDictionaryString rootNamespace, IEnumerable<Type> knownTypes);
    public DataContractSerializer(Type type, IEnumerable<Type> knownTypes, int maxItemsInObjectGraph, bool ignoreExtensionDataObject, bool preserveObjectReferences, IDataContractSurrogate dataContractSurrogate);
    public DataContractSerializer(Type type, string rootName, string rootNamespace, IEnumerable<Type> knownTypes, int maxItemsInObjectGraph, bool ignoreExtensionDataObject, bool preserveObjectReferences, IDataContractSurrogate dataContractSurrogate);
    public DataContractSerializer(Type type, XmlDictionaryString rootName, XmlDictionaryString rootNamespace, IEnumerable<Type> knownTypes, int maxItemsInObjectGraph, bool ignoreExtensionDataObject, bool preserveObjectReferences, IDataContractSurrogate dataContractSurrogate);
    internal static DataContract GetDataContract(DataContract declaredTypeContract, Type declaredType, Type objectType);
    internal override Type GetDeserializeType();
    internal override Type GetSerializeType(object graph);
    internal static Type GetSurrogatedType(IDataContractSurrogate dataContractSurrogate, Type type);
    private void Initialize(Type type, IEnumerable<Type> knownTypes, int maxItemsInObjectGraph, bool ignoreExtensionDataObject, bool preserveObjectReferences, IDataContractSurrogate dataContractSurrogate);
    private void Initialize(Type type, XmlDictionaryString rootName, XmlDictionaryString rootNamespace, IEnumerable<Type> knownTypes, int maxItemsInObjectGraph, bool ignoreExtensionDataObject, bool preserveObjectReferences, IDataContractSurrogate dataContractSurrogate);
    internal override bool InternalIsStartObject(XmlReaderDelegator reader);
    internal override object InternalReadObject(XmlReaderDelegator xmlReader, bool verifyObjectName);
    internal override void InternalWriteEndObject(XmlWriterDelegator writer);
    internal override void InternalWriteObject(XmlWriterDelegator writer, object graph);
    internal override void InternalWriteObjectContent(XmlWriterDelegator writer, object graph);
    internal override void InternalWriteStartObject(XmlWriterDelegator writer, object graph);
    public override bool IsStartObject(XmlDictionaryReader reader);
    public override bool IsStartObject(XmlReader reader);
    public override object ReadObject(XmlReader reader);
    public override object ReadObject(XmlDictionaryReader reader, bool verifyObjectName);
    public override object ReadObject(XmlReader reader, bool verifyObjectName);
    internal static object SurrogateToDataContractType(IDataContractSurrogate dataContractSurrogate, object oldObj, Type surrogatedDeclaredType, ref Type objType);
    public override void WriteEndObject(XmlDictionaryWriter writer);
    public override void WriteEndObject(XmlWriter writer);
    public override void WriteObject(XmlWriter writer, object graph);
    public override void WriteObjectContent(XmlDictionaryWriter writer, object graph);
    public override void WriteObjectContent(XmlWriter writer, object graph);
    public override void WriteStartObject(XmlDictionaryWriter writer, object graph);
    public override void WriteStartObject(XmlWriter writer, object graph);

    // Properties
    public IDataContractSurrogate DataContractSurrogate { get; }
    public bool IgnoreExtensionDataObject { get; }
    internal Dictionary<XmlQualifiedName, DataContract> KnownDataContracts { get; }
    public ReadOnlyCollection<Type> KnownTypes { get; }
    public int MaxItemsInObjectGraph { get; }
    public bool PreserveObjectReferences { get; }
    private DataContract RootContract { get; }
}

Comments

# Kelly Martens said on November 2, 2008 4:03 PM:

Excellent subject and thinking on this Bill. Though it is in fact in its infancy I do think we will see widepsread adoption over the next few years.

# Russell said on November 25, 2008 6:10 PM:

This article was given to me by a coworker, as he had to hear my venting due to this issue.  Thanks, it describes the WCF/WF data divide well.

The kicker, for me, is that the ExtensionDataObject used for round-tripping in WCF is neither [Serializable], nor inheritable.  So all the easy ways I could've made my WCF data contracts play nice with everybody are cut off.  Any suggestions?

Search

This Blog

Tags

Community

Archives

News

My other sites

Cool Stuff

Book Stuff

Security

ORM

Data Access

Funny Stuff

Compact Framework Stuff

Web Casts

My KnowledgeBase Articles

My MVP Profile

Design Patterns

Performance

Debugging

Remoting

My Fellow Authors

My Books

LINQ

Misc

Speech

Syndication

Email Notifications