Foundations

Loading Workflow Models in WF

Matt Milner

Code download available at:Foundations2008_05.exe(161 KB)

Contents

CLR Types for Models
XAML for Workflows
Workflow Loading
Comparing Modeling Methods
Conclusion

Workflows in Windows® Workflow Foundation (WF) can be modeled in several ways. Developers model a hierarchy of activities that defines the control flow and program statements to be executed in the workflow. (For more on the declarative nature of Windows WF, see "WinFX Workflow: Simplify Development with the Declarative Model of Windows Workflow Foundation," by Don Box and Dharma Shukla, at msdn2.microsoft.com/magazine/cc163661.aspx.)

The workflow model or definition is the artifact that tools will create and modify and provides the template for runtime instances of workflows. The flexibility to model workflows in different ways is a powerful feature and allows for modeling tools that target different audiences. But what format should you use to model your workflows? The answer, as usual, is not simple. In this column I will explain how two common models are applied and how the runtime handles them. In addition, I will discuss how the runtime creates new instances of workflows for both Microsoft® .NET Framework types and eXtensible Application Markup Language (XAML), and how you can customize that process to suit your needs.

CLR Types for Models

One way to model workflows is in .NET-targeted code. You create a class that will represent the workflow, and you build the hierarchy of activities in the constructor for that class. Typically, the class you write will derive from some composite activity, such as SequenceActivity, and then in the constructor you add activities as children. Figure 1 shows a workflow model using SequenceActivity as the base class and a simple WriteLine activity as the children.

Figure 1 Defining a Workflow in C#

public class WorkflowInCode : SequenceActivity
{
    public WorkflowInCode()
    {
        WriteLineActivity writeLine1 = new WriteLineActivity();
        writeLine1.Name = "writeLine1";
        writeLine1.OutputText = "Hello";

        WriteLineActivity writeLine2 = new WriteLineActivity();
        writeLine2.Name = "writeLine2";
        writeLine2.OutputText = "MSDN Reader";

        this.Activities.Add(writeLine1);
        this.Activities.Add(writeLine2);

    }
}

This model's mechanics and tool use are similar to the way Windows Forms are designed. A visual designer is used to model the workflow hierarchy, dragging and dropping activities into the model; behind the scenes the designer generates code to recreate the structure. This is one of the primary models supported by the Visual Studio® tools for Windows WF. The constructor for the workflow calls the InitializeComponent method, which is located in the designer file generated by the visual design surface.

Figure 1 models a very simple activity hierarchy; the class I created does not have any code outside of the hierarchy creation. It is also possible to use this .NET modeling mechanism to model activity binding, event handlers, and more complex interactions between activities. For example, if the workflow contained a ReadLine activity and a WriteLine activity, I could bind properties between them and also create event handlers for events raised by the activities. Figure 2 shows an updated workflow definition that includes these changes.

Figure 2 More Complex Workflow Code Model

public class WorkflowInCode : SequenceActivity
{
  public WorkflowInCode()
  {
    ReadLineActivity readLine1 = new ReadLineActivity();
    readLine1.Name = "readLine1";
    readLine1.Invoked += new EventHandler(readLine1_Invoked);

    WriteLineActivity writeLine1 = new WriteLineActivity();
    ActivityBind textBind = new ActivityBind("readLine1", "InputText");

    writeLine1.Name = "writeLine1";
    writeLine1.SetBinding(WriteLineActivity.OutputTextProperty,
       textBind);

    this.Activities.Add(readLine1);
    this.Activities.Add(writeLine1);
  }

  void readLine1_Invoked(object sender, EventArgs e)
  {
    ReadLineActivity readLine = sender as ReadLineActivity;
    Console.WriteLine("Event handler in workflow read \"{0}\" 
       from readline.", 
      readLine.InputText);
  }
}

In the code shown in Figure 2, the workflow class has code that represents part of the workflow but is not hierarchy related. When I compile the workflow definition, the event handler for the Invoked event on the ReadLine activity will also be included in the assembly.

When modeling workflows in code, you compile the model into a .NET assembly and instantiate a CLR class at run time to create the workflow hierarchy. When you want to start a workflow modeled in this way, you request the runtime to create an instance for you based on the class, as shown here:

WorkflowRuntime runtime = new WorkflowRuntime();
WorkflowInstance instance = runtime.CreateWorkflow(typeof(WorkflowInCode));
instance.Start();

Because I'm compiling all of this information into CLR classes, when the type is loaded, the runtime can easily resolve references to event handlers and other code that is part of the workflow definition. Later I'll go into how these types are actually loaded and the implications of using this modeling technique as opposed to XML.

XAML for Workflows

A more compelling way to model workflows is using XAML, an XML dialect for representing hierarchies of CLR objects. XAML is most often associated with Windows Presentation Foundation (WPF), where it is used to represent hierarchies of visual objects drawn on the screen: windows, buttons, menus, and so on. In Windows WF, XAML represents a hierarchy of activities that form a workflow. The following XML describes the simple workflow that I previously modeled in code:

<SequenceActivity xmlns="http://schemas.microsoft.com/winfx/2006
                         /xaml/workflow"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:msdn="clr-namespace:MSDN.CustomActivities;Assembly=
              CustomActivities">
  <msdn:WriteLineActivity x:Name="writeLine1" OutputText="Hello" />
  <msdn:WriteLineActivity x:Name="writeLine2" OutputText="MSDN Reader" />
</SequenceActivity>

The first thing to notice about this XAML is that because I am defining a structure of CLR types in XML, I need to provide a bit of mapping between the two worlds. It is necessary that the WorkflowRuntime be able to reconcile my custom WriteLine activity, which is represented in XAML, to a .NET type when building the hierarchy.

In the previous XML, an XML namespace declaration maps the msdn prefix to the MSDN.CustomActivities CLR namespace in the CustomActivities assembly. When the XAML is processed and deserialized to create the object hierarchy, this information will be used to resolve the .NET type. The attributes on the XAML element translate into the property values on the .NET object when it is created, thus fully initializing the object.

Via XAML, I also have the ability to manage activity binding, hook up event handlers, and do more complex modeling of relationships or interactions. Using something called Markup Extensions, I can bind properties or event handlers. The following code represents the workflow containing both ReadLine and WriteLine activities with a binding between them:

<SequenceActivity xmlns="http://schemas.microsoft.com/winfx/2006/
                         xaml/workflow" 
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:msdn="clr-namespace:MSDN.CustomActivities;Assembly=
              CustomActivities">
  <msdn:ReadLineActivity x:Name="readLine1" InputText="" />
  <msdn:WriteLineActivity x:Name="writeLine2" 
    OutputText="{ActivityBind readLine1 Path=InputText}" />
</SequenceActivity>

The OutputText property of the WriteLine activity has an ActivityBind extension. This turns the supplied text into an ActivityBind class instance when the workflow is deserialized into objects.

When defining workflows in XAML, you can either translate the XML to a CLR activity hierarchy at compilation time or at run time. If you choose to compile a XAML workflow definition, you need to use the WorkflowCompiler class or the wfc.exe command-line compiler, either of which will allow you to generate a .NET assembly containing the workflow definition in a CLR class. You also need to add the Class attribute to the root activity to indicate the class to be created when the workflow is compiled.

The result of compiling a XAML definition is the same as if you had created the definition in code: a .NET type that can be used to create workflows. The real difference is in how tools create the model and how the model is used at run time. I will talk about the trade-offs in both cases shortly.

If you build the hierarchy at run time, you load the XAML into an XmlReader and then ask the WorkflowRuntime to create a WorkflowInstance from the XAML definition. Earlier, I created the instance by its CLR type using the CreateWorkflow method on the WorkflowRuntime class. This method has several overloaded versions, including one that accepts an XmlReader containing the workflow definition. The code below shows this simple but powerful change, which loads the definition directly from a XAML file:

WorkflowInstance instance = runtime.CreateWorkflow(
XmlTextReader.Create("..\\..\\WorkflowInXML.xml"));
instance.Start();

It is CreateWorkflow that translates the model or definition into an instance that can be used by the runtime. I will discuss the details of this transition shortly, but first I want to elaborate on some more details of creating workflow models in XAML.

Looking at the sample XAML workflow definition, you will notice that there is no mapping for the root SequenceActivity element to the System.Workflow.Activities CLR namespace. This activity is included as part of the .NET Framework 3.0 and is used here as the root activity in the hierarchy.

One of the benefits of this sort of model is that the model itself does not have to be tied to a single interpretation or implementation of a type. Because I have included CLR type information in this XAML model for the WriteLine activity via the namespace mapping, I'm tied to a specific implementation of the activity class. Further, if I have different versions of the activity, I'm tied to a specific version of that activity for the workflow model by the strong name used to reference the assembly. What I need is an abstraction to decouple the model from the CLR type information but still allow me to deserialize this markup into an activity hierarchy.

This decoupling is done using a mapping layer between XML namespaces and CLR namespaces. Windows WF allows the hosting code to define the assemblies used to fulfill the mapping. In the previous example, the SequenceActivity element in the XAML is in the default XML namespace. The default namespace is called out as "http://schemas.microsoft.com/winfx/2006/xaml/workflow" in the first line of the XAML.

The other side of the equation is somehow mapping this XML namespace to a CLR namespace, and that is accomplished by adding custom attributes into the assembly where the activities are defined. The System.Workflow.Activities.dll assembly contains an attribute that maps the XML namespace seen in the XAML to the CLR namespace System.Workflow.Activities. I can do the same with my custom activities. I add the following attribute to the assemblyinfo.cs file in the activities project:

using System.Workflow.ComponentModel.Serialization;
[assembly:XmlnsDefinition("http://msdnmagazine/foundations/workflow", 
  "MSDN.CustomActivities")]

Then I change the XML namespace in my XAML file to match this attribute, as shown here:

xmlns:msdn="http://msdnmagazine/foundations/workflow"

At this point, I have the XML namespace neatly in my XAML file with no direct ties to the CLR assembly or namespace. I also have the mapping from XML to CLR namespaces defined via the CLR attribute on my assembly. When the workflow runtime attempts to deserialize the XAML, it needs to know which assemblies to inspect for the custom attribute. It uses the TypeProvider class to do so; this class implements the ITypeProvider interface and is a container for assemblies that need to be available when types are resolved. In my example, I create an instance of the TypeProvider class in my hosting application, add my activity assembly to the TypeProvider, and then add the TypeProvider to the WorkflowRuntime as a service:

using (WorkflowRuntime runtime  = new WorkflowRuntime())
{
  TypeProvider provider = new TypeProvider(runtime);
  provider.AddAssemblyReference("CustomActivities.dll");
  runtime.AddService(provider);

  //start workflow
  ...
}

I create a model in XML and then defer the mapping to CLR types until run time by configuring the TypeProvider with the assemblies I want for my host. This gives me a flexible model and control over the CLR activity types used at run time to create the instance. When the workflow is deserialized, the WorkflowMarkupSerializer class is used, which uses the TypeProvider to resolve XML names to CLR types. Figure 3 shows the interaction between the loader (described next), the WorkflowMarkupSerializer, and the TypeProvider.

Figure 3 XAML Workflow Creation

Figure 3** XAML Workflow Creation **

Workflow Loading

Once a workflow definition has been created, either as CLR classes or as XAML markup, the runtime needs to be able to take that definition and create a WorkflowInstance. Many developers assume that WorkflowRuntime is responsible for loading the workflow instance because that is the class they use to create the instance. But WorkflowRuntime uses a runtime service to do the actual loading of the workflow. A base class, WorkflowLoaderService, defines the abstract methods that a workflow loader service must implement. The definition of this class is shown here:

public abstract class WorkflowLoaderService
{
  protected internal abstract Activity CreateInstance(Type workflowType);
  protected internal abstract Activity CreateInstance(XmlReader 
    workflowDefinitionReader, XmlReader rulesReader);
}

As you can see, WorkflowLoaderService takes either a CLR Type or an XmlReader containing the workflow definition and creates an Activity hierarchy from it. The runtime also contains a DefaultWorkflowLoaderService that it uses if no other loader service is added to the runtime. In this default loader, when a workflow is to be created from type information, the service simply calls Activator.CreateInstance on the type and returns that object.

When DefaultWorkflowLoaderService is asked to create a workflow from an XmlReader, it attempts to deserialize the XAML into an activity hierarchy using the WorkflowMarkupSerializer class. Any validation errors are captured and an exception is thrown if the deserialization fails.

The default loader works in many situations, especially when using XAML or .NET workflow definitions, but there may be times when you want to create a custom workflow loader to augment or replace the default behavior. When creating a custom loader, you can either derive from the WorkflowLoaderService abstract class or DefaultWorkflowLoaderService, depending on how much functionality you want to reuse.

Many developers want to store their XAML workflows in a database. This is accomplished by updating the authoring or deployment tools to store the XAML in a database table. Once the XAML is stored, you could load the XAML into an XmlReader in the host application and then call CreateWorkflow, but a custom loader service centralizes that logic and makes it reusable across hosts.

There are some constraints when creating a custom workflow loader service. First, because both the CreateWorkflow method and the WorkflowLoaderService only have methods for creating workflows from a CLR Type or an XmlReader, the custom loader must be able to get the information it needs from either a CLR type or an XML file. For security reasons, the runtime validates that the loader service does not create a workflow that is a different type than specified. Suppose I defined an interface, and then wanted to implement logic such that when a type implementing that interface was passed to the loader, I would call a method on that interface to get the workflow type. This would allow for more dynamic code-driven workflows. But this would not work, as the type passed to the CreateWorkflow call would not match the type of the returned workflow.

