Wednesday, December 17, 2008

WF Workflow Custom Class Error

Error

After deploying your WF workflow and associating it to a list, you may see an issue where your workflow is immediately completed. When digging into the SharePoint logs, you will find the following:

DehydrateInstance: System.Runtime.Serialization.SerializationException: End of Stream encountered before parsing was completed. at System.Runtime.Serialization.Formatters.Binary.__BinaryParser.Run() at System.Runtime.Serialization.Formatters.Binary.ObjectReader.Deserialize(HeaderHandler handler, __BinaryParser serParser, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage) at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream serializationStream, HeaderHandler handler, Boolean fCheck, Boolean isCrossAppDomain, IMethodCallMessage methodCallMessage) at System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Deserialize(Stream serializationStream) at System.Workflow.ComponentModel.Activity.Load(Stream stream, Activity outerActivity, IFormatter formatter) at System.Workflow.ComponentModel.Activity.Load(Stream stream, Activity outerActivity) at System.Workflow.Runtime.Hosting.WorkflowPersistenceService.RestoreFromDefaultSerializedForm(Byte[] activityBytes, Activity outerActivity) at Microsoft.SharePoint.Workflow.SPWinOePersistenceService.LoadWorkflowInstanceState(Guid instanceId) at System.Workflow.Runtime.WorkflowRuntime.InitializeExecutor(Guid instanceId, CreationContext context, WorkflowExecutor executor, WorkflowInstance workflowInstance) at System.Workflow.Runtime.WorkflowRuntime.Load(Guid key, CreationContext context, WorkflowInstance workflowInstance) at System.Workflow.Runtime.WorkflowRuntime.GetWorkflow(Guid instanceId) at Microsoft.SharePoint.Workflow.SPWinOeHostServices.DehydrateInstance(SPWorkflowInstance workflow)

Cause

What is happening is that you have a custom class as a local attribute of your workflow and WF does not know how deserialize and serialize your object instance. This is not problem with types like int, string, datetime, etc. but will be for any custom class.

This error is described as a pitfall that developers run into by the Microsoft SharePoint Team.

  • Pitfall: Re-using non-serializable SharePoint objects after rehydration
    Many non-workflow specific SharePoint objects, like SPListItem, are not serializable, so when the workflow dehydrates, these items become invalid. So if you try to use them when the workflow wakes up, you will get an error. To avoid this, refetch items if you're using them after a workflow rehydrates.
  • Pitfall: Forgetting to make custom classes serializable
    When creating your own class in a workflow, don't forget to add the "Serializable" attribute to the class. If you don't do this and declare a variable of that class in the workflow's scope, the workflow will fail when it tries to go to sleep and serialize the class. If you notice that the workflow "completes" when it isn't supposed to, this may be the culprit.

Ranting

From what few posting on this, I found people trying to create local copies of the association and initiation data in a generated class by using the xsd.exe tool. Their solution was to make it [System.NonSerialized] so that it is ignored as part of the hydration process. This really makes no sense because if you make the attribute NonSerialized the value is not retained across events and what is the point of making an object instance at higher level of scope if it is only used locally in a method? As well, the association and initiation data is always available to you through the SPWorkflowActivationProperties.

You will not get this error if you have custom classes that are instantiated within the scope of an event or method. So if you have a variable that is class level scope of the workflow, you will get this error. Ok – I digress…

Resolution

The resolution is really simple, you just need to have your classes support ISerializable and you will be off and running. The specific business rule required was that everyone needed to approve before continuing. I wanted to create a simple structure that would track how many task assignees and approved before continuing to the next step in the workflow. I really did not feel like create an external DB with persistence was warranted. Instead I wanted to create some custom classes and just let them have a lifetime with the workflow instance.

Here is my solution was pretty simple. Here is my TaskAssignee class.

    [Serializable()]
public class TaskAssignee : ISerializable
{
#region Attributes

private string _accountName;
private string _displayName;
private string _email;
private ContractApprovalAction _action;

#endregion

#region Constructor

public TaskAssignee()
{
_action = ContractApprovalAction.NoAction;
}

#endregion

#region Properties

public string AccountName
{
get
{
return _accountName;
}
set
{
_accountName = value;
}
}

public string DisplayName
{
get
{
return _displayName;
}
set
{
_displayName = value;
}
}

public string Email
{
get
{
return _email;
}
set
{
_email = value;
}
}

public ContractApprovalAction Action
{
get
{
return _action;
}
set
{
_action = value;
}
}

#endregion

#region ISerializable
//http://www.codeproject.com/KB/cs/objserial.aspx

//Deserialization constructor.
public TaskAssignee(SerializationInfo info, StreamingContext ctxt)
{
_accountName = (String)info.GetValue("AccountName", typeof(string));
_displayName = (String)info.GetValue("DisplayName", typeof(string));
_email = (String)info.GetValue("Email", typeof(string));
_action = (ContractApprovalAction)info.GetValue("Action", typeof(int));
}

//Serialization function.
public void GetObjectData(SerializationInfo info, StreamingContext ctxt)
{
info.AddValue("AccountName", _accountName);
info.AddValue("DisplayName", _displayName);
info.AddValue("Email", _email);
info.AddValue("Action", Convert.ToInt32(_action));
}

#endregion

}

Here is a class that wraps a collection of TaskAssignees which is also Serialized.

    [Serializable()]
public class TaskAssignees : ISerializable
{
#region Attributes

private List<TaskAssignee> _assignees;

#endregion

#region Constructor

public TaskAssignees()
{
_assignees = new List<TaskAssignee>();
}

#endregion

#region Properties

public List<TaskAssignee> Assignees
{
get
{
return _assignees;
}
}

#endregion

#region Methods

/// <summary>
/// Reset the task assignee list.
/// </summary>
private void Reset() {
_assignees = new List<TaskAssignee>();
}

#endregion

#region ISerializable
//http://blog.paranoidferret.com/index.php/2007/04/27/csharp-tutorial-serialize-objects-to-a-file/

//Deserialization constructor.
public TaskAssignees(SerializationInfo info, StreamingContext ctxt)
{
_assignees = (List<TaskAssignee>)info.GetValue("Assignees", typeof(List<TaskAssignee>));
}

//Serialization function.
public void GetObjectData(SerializationInfo info, StreamingContext ctxt)
{
info.AddValue("Assignees", _assignees);
}

#endregion

}

Finally I just add the following to my state workflow and now I can populated this on a task create event and then use the same object instance in a task changed event.

private TaskAssignees _taskAssignees;

References

No comments: