Introduction
Long-running workflows are workflows that need some time to complete. For example,
an ordering system may include approving the order and waiting for the order
to be shipped to the customer, both of which may take some time. A manager might
only approve the order the next day. The order may have been received by the
customer a few days later. If the machine that hosts the workflow application
is accidentally turned off and the workflow was not persisted, then the current
state of the workflow will be lost. Also, a system’s performance will suffer
if there are currently many workflows loaded in memory. Some workflows should
be unloaded when they become idle.
To demonstrate how to persist a workflow, I’ll be reusing an application from my
previous article entitled WPF and the Model View View Model pattern. Currently,
it lets a user submit a new sales order and is added to a collection. In this
article, a workflow will be use to submit and approve a sales order.
The application has no user authentication and authorization for simplicity. Also
note that the focus of this article is how to persist a workflow and not how
to create the workflow. Also, if you have questions regarding MVVM, you can refer
to the article mentioned above.
Application
The following figure shows the application used as an example in this article.
A user can create a sales order. This will be added to a database and shown in the
data grid. The status of the sales order will be set to “Open”. The user can
also select a sales order and approve it, and the status will be set to “Approved”.
Workflow
The adding and updating of a sales order in the database is actually done by a state
machine workflow which is shown below. The salesOrderReceived and salesOrderApproved
activities are both event-driven. The workflow starts when a user submits a sales
order. It becomes idle afterwards and may take some time before a user approves
the sales order. When the machine is turned off or the application crashes during
this time, the user won’t be able to approve a sales order since the workflow
instance was not saved. Before tackling how to persist the workflow, let’s discuss
first how this workflow can be used by the application.

Local Service
The following code listing shows the class that is used by the application to communicate
with the workflow.
public class OrderingService : IOrderingService, IDisposable
{
#region IOrderingService Members
public event EventHandler<OrderingEventArgs> SalesOrderReceived;
public event EventHandler<OrderingEventArgs> SalesOrderApproved;
public void NotifySalesOrdersChanged()
{
if (SalesOrdersChanged != null)
{
SalesOrdersChanged(this, new EventArgs());
}
}
#endregion
#region Events
public event EventHandler SalesOrdersChanged;
#endregion
#region Private Fields
private static OrderingService service;
private WorkflowRuntime runtime;
private AutoResetEvent waitEvent;
private ObservableCollection<SalesOrder> salesOrders;
#endregion
#region Constructor
private OrderingService()
{
runtime = new WorkflowRuntime();
runtime.WorkflowCompleted +=
(
(object sender, WorkflowCompletedEventArgs e) =>
{
if (e.WorkflowDefinition is GetOrdersWorkflow)
{
salesOrders = (ObservableCollection<SalesOrder>)e.OutputParameters["SalesOrders"];
waitEvent.Set();
}
}
);
ExternalDataExchangeService dataExchangeService = new ExternalDataExchangeService();
runtime.AddService(dataExchangeService);
dataExchangeService.AddService(this);
runtime.StartRuntime();
waitEvent = new AutoResetEvent(false);
}
#endregion
#region Public Properties
public static OrderingService Instance
{
get
{
if (service == null)
{
service = new OrderingService();
}
return service;
}
}
#endregion
#region Public Methods
public ObservableCollection<SalesOrder> GetSalesOrders()
{
WorkflowInstance instance = runtime.CreateWorkflow(typeof(GetOrdersWorkflow));
instance.Start();
waitEvent.WaitOne();
return salesOrders;
}
public void ReceiveOrder(SalesOrder salesOrder)
{
if (SalesOrderReceived != null)
{
WorkflowInstance instance = runtime.CreateWorkflow(typeof(OrderingWorkflow));
salesOrder.WorkflowInstanceId = instance.InstanceId;
instance.Start();
SalesOrderReceived(null, new OrderingEventArgs(instance.InstanceId, salesOrder));
}
}
public void ApproveOrder(SalesOrder salesOrder)
{
if (SalesOrderApproved != null)
{
SalesOrderApproved(null, new OrderingEventArgs(salesOrder.WorkflowInstanceId, salesOrder));
}
}
#endregion
#region IDisposable Members
private bool disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if(!this.disposed)
{
if(disposing)
{
runtime.Dispose();
}
disposed = true;
}
}
~OrderingService()
{
Dispose(false);
}
#endregion
}
As you might have noticed in the constructor, the external data exchange service
is added to the workflow runtime’s services to communicate with the state machine
workflow. The event-driven activities mentioned previously are triggered when
the SalesOrderReceived and SalesOrderApproved events are raised. These events
are raised inside the ReceiveOrder() and ApproveOrder() methods.
In the ReceiveOrder() method, the workflow instance’s id is saved in the sales order’s
WorkflowInstanceId property. This is used to determine the workflow instance
when approving a sales order as you can see in the ApproveOrder() method. This
id is also saved to the database along with the sales order details.
Meanwhile, a GetSalesOrders() method is used for getting the sales orders from the
database, through the use of a sequential workflow. In contrast to the first
workflow, this workflow does not need to be persisted because it executes very
shortly.
Going back to the first figure, there’s already an open sales order in the data grid.
If we try to restart the application and approve the sales order, we’ll get an
exception like the following: Event "SalesOrderApproved" on interface
type "Workflows.IOrderingService" for instance id "8e4c1cb2-7552-4823-859b-c71abab1eccd"
cannot be delivered.
The exception happened because the workflow runtime can’t find the workflow instance
in memory. The runtime won’t also look at a persistence store because it does
not have a persistence service. If ever we add one now, it still won’t be able
to find the workflow instance because the instance wasn’t persisted previously.
Persistence Database
To enable persistence, we need to create a persistence database. In this example,
I’ll be using SQL Server Express 2005 to create the database and name it OrderingPersistence.
Execute the following SQL scripts on the new database: SqlPersistence_Schema
and SqlPersistence_Logic. These are both located at %WINDIR%\Microsoft.NET\Framework\v3.0\Windows
Workflow Foundation\SQL\<language>\. It is better if you could use Management
Studio to easily do this.
Check if the CompletedScope and InstanceState tables are created. In the example,
only the InstanceState table will be used. The current state of a workflow is
stored here when it is unloaded, but is removed once the workflow is completed.
On the other hand, the CompletedScope table is used when there’s a workflow that
uses compensation. For example, a transaction scope activity that supports compensation
can only be compensated after the scope is completed.
SQL Persistence Service
In order to use the database, an SQL persistence service must be added to the workflow
runtime’s services. This is an out-of-the-box persistence service provided by
WF. However, you can create a custom persistence service to be used for other
types of databases or to extend the functionality of an existing persistence
service.
Let’s add the following application setting in the project file where the persistence
service will be initialized: Data Source=.\SQLEXPRESS;Initial Catalog=OrderingPersistence;Integrated
Security=True. Add the following code when setting up the workflow runtime.
NameValueCollection args = new NameValueCollection();
args.Add("ConnectionString", Settings.Default.OrderingPersistenceConnectionString);
args.Add("UnloadOnIdle", "true");
SqlWorkflowPersistenceService persistenceService = new SqlWorkflowPersistenceService(args);
runtime.AddService(persistenceService);
We used a simple SqlWorkflowPersistenceService constructor, where we supplied the
connection string to the database and whether the workflow should be unloaded
when it has become idle. Please check the MSDN library for details on other constructors.
If you try again to create a sales order, a new record is inserted in the InstanceState
table of the persistence database.

Restart the application and approve the order. Notice that the exception previously
encountered did not occur. This means that the workflow runtime has found the
workflow instance stored in the persistence database. Also check the persistence
database and you could see that the entry has been removed because the workflow
is already completed.
The Visual Studio 2008 example can be downloaded here. Take note that the example used SQL Server 2005 Express and WPF Toolkit from Codeplex.
To run the example, create a database called Ordering and execute the SQL script
that is included in the DataAccess project.