Constructing WF

July 9, 2007

 

This post is motivated by “Deconstructing WF” chapter from the book Essential Windows Workflow Foundation. Essential WF  is one of my favorite technical books to come out in the last few years. I have also borrowed Don Box’s famous “you have just engineered the Component Object Model” approach to bring out the concepts behind the Windows Workflow Foundation.  

Get the complete sample code for this post here

Why Workflow

Managed code has helped make executables more transparent.  Unlike the unmanaged code, rich metadata associated with the managed source code is accessible at runtime via reflection.

However, it will be beneficial to move beyond the ability to access the metadata at runtime. For instance, it would be useful to know the state of an executable  at a  point time and if needed,  the ability to dynamically modify the structure of the program. It would also be very helpful if the runtime provided support for programs that tend to run for long periods of time.  For example,  runtime should be able to move a long-running program (that is paused waiting for an external stimulus) from the memory to a storage medium and then restart it at a later time.   

One approach for  achieving the aforementioned level of transparency is to add a workflow abstraction  layer over the .NET framework (alternatively think of it as being a meta-runtime over the .NET runtime ). Developers would use the abstraction layer to build workflow style programs, instead of  of building programs directly on top of the .NET framework. Building programs using the abstraction layer has the following benefits: (i)   Higher level constructs, in the form of pre-fabricated chunks of code referred to as workflow steps, that can be stitched together to model a workflow. This is in contrast to low-level IL instructions that make up a program written directly against the .NET runtime.  Since the meta-runtime is now responsible for executing the program, it has deep insight into the state of the program execution as well the ability to dynamically modify the structure of the program (ii) Meta-runtime can provide plumbing services such as scheduling, persistence, etc.

Let us a look at how we can implement such a generic workflow framework.

Generic Workflow Framework

Serialization Format

Consider the logic depicted in the adjacent diagram. This rather trivial program logic consists of two steps:  DoWork1 and DoWork2, executed in a sequence.   

The first thing we need to do is to represent this logic in a format that can be read by our generic workflow framework. An XML based format would make sense. However, it turns out that in XAML, we already have a XML based object serialization format. So rather than inventing a new format, let us just use XAML as the serialization format. This way we can reuse the XAML parsing capabilities built into the framework. 

For now our framework is quite rudimentary. There is no tooling available that would allow users to graphically define the workflow and automatically generate the XAML code. Users will need to manually develop the XAML. By they way, let us use .XOML file extension for the XAML files we create  for storing our workflow programs. This will make it easy for us to differentiate workflow XAML files  with other XAML files. 

Here is what an XOML representation the above program would look like

    <y:Workflow 
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:y="clr-namespace:ConstructingWF;assembly=ConstructingWF"    >
        <y:Workflow.Steps>
            <y:WorkflowStep x:Name="DoWork1" SimulatedTimeOut="11"></y:WorkflowStep>
            <y:WorkflowStep x:Name="DoWork2" SimulatedTimeOut="01"></y:WorkflowStep>
        </y:Workflow.Steps>
    </y:Workflow>

The root node represents the workflow program and maps to an instance of the Workflow class. It contains zero or more IWorkflowStepBase class instances. IWorkflowStepBase represents a prefabricated block of logic or workflow step, mentioned earlier. It is expected that developers will build custom classes that implement IWorkflowStepBase to help build workflow programs for their specific domains. For instance, workflow steps can be created to model banking industry operations such as check credit, update account, etc.

The only requirement for developing a workflow step is to implement the IWorkflowStepBase interface. This interface includes one method called Execute. The following class diagram depicts the relationship between the core classes.

 

In our XOML example above, workflow class consists of two steps executing in sequence (DoWork1 and DoWork2). Each step is of type WorkflowStep.  To keep things simple, we will simulate the work done inside each step with a sleep operation. The first step is simulated with a sleep of 11 seconds while the second step is simulated with a 1 second sleep duration.

 

Runtime Infrastructure

Next we need to implement a runtime infrastructure that is responsible for executing the workflow program. As described in the sections below, the runtime infrastructure needs to be able to provide services such as parsing the XOML file, scheduling the workflow steps and the ability to hydrate and dehydrate workflow instances.  

Parser

 We can parse the XOML (passed in as an argument ) using the XAML parser class that is part of .NET 3.0.  We can then load the parser output, an instance of the Workflow class, and add the contained WorkflowStep instances to a queue. As we will see shortly, scheduler will use this queue to execute steps inside the workflow.  

XmlTextReader reader = new XmlTextReader(args[1]);
Workflow wf = (Workflow)XamlReader.Load(reader);
foreach (IWorkflowStepBase step in wf.Steps)
{
     m_PendingSteps.Enqueue(step);
}

Note – We could replace the XAML based serialization format with another Domain Specific Language (DSL) format as long as the respective parser output can ultimately be  mapped to an instance of the Workflow class.

Scheduler

Scheduler dequeues the elements of the queue, populated in the previous step, and executes them as shown below. As noted earlier, to keep things simple our workflow framework only supports sequential workflow with a single branch.  This means that all that the scheduler needs to do is execute each IWorkflowStepBase instance in a sequence. Since no parallel branches are allowed, only one workflow step can be active at any give time.  To execute each workflow step, the scheduler invokes the IWorkflowStepBase.Execute method of the respective WorkflowStep instance. Even though each workflow instance can have only a single workflow  step executing at any given time, there can be more than one workflow instances executing concurrently. For scalability reasons  it important that scheduler thread not be blocked waiting for IWorkflowStepBase.Execute to complete. The obvious solution is to enable asynchronous behavior on the Execute method. This way a long running  WorkflowStep (waiting for days for an external stimulus to arrive, for instance) can return control back to the scheduler. The Execute method of our WorkflowStep class is a contrite example of how to take advantage of the asynchronous behavior. If the simulated timeout is greater than 10 seconds, it notifies the scheduler that it is in an idle state. WorkflowContext.WorkflowStepIdled method is responsible for sending the idled notification to the scheduler.

 

STEP_EXECUTION_STATUS IWorkflowStepBase.Execute(WorkflowContext context)
{
    
    System.Timers.Timer timer = new System.Timers.Timer();
    TimeSpan timeoutDuration = new TimeSpan(0,0,Int32.Parse(m_SimulatedTimeOut));
    timer.Interval = (double)timeoutDuration.TotalMilliseconds;
    

    if (timer.Interval > 10000)
    {
        WorkflowContext.WorkflowStepIdled();
        timer.AutoReset = false;
        timer.Elapsed += new System.Timers.ElapsedEventHandler
                    (delegate(object sender, ElapsedEventArgs e) { WorkflowContext.WorkflowStepComplete(); });
        timer.Start();
        return STEP_EXECUTION_STATUS.STEP_EXECUTING;
    }
    else
    {
         System.Threading.Thread.Sleep(Int32.Parse(timer.Interval.ToString()));
         WorkflowContext.WorkflowStepComplete();
         return STEP_EXECUTION_STATUS.STEP_COMPLETE;
      
    }
}

The scheduler in turn uses  the WaitHandle.WaitAny construct  to handle multiple aysnchronous notifications received from the executing workflow steps.

while (m_PendingSteps.Count > 0 )
{
    // Schedule the workflow step for execution
    ((IWorkflowStepBase)(m_PendingSteps.Dequeue())).Execute(new WorkflowContext());

    // Wait for notifications from workflow step(s)
    state = WaitHandle.WaitAny(WorkflowContext.g_waitHandles);

}

Persistence

Once a workflow step has been idled,  the parent workflow  can be deemed idle as well (remember that there can only be one executing workflow step inside a workflow). To conserve resources, it would be nice if we can remove the idled workflow instance from memory and hydrate it to a storage medium. Hydrated workflow instances can be brought back to life at an appropriate time in the future ( i.e. arrival of an external stimulus).  We need to add the hydrate/dehydrate capability to our workflow runtime. One approach to implementing this capability is to add a PeristenceService class (shown below). The PeristenceService class has two primary methods SaveWorkflowInstanceState and LoadWorkflowInstanceState.  SaveWorkflowInstanceState, as the name suggests,  persists the state of an in-memory Workflow instance to file. LoadWorkflowInstanceState method resurrects a Workflow class instance based on a previously persisted state. Persisting a Workflow instance requires that the workflow steps that constitute a workflow, be persisted as well. We can use the notion of serialization surrogates to persist workflow steps.  We can create a custom serialization surrogate for the IWorkflowStepBase  (called CustomStepSurrogate. Please see the code below). Next, we need to create an instance of the SurrogateSelector class and register it with PersistenceService. Once the surrogate is registered, all workflow steps that implement IWorkflowStepBase  will be inherit the persistence behavior. Refer to methods GetObjectData and SetObjectData of the CustomSurrogate for additional details.

    public class PersistenceService 
    {
       string FILE_NAME = @".\workflowstate.bin";

        public  PersistenceService ()
        {
            m_BinaryFormatter = new BinaryFormatter();
            m_SurrogateSelector = new SurrogateSelector();
            m_CustomStepSurrogate = new CustomStepSurrogate();
            m_SurrogateSelector.AddSurrogate(typeof(Queue<IWorkflowStepBase>), new StreamingContext(StreamingContextStates.All), m_CustomStepSurrogate);
            m_BinaryFormatter.SurrogateSelector = m_SurrogateSelector;
                
        }

        public void SaveWorkflowInstanceState(Queue<IWorkflowStepBase> PendingSteps)
        {
            m_BinaryFormatter.Serialize(m_StreamReader.BaseStream, PendingSteps);
        }
        public Queue<IWorkflowStepBase> LoadWorkflowInstanceState()
        {
            Queue<IWorkflowStepBase> PendingSteps = (Queue<IWorkflowStepBase>)m_BinaryFormatter.Deserialize(m_StreamReader.BaseStream);
            return PendingSteps;
           
        }
         ~PersistenceService()
        {
               m_StreamReader.Close();
       
        }

    }
internal sealed class CustomStepSurrogate : ISerializationSurrogate
{


// Methods
public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
{
    foreach (WorkflowStep step in (Queue<IWorkflowStepBase>)obj)
    {

        info.AddValue("SimulatedTimeOut", step.SimulatedTimeOut);
    }
    info.SetType(typeof(Queue<IWorkflowStepBase>));

}


public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector selector)
{
    Queue<IWorkflowStepBase> queue = new Queue<IWorkflowStepBase>();

    for (int count = 0; count < info.MemberCount ; count++)
    {
        WorkflowStep step = new WorkflowStep();
        step.SimulatedTimeOut = info.GetString("SimulatedTimeOut");
        queue.Enqueue(step);
    }


    return queue;
}

 

Note – Rather than bolting the persistence related code directly into our workflow runtime, we chose to abtract  it inside a seperate service (PersistenceService). This has two benefits: (i) Workflow runtime remains light weight (ii) It is possible for replace PersistenceService, that is provided with our rutime, with a custom service implementation.

 

In summary, we first defined a XAML serialization format that can be used  to model a workflow program. We established that a workflow program is comprised of a sequence of pre-fabricated chunks of logic, referred to as workflow steps. We then defined two custom classes Workflow and  IWorkflowStepBase that map to notion of a workflow program and workflow step respectively. Next we defined a workflow runtime responsible for execution of the workflow program. It provides services such as scheduling workflow steps in the appropriate order.  Finally, we added the ability to hydrate-dehydrate workflow instances using the PersistenceService.

In short, we have just engineered the Windows Workflow Foundation!

 

 Download the complete sample code for this post.

One Response to “Constructing WF”

  1. Emad Ibrahim Says:

    Good post… Very informative. I can’t wait to actually use workflow in my applications. Unfortunately, I haven’t had the need yet.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: