Persisting WF Workflows

Persisting a workflow means to store a workflow in a durable medium to be loaded for later use. Some workflows are executed very shortly that they don’t need to be persisted. However, some workflows are long-running and are not completed for some period of time. This article shows how a workflow is persisted to a database and how to load it to continue execution.

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.

By Michael Detras   Popularity  (2264 Views)
Biography - Michael Detras
.NET developer. Interested in WPF, Silverlight, and XNA.
My blog