Windows Workflow Foundation (WF) is a powerful framework for building workflow-driven applications. But WF itself is just a set of libraries that are added to the .NET Framework. There is a WF runtime and a set of services, built-in activities, and the workflows and activities that you write, but those are all just classes and libraries that need to be hosted in some process to have a chance to run.

In my last WF article, Manage application processes with Windows Workflow Foundation, I gave a high level overview of WF and how to use it to create and run workflows. This article will focus in more depth on the hosting model of WF and shows you how get your workflows up and running in any given process. I'll discuss the way WF runs within your process, what some of the considerations are for using WF in different kinds of applications, and some of the hosting services you will need to invoke. I'll also briefly look at one of the most critical things to understand for integrating workflows into applications of any type - the host communications model.

Workflow Hosting Architecture

WF is not tied to any given type of application architecture. WF can be used in Windows Forms, ASP.NET Pages, Web Services, WCF services, Windows NT Services, console applications or any other kind of application you can build with .NET. Workflow and the infrastructure for running them provided by WF are all just code that lives in .NET assemblies. That code needs to be loaded into some executable process to run. The particular kind of executable process is irrelevant to WF.

WF just introduces some additional layers into your process between the code that you write - in the form of workflows and custom activities - and the Windows operating system that runs your code. Any given .NET application already has the layers of the .NET runtime and Framework class libraries running in process between your code and the underlying operating system. WF introduces its own runtime layer on top of that, as well as a set of services that provide things like persistence, tracking, scheduling, and host communications. On top of that runs your workflows and the activities within them (see Figure 1).


Figure 1: Workflow Host Layers

To get a workflow running in your application, you need a workflow host. That means that you need to construct an instance of the WorkflowRuntime class and set it running. You can get the runtime running one of two ways:

The latter approach is the more common that you will see in code samples. You need to make sure that only one WorkflowRuntime instance is created per AppDomain.

Windows Application Hosting

For a Windows Forms application or console application, you will typically do this by constructing the workflow runtime once in the Main method since this is the entry point for your process. For a Windows NT Service, a good place to do this is in the Start method that gets called whenever your service is started by the operating system or by the user.

If you create a WF Console application using either the Sequential Workflow Console Application or State Machine Workflow Console Application project templates in Visual Studio, you will get a main method that looks like the following:

static void Main(string[] args)
{
   using (WorkflowRuntime WorkflowRuntime = 
      new WorkflowRuntime())
   {
      AutoResetEvent waitHandle = new AutoResetEvent(false);
      WorkflowRuntime.WorkflowCompleted += 
         delegate(object sender, WorkflowCompletedEventArgs e) 
         { waitHandle.Set(); };
      WorkflowRuntime.WorkflowTerminated += 
         delegate(object sender, WorkflowTerminatedEventArgs e)
         {
            Console.WriteLine(e.Exception.Message);
            waitHandle.Set();
         };

      WorkflowInstance instance = 
         WorkflowRuntime.CreateWorkflow(
         typeof(WorkflowConsoleApplication2.Workflow1));
      instance.Start();

      waitHandle.WaitOne();
   }
}

Figure 2: Console Application Startup Code

The code in the Main method first constructs an instance of the WorkflowRuntime class inside a using statement. Because the WorkflowRuntime class is a disposable object, you should call Dispose on it before the instance variable goes out of scope, and the using statement takes care of that for you. The truth is that it is also a finalizable object; garbage collection will run as the process is shutting down and will finalize the object. Thus, you could get away with not calling Dispose in the case of this application. But it is just better coding practice to dispose of the object before it goes out of scope so that you do not forget to do so in other situations where it does matter.

Most of the code in the Main method is there to manage the process lifetime. When you start the workflow runtime, it is a non-blocking call, so the main thread will continue running. If you did not do something to block it, the Main method would complete, the process would shut down, and the workflow runtime and running workflows would all be destroyed. So you will need to do something appropriate to keep the process alive until your workflows have completed execution or you choose to shut down the application.