This leaves the XmlReader route, and here I have a lot more flexibility. Since the runtime doesn't know anything about the workflow type itself until after the loader creates it, the workflow loader has the ability to interpret the XML as it sees fit and return a root activity representing the workflow. What I generally do is create an XML file format that includes all of the information I need to create a workflow. As an example, I will show you how to create a custom loader service that loads workflows from a SQL Server® database. The information about the workflow will be stored in a simple XML file that can be wrapped in an XmlReader and passed to the CreateWorkflow method. Figure 4 shows the interactions among the runtime, my loader service, the database, and the WorkflowMarkupSerializer.

Figure 4 Custom Workflow Loading from a Database

Figure 4** Custom Workflow Loading from a Database **

The first decision was how I would store my workflows in the database. One obvious way to store the model is as a XAML document in a column. In that case, the first question is whether I store the XAML as one of the SQL text types or as an XML type in SQL Server 2005. If I wanted to be able to query or update the XAML in the database, I would use the XML type, but for this example, I just want a place to store the XAML, so I used the ntext data type. I also added some metadata columns and an ntext column to store the serialized representation of the rules for my workflow. Most workflows will contain some rules, if only to control composite activity execution like the IfElseActivity, so a custom loader solution should also include rule management capabilities.

I chose to store the entire workflow model in the database as a XAML file, but there are many alternatives. One simple example would be to store workflow fragments in the database as XAML and then have the complete workflow definition consist of a collection of those fragments. This would create reusable workflow logic in XML, unlike creating custom composite activities, which are compiled into binary form. Another idea would be to store the workflow definition as relational data. For example, I might have a table that consists of individual steps in a workflow, which defines the basic activities, and then have a workflow table that would link these activities together.

The storage model for the workflow depends on your requirements for managing the model and its various parts. When you create a custom loader, you simply need to be able to correctly combine that data to create the activity hierarchy that represents the modeled workflow.

The next step is to determine the format of the XML document that references the workflow from the database. I chose to select workflows based on a name and version number, and I created a very simple XML model. The sample XML document that follows is what I load into an XmlReader and then pass to the CreateWorkflow call:

<Workflow xmlns="http://msdnmagazine/foundations/workflow"  
  Name="HelloWorld" Version="1.0" />

You might choose a variety of identifiers to select the correct workflow, but I find that using a common name and version number can make it easier to select or identify workflows.

I wrote my custom loader service to consume the XML document and return the workflow definition from the database. I had two design considerations: to support CLR-based workflow models and to support someone passing XAML directly to the CreateWorkflow method instead of using my custom XML format. Since the WorkflowRuntime can only use one WorkflowLoaderService, my loader is derived from the DefaultWorkflowLoaderService. It depends on functionality in the base to support the existing models as well as my own database model.

I define a connection string property on my service and provide constructors that allow this value to be supplied when the service is created in code or in configuration. This eliminates the need to have connection information in the XML files.

For the CreateInstance method that takes a CLR type as a parameter, I simply rely on the base class implementation to return the instance. To overload the CreateInstance method that takes an XmlReader, I first check to see if the XML matches my custom format and extract the name and version of the workflow to look up.

Once I have a valid workflow name, I call a stored procedure in the database to return the XAML and serialized rules for my workflow. Once I have the XAML, I call down to the base class to deserialize the XAML into an Activity hierarchy and return the result. In the case where the XML does not meet my format, I simply pass a clone of the XmlReader to the base class and let the default loading behavior and validation take over. Figure 5 shows the CreateInstance method that handles the XmlReader logic.

Figure 5 Custom CreateInstance

protected overrideActivity CreateInstance(XmlReader workflowDefinitionReader, 
  XmlReader rulesReader)
{
  string workflowName = default(string);
  string workflowVersion = default(string);
  XmlReader dbWfDefinitionReader = null;
  XmlReader dbWfRulesReader = null;

  //get the workflow name and version from the xml
  workflowDefinitionReader.MoveToContent();

  //if we have the correct root node, read the attributes
  if (String.Compare(
    workflowDefinitionReader.LocalName, "Workflow", true, 
    CultureInfo.InvariantCulture) == 0 && 
    String.Compare(workflowDefinitionReader.NamespaceURI, 
    "http://msdnmagazine/foundations/workflow", true, 
    CultureInfo.InvariantCulture) == 0)
  {
    workflowName = workflowDefinitionReader.GetAttribute("Name");
    workflowVersion = workflowDefinitionReader.GetAttribute("Version");

    if (String.IsNullOrEmpty(workflowName))
      throw new ArgumentNullException("WorkflowName");
  }
  else
  {
    //try to load the workflow as normal
    XmlReader wfclone = workflowDefinitionReader.ReadSubtree();
    return base.CreateInstance(wfclone, rulesReader);
  }

  //get the workflow definition and rules from the database
  GetWorkflowFromDB(workflowName, workflowVersion,
    ref dbWfDefinitionReader, ref dbWfRulesReader);

  //use the base class to create the workflow instance 
  if(dbWfDefinitionReader != null)
    return base.CreateInstance(dbWfDefinitionReader, dbWfRulesReader);
  else
    throw new ArgumentException(
      "No workflow found with that name and version", "WorkflowName");
}

Comparing Modeling Methods

Now let's compare modeling types. There are three broad dimensions of comparison: tooling, versioning, and deployment. Tooling actually comes in two flavors: the tools you get from Microsoft and the tools you might create on your own. The project and item templates in the Microsoft-provided extensions to Visual Studio allow you to create either CLR-based workflows in Visual Basic® or C#, or XAML-based workflows that are compiled into assemblies. While there are no templates included for XAML workflows that are not compiled, you can find some third-party templates, including some that I have posted on my blog at pluralsight.com/blogs/matt. With these tools and Visual Studio, a developer can create workflows in the two predominant formats, manage them with source control, and end up with a compiled assembly or XAML files. For a custom loader solution such as the one described here, you would still have to create a mechanism for loading workflow definitions into the database outside of the tools.

When creating custom tools for authoring workflows, the approach with the lowest barrier to entry is using the workflow designer control included in the .NET Framework 3.0 runtime components and storing the definition as XAML with no codebehind file. A development team could quickly get a custom designer up and running with the XAML being stored in SQL Server. Team members could open the workflow model directly from the database, work on it, and save the changes back to the database when finished.

From a tooling perspective, the XAML model is the easiest way to manage the workflow model, as it requires a single XML file, with no worries about .NET code files and compilation. Since XAML is supported in Visual Studio, developers can use the visual designer to create and edit workflows that are not going to be compiled.

Managing workflows after they are developed is another consideration. If you model your workflows as CLR types, then you will be managing .NET assemblies for deployment; if you use XAML, you will be managing XML files and .NET assemblies. Remember, most workflows will use custom activities, which will have to be compiled and deployed to be usable by the workflows. XML files are more easily edited, scanned, differenced, and otherwise processed than assemblies. Because they are text files, their contents are more transparent.

Finally, but perhaps most importantly, how you version your workflows depends heavily on the model you choose. If you want to be able to have two different versions of a workflow model in use at the same time, you have different considerations when using CLR types or XAML as your model.

When you use CLR types to model your workflow, your versioning is tied to the CLR versioning system. This means your workflow assemblies must be signed with a strong name and you must increment the version number to indicate that you are creating a new version of the assembly. When your host application creates a workflow, it will be referencing a particular version of the CLR type that models the workflow.

Running instances of your workflow that were created from an earlier version of the model must be able to find that same type information so that they are able to resume. This means that you have to keep all versions of your .NET assembly around until all workflows are finished executing. In addition, if you use tools to visualize workflows or otherwise need to reconstruct the activity graph, you will need that .NET type information around. If you want to look at historical analyses of a workflow, you might have to keep that .NET assembly around for a long time.

When you have XAML-based workflows, the potential version dependencies are from the XML to the CLR types for the activities. If you do not use the namespace mapping discussed earlier, your XAML will be version-dependent and must be able to find that version when it is created. If you have mapped the namespaces, then the XAML is not bound to a specific version or even a specific implementation. The host environment, through the use of the TypeProvider, can provide the appropriate activity assemblies to be used by the runtime when resolving references from XAML to CLR. If you are building XAML workflows, I suggest you create and use the namespace mapping to simplify your versioning story.

Conclusion

Workflow models and their transformation into workflow instances is extremely flexible in Windows WF. When building workflows for your environment, it is important that you understand the choices and implications of choosing a particular model. Consider not only the developer experience and tooling, but also long-term maintenance and subsequent versions of your workflows.

Send your questions and comments to mmnet30@microsoft.com.

Matt Milner is an independent software consultant specializing in Microsoft technologies including .NET, Windows WF, WCF, and BizTalk Server. As an instructor for Pluralsight, Matt authors courses on Workflow and BizTalk Server, and teaches WCF. Matt lives in Minnesota with his wife, Kristen, and his two sons.