In the case of the code above, an AutoResetEvent is created and event handlers are hooked up for the WorkflowCompleted and WorkflowTerminated events. The event handlers are hooked up using anonymous method syntax, and you can see that the event handler method bodies mainly just call Set on the AutoResetEvent. The last thing the Main method does is to block on that event using the call to WaitOne. If you are not familiar with the AutoResetEvent synchronization object, the way it works is that any call to WaitOne blocks the calling thread until some other thread calls Set on the same object instance. In this case, the WorkflowCompleted and WorkflowTerminated events will fire on threads managed by the WorkflowRuntime - specifically the thread from the thread pool that the workflow was running on. So the main thread will be blocked until the workflow completes either normally (WorkflowCompleted event) or abnormally (WorkflowTerminated event).

The real work of the Main method in Figure 1 is to get the workflow running. You can see from the call to CreateWorkflow that you pass a Type object instance to the method that contains the type information for the workflow you want to run. Typically you will get this Type object from a call to the typeof operator since it is the workflow runtime's responsibility to create the actual instances for you. After the runtime has created the workflow instance, it gives you back a reference to a WorkflowInstance object, which is a wrapper around the instance context for the running workflow. After the WorkflowInstance is obtained through the CreateWorkflow call, Start is called on the WorkflowInstance, which starts the execution of the workflow. If the workflow is a sequential workflow, the first activity in the flow will get called. If the workflow is a state machine workflow, the state marked as the initial state is entered.

Workflow Execution Context

One thing to get used to in dealing with WF is that there are layers of abstraction between the code that you write in the hosting application and the code that you write in your workflows. It is up to the runtime to create workflow instances, and you never have direct access to the workflow instance reference yourself. Likewise the workflow runtime will create activity instances and execute them at the appropriate time within an activity execution context, and you usually won't have direct instance references in the workflow to the activity instances running within it (see Figure 3).


Figure 3: Workflow Execution Context

Another thing to be cognizant of when designing workflow applications is that your workflows are inherently multithreaded. When the workflow runtime starts a workflow running, it grabs a thread off the thread pool and uses that to execute the workflow. The particular thread that it uses is transparent to you, and in fact may change during the execution of the workflow if there are any blocking activities in the workflow such as a Delay or a HandleExternalEvent activity. The thread that the workflow runs on is definitely a different thread than the main thread of your application, so you need to synchronize access to any shared variables that both the host application and the workflow will have access to.

Web Application Hosting

If your workflow will be running in a web application (whether a page-oriented ASP.NET web site, ASP.NET web service, or a WCF IIS hosted service), then the entry point for your AppDomain is indeterminate since ASP.NET will create the AppDomain the first time an HTTP request comes in for one of the pages or services in the application. As a result, you need to put some code somewhere that will only be run once to initialize the workflow runtime. Also, that workflow runtime instance will need to be accessible to any part of your application that needs to create and start workflows or to access running workflows.

One of the easiest ways to handle this is to use a static helper class that caches the WorkflowRuntime instance after it is created. Figure 4 shows a helper class that works nicely in an ASP.NET context.

public static class WorkflowHelper
{
   private const string WFCACHEKEY = "WFRocks";
   private static object m_Lock = new object();

   public static WorkflowRuntime WorkflowRuntime
   {
      get
      {
         lock (m_Lock)
         {
            WorkflowRuntime WorkflowRuntime =
               HttpContext.Current.Cache[WFCACHEKEY]
                  as WorkflowRuntime;
            if (WorkflowRuntime == null)
            {
               // First access – create and cache it
               WorkflowRuntime = new WorkflowRuntime();
               // Create and add the scheduler service if running in ASP.NET
               ManualWorkflowSchedulerService sked = 
                  new ManualWorkflowSchedulerService();
               WorkflowRuntime.AddService(sked);
               HttpContext.Current.Cache[WFCACHEKEY] 
                  = WorkflowRuntime;
            }
            return WorkflowRuntime;
         }
      }
   }
}

Figure 4: WorkflowHelper Class

If you add the WorkflowHelper class to any ASP.NET web application, any time you need to access the WorkflowRuntime to create or access a workflow, you can just use the static WorkflowHelper.WorkflowRuntime property.

Workflow Services

In addition to providing an environment in which workflows can run, the WF runtime also hosts application services that allow the runtime to do its job. There are a number of built-in services that come with WF. These services allow the WF runtime to interact with resources outside of the execution context of the runtime. You can also design your own services and add them to the runtime to perform other kinds of custom extensibility of the runtime model.

Scheduler Service

The DefaultWorkflowSchedulerService is the built-in service that handles obtaining threads from the thread pool and using them to execute the workflow. This service is loaded by default whenever you start the WorkflowRuntime.

The scheduler service decides when a given activity is supposed to run and on what thread it should run. It also gets involved in workflow activation, because a workflow may not even be loaded into memory when it is time for a given activity to execute. WF supports unloading and persisting workflows when they are in a blocked state.

For example, you may have an activity in a workflow that says that the workflow should wait for input from a loan application processor, or until 30 days elapse, whichever comes first. In that case, it does not make sense to be consuming application memory for a workflow that may not do anything for 30 days. So the workflow can be suspended and unloaded from memory, either automatically or manually, and reactivated in the future when the input from the loan processor comes in or when 30 days elapses. The scheduler will take care of determining when the workflow needs to be reactivated and gets it running again.

There is another workflow scheduler service that ships with WF that you should use whenever you are hosting your workflows in an ASP.NET application. This scheduler is the ManualWorkflowSchedulerService and will need to be added to the runtime when you create it. Keep in mind that ASP.NET also uses threads from the thread pool to service requests for the application. The DefaultWorkflowSchedulerService can get into contention with the ASP.NET runtime in certain situations for threads in the thread pool, so the ManualWorkflowSchedulerService has been designed to make it safe to use WF in a web application. You can see the creation and loading of the ManualWorkflowSchedulerService in the code in Figure 4.

Persistence Service

As mentioned in the previous section, workflows can be "dehydrated" - meaning suspended and unloaded from memory. They can later by "re-hydrated," or loaded and reactivated. To do this, you need to have somewhere to put them when they are unloaded from memory. A workflow persistence service's purpose is to take care of that for you. There is a SqlWorkflowPersistenceService class that comes with WF that is designed to persist workflows out to SQL Server. To use it, you first have to construct an instance of the service and add it to the workflow runtime as was shown in Figure 4 for the scheduler service. The constructor for this class takes a connection string that tells it where to find the database that it is supposed to use for persistence. That database will obviously have to have a schema that the persistence service knows about. To create this database, you can use some SQL scripts that come with WF. These scripts are placed in the C:\WINDOWS\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL folder after installing WF on your machine. There are separate SQL scripts for creating the schema and the stored procedures (logic) that need to go into that database.

Tracking Service

When you have a workflow application up and running, you may have many instances of multiple workflow types up and running concurrently at any given point in time. You may want to know what workflows are running, and where they are in their execution at any given point in time. Because the workflow runtime and scheduler are responsible for controlling that, they can keep track of what is happening within their execution context for you. To do so, like persistence, they need somewhere to record what is happening in the runtime. And anytime the runtime needs to reach outside of its execution context, it does so through a service. The SqlTrackingService comes with WF and like the persistence service, allows you to write out all the execution tracking information to a SQL Server database. The scripts to create the appropriate database schema and stored procedures are located in the same directory mentioned in the previous section.

External Data Exchange Service

A critical form of interface outside the runtime execution context is to communicate with the host application process, and more specifically with your code that is running within it. Figure 3 depicted the runtime as a barrier that surrounds the objects running inside of it and isolating them from your code to a large degree. In order to bridge that barrier, a model was designed into the runtime based on a service called the ExternalDataExchangeService. This service allows you to make calls into a running workflow instance using events, and to call out from a running workflow using method calls. The next section describes what is needed to use this model.

Host Communications

The host communications model of WF revolves around the ExternalDataExchangeService and a related set of classes in the Framework. The way that this model is designed, communications into the workflow happen in the form of events that are fired by the host process. Notifications from the workflow itself to the host process are done in the form of method calls from the workflow into objects in the host. The contracts for what these events and methods look like are defined through interfaces.

Using this model, to set up a simple call from the host process code into a running workflow is not exactly simple and requires you to perform the following steps:

If you need to call out from the workflow to the host, the process is a little simpler:

For firing events into the workflow or calling methods on the host from the workflow, you also must add the ExternalDataExchangeService to the runtime before executing the workflow.

Loan Application Processing Sample

To demonstrate the steps described in the previous section, this article includes a sample workflow application implemented as a Web site that processes loan applications. The workflow is a state machine workflow defined in a separate class library referenced from the web application, which is typically how you would integrate a workflow directly into a web application. When loan applications are submitted, they start a workflow instance per loan application. The workflow transitions through the states of the workflow (AppSubmitted, AppAccepted, LoanApproved or LoanDenied, ApplicationCompleted) based on a number of events in the workflow (See Figure 5).


Figure 5: Loan Processing State Machine Workflow

The first event is just an automatic one - as soon as the application is submitted it is set to the Accepted LoanStatus and passes into the AppAccepted state. This is done in the sample by using a Delay activity with the delay duration set to zero in the event handler for the AppSubmitted state. Typically in a real application a number of automated checks would be done on the submitted application at this point. The LoanStatus gets transmitted to the host application by calling a CallExternalMethod activity in the StateFinalization activity that gets called as the state is transitioning out of the AppSubmitted state and into the AppAccepted state (see Figure 6).


Figure 6: Calling a Host Method with a CallExternalMethod Activity

Once the application workflow is sitting in the AppAccepted state, it waits for one of two events to occur. The primary event is that a UpdateLoanStatus event is fired from the host application. This represents a Loan Processor logging on to the application, reviewing the pending loan application, and either approving or denying the application. When the UpdateLoanStatus event fires, the workflow transitions to the appropriate state - either LoanApproved or LoanDenied based on the event handler in the AppAccepted state (see Figure 7).


Figure 7: UpdateLoanStatus Handling

Once the processing flows into either the LoanApproved or LoanDenied states, a StateInitialization activity is called that makes another call out to the host application to notify of the changed loan status. This call is made with another CallExternalMethod activity like the one shown in Figure 6. After the notification to the host in the StateInitialization activity, the event handler in either the LoanApproved or LoanDenied state contains another zero wait delay that is followed by a SetState that transitions to the ApplicationCompleted state.

To hook up all those CallExternalMethod and HandleExternalEvent activities, some interfaces and other code was required first as mentioned in the previous section. The first is an interface that defines the calls from the workflow into the host. This interface looks like the following:

[ExternalDataExchange]
public interface INotifyLoanStatusChanged
{
   void LoanStatusChanged(Guid loanId, LoanStatus status);
}

Figure 8: External Method Interface

Note the ExternalDataExchange attribute to signal the workflow designer and runtime that this interface is intended for communications between the host and workflow. In this case, you know it is only for communications from the workflow into the host because there are only methods (in fact just one) defined.

For the calls into the workflow from the host, we first need an event argument type, then an interface that uses that type to define an event. Figure 9 shows these type definitions.

[Serializable]
public class LoanProcessingEventArgs : 
   ExternalDataEventArgs
{
   private LoanStatus m_Status = LoanStatus.Unknown;
   public LoanProcessingEventArgs(Guid instanceId) : 
      base(instanceId) { }

   public LoanProcessingEventArgs(
      Guid instanceId, LoanStatus status)
      : base(instanceId)
   {
      m_Status = status;
   }

   public LoanStatus Status
   {
      get
      {
         return m_Status;
      }
   }
}

[Serializable]
public enum LoanStatus
{
   Unknown,
   Accepted,
   Approved,
   Denied
}

[ExternalDataExchange]
public interface ILoanProcessing
{
   event EventHandler<LoanProcessingEventArgs> UpdateLoanStatus;
}


Figure 9: Host-to-Workflow Communications Types

Note that the event argument type (and any type it contains) must be serializable. This is a requirement for working with workflows because of the fact that workflows can be serialized and persisted at any point, so their contained state must also be serializable. The LoanProcessingEventArg type derives from the ExternalDataEventArgs as is always required for events fired from the host into the workflow, and adds a LoanStatus enumeration member to hold the desired status being passed into the workflow. This acts as a command to the workflow from the host to set that state in this sample application. The event must be defined on an ExternalDataExchange interface and be defined using the EventHandler<T> generic delegate type.

Note that I chose to separate the incoming and outgoing calls onto separate interfaces. This is not required, and in fact many of the samples for WF do not do this, they combine them on a single interface. This is a design decision that you will need to make. I prefer to keep the interfaces separate to make it very clear which interface is intended for communications in which direction.

To facilitate hooking up the external communications activities, some properties are defined on the workflow class to hold the state associated with the parameters to the communications calls. These properties are shown in Figure 10.

private LoanProcessingEventArgs m_LoanProcessingResults;
private LoanStatus m_LoanStatus = LoanStatus.Unknown;

public LoanStatus LoanAppStatus
{
   get
   {
      return m_LoanStatus;
   }
   set
   {
      m_LoanStatus = value;
   }
}

public Guid LoanAppId
{
   get
   {
      return WorkflowInstanceId;
   }
}

public LoanProcessingEventArgs LoanProcessingResults
{
   get
   {
      return m_LoanProcessingResults;
   }
   set
   {
      m_LoanProcessingResults = value;
      m_LoanStatus = value.Status;
   }
}

Figure 10: Workflow Properties to Hold External Communication State

Once all these code artifacts are defined, you are finally ready to hook up the CallExternalMethod and HandleExternalEvent activities in the designer. I'll step you through the process for the first CallExternalMethod activity in the AppSubmitted StateFinalization activity.

To hook this up, you select the CallExternalMethod activity in the designer and go to the Properties window. When you first drag and drop a CallExternalMethod activity onto your workflow, its properties look like those shown in Figure 11.


Figure 11: CallExternalMethod Properties Before Setting

The first thing you need to do is select the InterfaceType property and select the desired interface from the type selection dialog shown in Figure 12. This dialog will only show you interfaces that are marked with the ExternalDataExchange attribute, so if you don't see your interface here, that is the first thing to check.


Figure 12: Type Selection Dialog

After selecting a type, you need to select the method on that interface that will be called. The MethodName property in the Properties window will give you a drop down list of all methods defined on the interface to select from. Once you have selected the method, the Properties window property list will update to include the parameters for the method by name, and the return type if appropriate. In the case of the INotifyLoanStatusChanged.LoanStatusChanged method, the loanId and status method parameters show up with little blue i icons (see Figure 13). These icons indicate that you can bind this property to some other value in the workflow - either a property exposed on the workflow itself of the appropriate type, or one exposed on one of the other child activities in the workflow. This is why we defined the LoanAppId and LoanAppStatus properties in Figure 10.


Figure 13: Method Parameter Properties Added to Properties Window

To bind those properties, just double click on the little i icon and you will get a binding dialog like the one shown in Figure 14. You can select the desired property on the workflow or drill down to a property on one of the other activities or complex types exposed in the dialog to select them as the source value for the method parameter when the method gets invoked. You will need to do the same kind of thing for the status parameter.


Figure 14: Binding a Method Parameter to a Workflow Property

The same kind of a process is followed for hooking up a HandleExternalEvent activity. You select the InterfaceType, then select the EventName from a drop down list in the Properties window. The event argument then shows up as a property in the Properties window, and you can bind it to another workflow property that will accept the incoming value when the event is fired. For example, the LoanProcessingResults property in Figure 10 receives the event argument passed in when the UpdateLoanStatus event fires from the host.

The final piece of goo you have to put in place to make the events fire and to have something to answer the method calls from the workflow is to create a class that implements the interfaces in the host. For the sample application, I went ahead and implemented both the incoming and outgoing interfaces on the LoanManager class. That class is shown in Figure 15. The LoanManager is a singleton class that exposes the singleton instance through the Default property, similar to the Settings class for user and application configuration settings in Visual Studio 2005.

public class LoanManager : ILoanProcessing, INotifyLoanStatusChanged
{
   private static LoanManager m_Instance = new LoanManager();
   private static List<LoanApplication> m_Loans = 
   new List<LoanApplication>();

   public static LoanManager Default
   {
      get
      {
         return m_Instance;
      }
   }

   public static List<LoanApplication> Loans
   {
      get
      {
         return m_Loans;
      }
   }

   private LoanManager()
   {
   }

   public void ApproveLoan(Guid loanId)
   {
      Debug.WriteLine("ApproveLoan called");
      SetLoanStatus(loanId, LoanStatus.Approved);
      if (UpdateLoanStatus != null)
      {
         UpdateLoanStatus(null, 
            new LoanProcessingEventArgs(loanId, LoanStatus.Approved));
      }
   }

   public void DenyLoan(Guid loanId)
   {
      Debug.WriteLine("DenyLoan called");
      SetLoanStatus(loanId, LoanStatus.Denied);
      if (UpdateLoanStatus != null)
      {
         UpdateLoanStatus(null, 
            new LoanProcessingEventArgs(loanId, LoanStatus.Denied));
      }
   }

   private void SetLoanStatus(Guid loanId, LoanStatus status)
   {
      LoanApplication app = FindLoan(loanId);
      app.Status = status;
   }

   private LoanApplication FindLoan(Guid loanId)
   {
      lock (m_Loans)
      {
         Predicate<LoanApplication> match = 
            delegate(LoanApplication app)
            {
               if (app.LoanId == loanId)
                  return true;
               return false;
            };
         LoanApplication theApp = m_Loans.Find(match);
         return theApp;
      }
   }

   public event EventHandler<LoanProcessingEventArgs> UpdateLoanStatus;

   void INotifyLoanStatusChanged.LoanStatusChanged(Guid loanId, 
      LoanStatus status)
   {
      Debug.WriteLine(string.Format(
          "Loan {0} processed, status {1}",loanId,status));
      LoanApplication app = FindLoan(loanId);
      if (app != null)
      {
         app.Status = status;
      }
      else
      {
         lock (m_Loans)
         {
            m_Loans.Add(new LoanApplication(loanId, status));
         }
      }
   }
}

Figure 15: LoanManager Communications Handling Class

The LoanManager class in Figure 15 manages a list of loan applications and their state for the host application. The code is pretty straightforward - submitted loan applications are added to a list and the events are fired into the workflow from the class. When calls come back out from the workflow, they are handled by the class.

The LoanManager is effectively maintaining some parallel state with what is contained in the workflow itself. Each workflow instance represents a loan application in progress, and contains the state of that application. That state could be obtained directly through the workflow object model, but the code required for doing that will have to be the subject of another article. This one has gone on long enough! In this case, I just went with the parallel state for simplicity. The state is manipulated by the hosting web application, which you can check out in the download code.

Wrapping Up

I've covered quite a bit of ground in this article. You got a sense of what the hosting model is in Windows Workflow Foundation, what workflow services are available in that host environment, and then you saw a fairly deep example of using the ExternalDataExchangeService and its related host communications model to set up two way communications between a workflow and its host.

Unfortunately, some simple things in WF are not so simple. Host communications is one of those. Part of the reason for the complexity has to do with the complex hosting environment itself. If a workflow can be dehydrated and persisted out to a database at any given point in time, you can't just hold object references in memory to a workflow to communicate with it the way you would with other objects in a program. Even though there are a lot of steps involved, the design is consistent between calling out and calling into a workflow, and becomes routine after you have done it a few times. Perhaps in a future release we will get an even better designer experience, such as a wizard that steps us through all those steps. But for now you just need to get used to the process described in the previous section, because you will use it a lot if you design WF workflows.

Open this zip file for the complete collection of code samples for this article.