images

This chapter continues the discussion of workflow persistence that began in Chapter 11. In that chapter, you learned the basics of workflow persistence using the SqlWorkflowInstanceStore. By following the examples presented in that chapter, you should now know how to enable persistence for applications that are hosted by the WorkflowApplication class as well as workflow services hosted by IIS or self-hosted by the WorkflowServiceHost class.

This chapter focuses on ways to extend or customize workflow persistence. It builds upon the examples that were presented in Chapter 11. Additional examples in this chapter extend persistence using the PersistenceParticipant class and demonstrate the promotion of properties to make them externally queryable. Another example demonstrates how to use the WorkflowControlEndpoint to manage active workflow instances.

The chapter concludes with an example that implements a custom instance store. The instance store persists workflow instances to the file system rather than to a database.

images Note This chapter assumes that you are using the examples presented in Chapter 11 as the starting point for this chapter. In particular, the ActivityLibrary, ServiceLibrary, ServiceHost, and OrderEntryConsoleClient projects that were first developed in Chapter 11 will be used in this chapter.

Understanding the PersistenceParticipant Classes

You can customize workflow persistence in two primary ways. First, you can implement your own instance store. This option provides you with complete flexibility as to how persistence is implemented. However, this is also the most labor-intensive option. Second, WF provides the PersistenceParticipant class that enables you to participate in workflow persistence without the need to implement your own instance store. By deriving a custom workflow extension from the abstract PersistenceParticipant class, you can inject additional data that is persisted along with the workflow instance.

images Note Developing your own instance store is demonstrated later in this chapter.

The PersistenceParticipant Class

Here are the most important members of the PersistenceParticipant class:

images

You can follow these steps to use this extension mechanism:

  1. Develop a custom workflow extension that derives from the PersistenceParticipant class (found in the System.Activities.Persistence namespace).
  2. Override the virtual CollectValues method to inject additional data to be persisted.
  3. Optionally, override the virtual PublishValues method to load additional data that was previously persisted.
  4. Optionally, override the virtual MapValues method to review data to be persisted that was collected from all persistence participants.

The CollectValues and PublishValues methods complement each other. The CollectValues method is invoked when a workflow instance is persisted and is your opportunity to add data to be persisted. The method signature defines two out arguments of type IDictionary that must be populated by your code. One dictionary is used for read-write values and the other for write-only values. The difference between the two collections is that read-write values are expected to make a round-trip. They are persisted and then loaded when the workflow instance is loaded. The write-only values are persisted but not loaded. By specifying them as write-only values, you are indicating that they are not vital to the successful execution of the workflow. They might be queried and used by other nonworkflow portions of the application.

The PublishValues method is invoked during the process of loading a workflow instance that was previously persisted. The method is passed an IDictionary of read-write values that were previously persisted. This is your opportunity to retrieve each named value from the collection and load it back into memory.

Each value that you persist or load is uniquely identified with a string name that you must provide. The name is defined as an XName so it includes a full namespace.

The other virtual method that you can choose to override is MapValues. The purpose of this method is not as straightforward as the other methods. To better understand its purpose, you need to understand that persistence is accomplished in stages. In the first stage, the CollectValues method is invoked for all persistence participants that are currently loaded. At the end of this stage, all data to be persisted has been collected. In the second stage, the MapValues method is invoked to provide you with an opportunity to make additional persistence decisions based on the superset of data that was collected from all participants. The MapValues method is passed two IDictionary arguments (one for read-write values and another for write-only values) and returns a new IDictionary instance. The IDictionary of values that you must return is added to the previously collected data as write-only values. In the third stage of persistence, the SaveWorkflowCommand is executed against the instance store to persist the workflow instance and any additional data that was collected.

The PersistenceIOParticipant Class

WF also provides the PersistenceIOParticipant class, which derives from PersistenceParticipant. Here are the most important additional members of this class:

images

The additional methods provided by the PersistenceIOParticipant class enable you to save and load data during the standard persistence operations, but to use a storage mechanism that is separate from the instance store. For example, you want to use the standard SqlWorkflowInstanceStore for the primary workflow persistence. But in additional to the standard workflow persistence, you also need to save a subset of the instance data to an XML file or to a table in another application database. The PersistenceIOParticipant class allows you to accomplish this by overriding the methods listed previously.

Which Class to Use?

You can follow these general guidelines to help you determine which base class to use:

  • Use PersistenceParticipant when you need to persist and reload additional data with each workflow instance.
  • Use PersistenceParticipant when you need to persist additional write-only data.
  • Use PersistenceIOParticipant when you want to persist and reload workflow instance data using an instance store but you also need to persist (and possibly reload) data from a separate data store.

Using the PersistenceParticipant Class

To demonstrate how to use the PersistenceParticipant class, you will modify the custom extension that you implemented in Chapter 11. One of the design problems with the previous examples is that the ItemSupportExtension maintains a running count of available inventory for each item. As each new item is added to an order, the available inventory is decremented by the requested quantity. In the previous examples, the inventory is reset to the starting values when the applications are recycled. It would be better if the current inventory values were persisted and then reloaded along with the workflow instance.

In this example, you will modify the ItemSupportExtension class to persist the inventory values. You will add the PersistenceParticipant class as the base class and override the CollectValues and PublishValues methods.

images Note Even with these changes, this is not an optimal solution to this particular test scenario. Saving the available inventory with each workflow instance works great if you work with only a single instance at any one time. In a more realistic scenario where you might have hundreds or thousands of orders executing at any one time, the inventory would naturally be persisted in a database. However, this contrived example should be adequate to demonstrate the persistence concepts that are the focus of this chapter.

Modifying the ItemSupportExtension Class

You originally implemented the ItemSupportExtension class in Chapter 11. It is located in the ActivityLibrary project. Here is an abbreviated copy of the class showing the changes that you need to make:

using System;
using System.Activities.Persistence;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;

namespace ActivityLibrary
{
    public class ItemSupportExtension : PersistenceParticipant, IItemSupport
    {

        private XName _itemsName = XName.Get(
            "ItemDefinitions", "ActivityLibrary.ItemSupportExtension");
        private XName _orderIdName = XName.Get(
            "OrderId", "ActivityLibrary.ItemSupportExtension");

        #region PersistenceParticipant members

The code in the CollectValues method adds the entire collection of ItemDefinition objects (the _items variable) as a read-write value. This is the collection containing the available inventory values for each item. Additionally, the value of the _orderId variable is added to the write-only collection. Since it is added to the write-only collection, this value is serialized and persisted along with the instance store but is not reloaded. It will be used in the next example to demonstrate the use of the Promote method of the SQL Server instance store.

        protected override void CollectValues(
            out IDictionary<System.Xml.Linq.XName, object> readWriteValues,
            out IDictionary<System.Xml.Linq.XName, object> writeOnlyValues)
        {
            readWriteValues = new Dictionary<System.Xml.Linq.XName, object>();
            lock (_items)
            {
                readWriteValues.Add(_itemsName, _items);
            }

            if (_orderId > 0)
            {
                writeOnlyValues = new Dictionary<System.Xml.Linq.XName, object>();
                writeOnlyValues.Add(_orderIdName, _orderId);
                _orderId = 0;
            }
            else
            {
                writeOnlyValues = null;
            }
        }

The PublishValues method retrieves the value of the collection of ItemDefinition objects that was previously persisted and assigns it to the _items member variable.

        protected override void PublishValues(
            IDictionary<System.Xml.Linq.XName, object> readWriteValues)
        {
            object value = null;
            if (readWriteValues.TryGetValue(_itemsName, out value))
            {
                if (value is Dictionary<Int32, ItemDefinition>)
                {
                    lock (_items)
                    {
                        _items = value as Dictionary<Int32, ItemDefinition>;
                    }
                }
            }
        }

        #endregion
    }
}

Testing the Revised Extension

After rebuilding the solution, you can test the enhanced extension class by running the ServiceHost and OrderEntryConsoleClient projects as you did for the previous example. Start a new order, add a few items, and then close the projects without completing the order. Then restart the projects and complete the order.

You should see that the final ending inventory that is displayed has been reduced by the items that you added to the order. This confirms that the collection of ItemDefinition objects was persisted and loaded along with the workflow instance. Previously, the inventory values were reset to their original values when the ServiceHost project was restarted.

images Caution The ServiceHost project loads a singleton instance of the ItemSupportExtension that is shared by all workflow instances. When you are adding your own extensions, you can also use the override of the WorkflowExtensions.Add method that allows you to assign a delegate. The delegate is executed for each workflow instance, allowing you to construct a new instance of the extension for each workflow instance.

Be careful if you use this approach with an extension that derives from the PersistenceParticipant class. If the extension implements an interface that you defined (IItemSupport in this example), you might be tempted to specify the interface as the generic type when you add the extension like this: host.WorkflowExtensions.Add<IItemSupport>(() => return new ItemSupportExtension()). If you do this, the extension will not be recognized as a persistence participant, and the persistence methods (CollectValues, MapValues, PublishValues) will not be invoked. Instead, you need to specify the concrete type name as the generic type like this: host.WorkflowExtensions.Add<ItemSupportExtension>(() => return new ItemSupportExtension()).

Promoting Properties

When you extend workflow persistence by providing your own PersistenceParticipant-derived class, the additional data that you inject is serialized and saved in the instance store along with the workflow instance. This means that the data is completely opaque and not easily available for consumption by any process except for workflow persistence. You can’t easily query it or use it to look up the workflow instance ID that is associated with it.

However, many times you might want to extend workflow persistence specifically to provide a lookup mechanism. For example, in the workflow service examples that were presented in Chapter 11, each workflow instance was associated with an order ID number. You might need the ability to look up the workflow instance ID that is associated with a particular order ID.

To address this need, the SqlWorkflowInstanceStore class supports the concept of property promotion. Promotion instructs the instance store to save the properties that you identify to another table in the persistence database. The fully qualified name of this table is  System.Activities.DurableInstancing.InstancePromotedProperties. There is also a view provided with the same name (InstancePromotedProperties). The table contains a number of variant columns (named Value1, Value2, and so on) that can store most simple data types (integers, strings, and so on) and columns that can store binary data. The workflow instance ID is one of the columns in the table. Therefore, once the properties are persisted in this manner, they are queryable and available for your use. If you query for some known value (for example an order ID), you will be able to identify the workflow instance ID associated with that value.

Promotion is enabled by calling the Promote method of the SqlWorkflowInstanceStore class. The method is defined with this signature:

public void Promote(string name, IEnumerable<XName> promoteAsVariant,
    IEnumerable<XName> promoteAsBinary)

The name parameter should be a meaningful name that you apply to the promotion. It is also saved to the database along with the individual properties that you specify in the collection. The promoteAsVariant parameter is a collection of properties that you want to store in the variant columns. Each one is uniquely identified by a namespace-qualified name that must exactly match the name you used in your custom PersistenceParticipant class to persist the values. The properties that you specify are persisted in the variant columns in the order in which they are defined. So, the first property goes into the Value1 column, the second in Value2, and so on. The second collection is for properties that you want to save as binary data.

images Note The promoted data exists for the same duration as the workflow instance itself. When the workflow instance completes and is removed from the database, the promoted data is also removed.

Using Property Promotion

To demonstrate property promotion, you will make a few small changes to the previous example to promote the order ID property. Since the order ID is used for content correlation, it makes sense that someone might want to query for this value.

You will complete these tasks to implement this example:

  1. Modify the ServiceHost project to promote the order ID.
  2. Modify the OrderEntryConsoleClient project to query for the promoted values.
  3. Add the SQL Server connection string to the App.config file of the OrderEntryConsoleClient project.

Modifying the ServiceHost

Modify the Program.cs file of the ServiceHost project to promote the OrderId property. Here is an abbreviated listing of the code showing the new code that you need to add:

 namespace ServiceHost
{
    class Program
    {

        private static WorkflowServiceHost CreateServiceHost(
            String xamlxName, IItemSupport extension)
        {

            List<XName> variables = new List<XName>()
            {  
                XName.Get("OrderId", "ActivityLibrary.ItemSupportExtension")
            };
            storeBehavior.Promote("OrderEntry", variables, null);

            host.Description.Behaviors.Add(storeBehavior);

        }
    }
}

images Tip Notice that the property name and namespace specified here exactly match the name used in the ItemSupportExtension class to save the order ID.

Modifying the Client Application

You will now modify the OrderEntryConsoleClient project to query the table containing the promoted values. This provides the client application with the ability to display a list of all active and incomplete workflow instances along with their order IDs.

Here is an abbreviated listing of the Program.cs file of the OrderEntryConsoleClient project with the implementation of the private Query method:

namespace OrderEntryConsoleClient
{
    class Program
    {

        static void Query()
        {
            String sql =
             @"Select Value1, InstanceId from
               [System.Activities.DurableInstancing].[InstancePromotedProperties]
               where PromotionName = 'OrderEntry'              
               order by Value1";
            try
            {
                queriedInstances.Clear();
                string connectionString = ConfigurationManager.ConnectionStrings
                    ["InstanceStore"].ConnectionString;
                using (SqlConnection conn = new SqlConnection(connectionString))
                {
                    conn.Open();
                    SqlCommand cmd = new SqlCommand(sql, conn);
                    using (SqlDataReader reader = cmd.ExecuteReader())
                    {
                        Console.WriteLine("Promoted OrderId values:");
                        while (reader.Read())
                        {
                            Int32 orderId = (Int32)reader["Value1"];
                            Guid instanceId = (Guid)reader["InstanceId"];

                            Console.WriteLine("OrderId={0}, InstanceId={1}",
                                orderId, instanceId);
                            if (!queriedInstances.ContainsKey(orderId))
                            {
                                queriedInstances.Add(orderId, instanceId);
                            }
                        }
                    }
                }
            }
            catch (Exception exception)
            {
                lastOrderId = 0;
                Console.WriteLine("Query Unhandled exception: {0}",
                    exception.Message);
            }
        }

    }
}

The code not only displays the results of the query on the console, but it also saves the data in a dictionary. This dictionary will be used in a subsequent example to cancel an active workflow instance.

Configuring the Client Application

The query code assumes that the SQL Server connection string is defined in the App.config file, so you’ll need to add it to the existing file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add name="InstanceStore"
        connectionString="Data Source=localhostSQLExpress;
           Initial Catalog=InstanceStore;Integrated Security=True;
           Asynchronous Processing=True"
        providerName="System.Data.SqlClient" />
  </connectionStrings>

</configuration>

Testing the Revised Example

After rebuilding the project, you should be ready to run the ServiceHost and OrderEntryConsoleClient projects. To test this new functionality, I started two new orders, adding an item to each order. I then executed the query command, which successfully listed the workflow instance IDs along with the order ID that is associated with each instance. Here is a representative sample of my results:


Commands: start | add | complete | query | cancel | exit

start

Enter an OrderId (int) for the order

1

New order 1 started


Commands: start | add | complete | query | cancel | exit

add

Enter ItemId and Quantity (Ex: 101 1)

101 1

Ordered 1 of ItemId 101 for OrderId 1

Commands: start | add | complete | query | cancel | exit

start

Enter an OrderId (int) for the order

2

New order 2 started


Commands: start | add | complete | query | cancel | exit

add

Enter ItemId and Quantity (Ex: 101 1)

202 2

Ordered 2 of ItemId 202 for OrderId 2


Commands: start | add | complete | query | cancel | exit

query

Promoted OrderId values:

OrderId=1, InstanceId=0ce418f4-74a4-49d0-85cb-886d9139d120

OrderId=2, InstanceId=4b6a0ade-9afe-435e-ba5d-99dee801e5fc


Commands: start | add | complete | query | cancel | exit

Understanding the Management Endpoint

WF provides a standard WCF management endpoint (WorkflowControlEndpoint) that can be added to a service. This endpoint is designed to assist with the management of long-running workflow services, such as the one that you have been using in this chapter and in Chapter 11. This endpoint supports operations that manage existing service instances such as Cancel, Suspend, Terminate, Abandon, Unsuspend, and Run.

To enable this endpoint, you simply need to add it to the WorkflowServiceHost instance. This can be accomplished in code or via entries in the App.config file for the project. You can also add these same entries to a Web.config file if you are using IIS to host your workflow services.

To consume the management endpoint, WF also provides a client proxy class named WorkflowControlClient. It eliminates the need for you to create your own client proxy for the management endpoint.

Using the Management Endpoint

To demonstrate a realistic use of the managed endpoint and client class, you will modify the previous example to allow cancellation of an active workflow instance. The client application will use the results of the query (implemented in the last example) to identify the workflow instance ID to cancel.

You will complete these tasks to implement this example:

  1. Add the WorkflowControlEndpoint to the ServiceHost configuration file.
  2. Modify the OrderEntryConsoleClient project to use the WorkflowControlClient.
  3. Define the management endpoint in the client configuration file.

Modifying the ServiceHost Configuration

Since the ServiceHost project is already defining all endpoints via the App.config file, it makes sense to add the WorkflowControlEndpoint in the same manner. Here are the additional entries that you need to make to the App.config file of the ServiceHost project:

<?xml version="1.0"?>
<configuration>

  <system.serviceModel>
    <services>
      <service name="OrderEntryService">

        <endpoint kind="workflowControlEndpoint"
          address="http://localhost:9000/OrderEntryControl"
          binding="wsHttpBinding" />
      </service>
    </services>

  </system.serviceModel>

</configuration>

Modifying the Client Application

The OrderEntryConsoleClient project will now be modified to cancel an active workflow instance using the WorkflowControlClient class. Here is the implementation for the private Cancel method in the Program.cs file of this project:

namespace OrderEntryConsoleClient
{
    class Program
    {

        static void Cancel()
        {
            try
            {
                Console.WriteLine("Enter an OrderId to cancel");
                String input = Console.ReadLine();
                Int32 orderIdToCancel = 0;
                if (String.IsNullOrEmpty(input))
                {
                    Console.WriteLine("A value must be entered");
                    return;
                }
                Int32.TryParse(input, out orderIdToCancel);
                if (orderIdToCancel == 0)
                {
                    Console.WriteLine("OrderId must not be zero");
                    return;
                }

The workflow instance ID to cancel is obtained from the private queriedInstances dictionary. This dictionary is populated by a call to the Query method.

                Guid instanceId = Guid.Empty;
                if (!queriedInstances.TryGetValue(orderIdToCancel,
                    out instanceId))
                {
                    Console.WriteLine("Instance not found");
                    return;
                }

                using (WorkflowControlClient client =
                    new WorkflowControlClient("ClientControlEndpoint"))
                {
                    client.Cancel(instanceId);
                }
            }
            catch (Exception exception)
            {
                lastOrderId = 0;
                Console.WriteLine("Cancel Unhandled exception: {0}",
                    exception.Message);
            }
        }
    }
}

Configuring the Client Application

The client project now requires the address of the management control endpoint that is hosted by the ServiceHost project. Add these lines to the App.config file of the OrderEntryConsoleClient project:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>

  <system.serviceModel>

    <client>

      <endpoint name="ClientControlEndpoint"
        contract="System.ServiceModel.Activities.IWorkflowInstanceManagement"
        address="http://localhost:9000/OrderEntryControl"
        binding="wsHttpBinding" />
    </client>
  </system.serviceModel>
</configuration>

Testing the Revised Example

After rebuilding the solution, start the ServiceHost and OrderEntryConsoleClient projects once again. To test this new functionality, you need to have one or two active workflow instances. If you didn’t complete the instances from the previous test, you can use them. Otherwise, you’ll need to add at least one instance with at least one item. Use the query command to prepare a list of active instances. Then you can use the cancel command to cancel one of the instances. Finally, execute the query command one more time to verify that the canceled instance is actually gone.

Here are my results, which assume that the instances from the previous example were not completed and are still active:


Commands: start | add | complete | query | cancel | exit

query

Promoted OrderId values:

OrderId=1, InstanceId=0ce418f4-74a4-49d0-85cb-886d9139d120

OrderId=2, InstanceId=4b6a0ade-9afe-435e-ba5d-99dee801e5fc


Commands: start | add | complete | query | cancel | exit

cancel

Enter an OrderId to cancel

1


Commands: start | add | complete | query | cancel | exit

query

Promoted OrderId values:

OrderId=2, InstanceId=4b6a0ade-9afe-435e-ba5d-99dee801e5fc


Commands: start | add | complete | query | cancel | exit

complete

Enter an OrderId (int) for the order

2

Order 2 Is Completed


Ordered Items:

ItemId=202, Quantity=2, UnitPrice=2.34, Total=4.68


Commands: start | add | complete | query | cancel | exit

Implementing a Custom Instance Store

As you have already seen, WF includes an instance store (SqlWorkflowInstanceStore) that uses a SQL Server database for persistence. This instance store should really be your default answer when you need to add workflow persistence to an application. In most garden-variety scenarios, the instance store that is provided with WF should meet your needs.

However, WF does provide the necessary classes to develop your own instance store. In this section of the chapter, you will develop a custom instance store that persists workflow instances to the file system instead of a database.

I chose to persist to the file system for a couple of reasons. First, WF already has an instance store that uses a SQL Server database, and I wanted to demonstrate something that was significantly different from what you get out of the box. Second, using the file system keeps things as simple as possible. It eliminates the SQL (or LINQ), connection strings, transaction management, and so on. It allows me to present code that focuses on the required interaction with WF rather than with SQL Server.

Understanding the InstanceStore Class

To implement a custom instance store, you must derive from the InstanceStore class (located in the System.Runtime.DurableInstancing namespace). This class defines a number of members, but here are the virtual members that you must override:

images

The WF runtime communicates with an instance store through a set of command classes (listed in the next section). Your custom instance store is responsible for handling these commands.

The BeginTryCommand and EndTryCommand methods are invoked by the persistence framework to execute commands asynchronously. The commands that are passed to BeginTryCommand are the result of automatic persistence operations (for example, when a workflow instance becomes idle). On the other hand, the TryCommand method is invoked in response to some direct action by you. For example, when you execute CreateWorkflowOwnerCommand or DeleteWorkflowOwnerCommand against an instance store, those commands are passed to TryCommand, not to BeginTryCommand.

What this means is that you must provide an implementation for the synchronous and asynchronous versions of these methods. However, as you will see in the example code, the TryCommand can simply pass the command to BeginTryCommand to satisfy this requirement.

Understanding the Instance Persistence Commands

All persistence  commands derive from the base InstancePersistenceCommand class. Here is a brief summary of each command and its purpose:

images

Some of these command classes contain additional properties that you will definitely need to use in your instance store. For example, the SaveWorkflowCommand contains the workflow instance data that you need to persist. And the LoadWorkflowByInstanceKeyCommand contains the lookup key (a correlation key) that you must use to retrieve the workflow instance.

images Note The properties of these commands are more easily understood by seeing them in actual use. For this reason, I’ll skip the usual list of properties for each class to keep this discussion moving along.

Understanding the InstancePersistenceContext Class

The InstancePersistenceContext class is similar in purpose to the ActivityContext that you reference within custom activities. The ActivityContext provides access to the workflow runtime environment. In a similar way, the InstancePersistenceContext provides access to the runtime environment for persistence. It provides properties and methods that you use to interact with the WF persistence runtime.

An InstancePersistenceContext object is passed as an argument to the TryCommand and BeginTryCommand methods of the InstanceStore class along with the command to execute.

One of the more important properties of the InstancePersistenceContext class is the InstanceView property. It provides access to an InstanceView object, which is a snapshot of a single workflow instance.

The InstanceView contains properties that describe the workflow instance that the current command executes against. For example, this class has an InstanceId property that uniquely identifies the workflow instance. You will frequently need to navigate to this InstanceId property of the InstanceView when you need to know the ID of the workflow instance to load or save.

Implementing a File System–Based Instance Store

The custom instance store that you are about to develop is separated into two classes. The FileSystemInstanceStore class is derived from the base InstanceStore class and is responsible for the handling of persistence commands and interacting with the WF persistence API. It defers all the actual file system I/O to a second class named FileSystemInstanceStoreIO. This separation should make it easier to distinguish between the code that responds to the persistence commands (which you will need to implement regardless of the storage medium) and the I/O code that is specific to the chosen storage medium (the file system).

To begin the implementation of the instance store, add a new class to the ActivityLibrary project named FileSystemInstanceStore. Here is the annotated code for this class:

using System;
using System.Activities.DurableInstancing;
using System.Collections.Generic;
using System.Runtime.DurableInstancing;
using System.Threading;
using System.Xml.Linq;

namespace ActivityLibrary
{
    public class FileSystemInstanceStore : InstanceStore
    {
        private Guid _ownerId = Guid.NewGuid();
        private Guid _lockToken = Guid.NewGuid();
        private FileSystemInstanceStoreIO _dataStore;

        public FileSystemInstanceStore()
        {
            _dataStore = new FileSystemInstanceStoreIO();
        }

        #region InstanceStore overrides

The BeginTryCommand is invoked to asynchronously process a persistence command. The code uses a switch statement to branch the processing based on the type of command that is received. The code to process each command follows a consistent pattern. The only real difference is the private method that is invoked to process each command.

To easily process the commands asynchronously, a Func delegate is declared that executes the correct private method. The BeginInvoke method of the delegate is used to begin asynchronous execution of the code that is assigned to the delegate. The Func delegate is declared to return an Exception. This is necessary in order to pass any unhandled exception from the asynchronous thread to the original thread. The callback code that is assigned to the BeginInvoke method first calls EndInvoke on the delegate. This completes the asynchronous operation. It then passes the Exception (if any) to the original callback that was provided as an argument to the BeginTryCommand method. Execution of the original callback triggers execution of the EndTryCommand method.

        protected override IAsyncResult BeginTryCommand(
            InstancePersistenceContext context,
            InstancePersistenceCommand command,
            TimeSpan timeout, AsyncCallback callback, object state)
        {
            Console.WriteLine("BeginTryCommand: {0}", command.GetType().Name);

            switch (command.GetType().Name)
            {
                case "CreateWorkflowOwnerCommand":

                    Func<Exception> createFunc = () =>
                    {
                        return ProcessCreateWorkflowOwner(context,
                            command as CreateWorkflowOwnerCommand);
                    };

                    return createFunc.BeginInvoke((ar) =>
                        {
                            Exception ex = createFunc.EndInvoke(ar);
                            callback(new InstanceStoreAsyncResult(ar, ex));
                        }, state);

                case "LoadWorkflowCommand":
                    Func<Exception> loadFunc = () =>
                    {
                        return ProcessLoadWorkflow(context,
                            command as LoadWorkflowCommand);
                    };

                    return loadFunc.BeginInvoke((ar) =>
                        {
                            Exception ex = loadFunc.EndInvoke(ar);
                            callback(new InstanceStoreAsyncResult(ar, ex));
                        }, state);

                case "LoadWorkflowByInstanceKeyCommand":
                    Func<Exception> loadByKeyFunc = () =>
                    {
                        return ProcessLoadWorkflowByInstanceKey(context,
                            command as LoadWorkflowByInstanceKeyCommand);
                    };

                    return loadByKeyFunc.BeginInvoke((ar) =>
                        {
                            Exception ex = loadByKeyFunc.EndInvoke(ar);
                            callback(new InstanceStoreAsyncResult(ar, ex));
                        }, state);

                case "SaveWorkflowCommand":
                    Func<Exception> saveFunc = () =>
                    {
                        return ProcessSaveWorkflow(context,
                            command as SaveWorkflowCommand);
                    };

                    return saveFunc.BeginInvoke((ar) =>
                        {
                            Exception ex = saveFunc.EndInvoke(ar);
                            callback(new InstanceStoreAsyncResult(ar, ex));
                        }, state);

                default:

                    return base.BeginTryCommand(
                        context, command, timeout, callback, state);
            }
        }

The EndTryCommand method is paired with the execution of the BeginTryCommand method. Execution of this method is triggered by invoking the original callback argument that was passed to the BeginTryCommand.

A private InstanceStoreAsyncResult class is passed to this method by the Func delegate callback code. This private class was needed to pass the unhandled exception (if any) along the standard IAsyncResult properties. If an Exception was passed, it is rethrown here in order to throw it on the correct thread.

        protected override bool EndTryCommand(IAsyncResult ar)
        {
            if (ar is InstanceStoreAsyncResult)
            {
                Exception exception = ((InstanceStoreAsyncResult)ar).Exception;
                if (exception != null)
                {
                    throw exception;
                }
            }

            return true;
        }

The TryCommand method defers execution of the command to the BeginTryCommand. Since BeginTryCommand executes the command asynchronously, TryCommand has to wait for the command to complete before returning.

        protected override bool TryCommand(
            InstancePersistenceContext context,
            InstancePersistenceCommand command, TimeSpan timeout)
        {
            ManualResetEvent waitEvent = new ManualResetEvent(false);
            IAsyncResult asyncResult = BeginTryCommand(
                context, command, timeout, (ar) =>
            {
                waitEvent.Set();
            }, null);

            waitEvent.WaitOne(timeout);
            return EndTryCommand(asyncResult);
        }

        #endregion

        #region Command processing

The ProcessCreateWorkflowOwner command is executed to associate a host application as an instance owner. The BindInstanceOwner method of the context is executed to satisfy the requirements of this command.

        private Exception ProcessCreateWorkflowOwner(
            InstancePersistenceContext context,
            CreateWorkflowOwnerCommand command)
        {
            try
            {
                context.BindInstanceOwner(_ownerId, _lockToken);
                return null;
            }
            catch (InstancePersistenceException exception)
            {
                Console.WriteLine(
                    "ProcessCreateWorkflowOwner exception: {0}",
                    exception.Message);
                return exception;
            }
        }

The ProcessLoadWorkflow method is invoked to load a workflow instance when a LoadWorkflowCommand is received. This command may be sent to load an existing workflow instance that was previously persisted or to initialize a new instance.

If an existing instance is to be loaded, the instance ID to load is obtained from the InstanceView.InstanceId property of the context. This value, along with the context, is passed to a private SharedLoadWorkflow method.

        private Exception ProcessLoadWorkflow(
            InstancePersistenceContext context,
            LoadWorkflowCommand command)
        {
            try
            {
                if (command.AcceptUninitializedInstance)
                {
                    context.LoadedInstance(InstanceState.Uninitialized,
                        null, null, null, null);
                }
                else
                {
                    SharedLoadWorkflow(context, context.InstanceView.InstanceId);
                }
                return null;
            }
            catch (InstancePersistenceException exception)
            {
                Console.WriteLine(
                    "ProcessLoadWorkflow exception: {0}",
                    exception.Message);
                return exception;
            }
        }

The ProcessLoadWorkflowByInstanceKey method is invoked in response to the receipt of a LoadWorkflowByInstanceKeyCommand. This command is used to retrieve an existing workflow instance based on an instance key. This is not the workflow instance ID. It is a correlation key that must be used to look up the actual workflow instance ID. A single workflow instance may have multiple instance keys.

        private Exception ProcessLoadWorkflowByInstanceKey(
            InstancePersistenceContext context,
            LoadWorkflowByInstanceKeyCommand command)
        {
            try
            {
                Guid instanceId = _dataStore.GetInstanceAssociation(
                    command.LookupInstanceKey);
                if (instanceId == Guid.Empty)
                {
                    throw new InstanceKeyNotReadyException(
                        String.Format("Unable to load instance for key: {0}",
                            command.LookupInstanceKey));
                }
                SharedLoadWorkflow(context, instanceId);
                return null;
            }
            catch (InstancePersistenceException exception)
            {
                Console.WriteLine(
                    "ProcessLoadWorkflowByInstanceKey exception: {0}",
                    exception.Message);
                return exception;
            }
        }

The SharedLoadWorkflow method is common code that is executed by the ProcessLoadWorkflow and ProcessLoadWorkflowByInstanceKey methods. It loads the instance and instance metadata for the workflow instance. After loading the data, the LoadedInstance method of the context is invoked to provide this data to the persistence API.

        private void SharedLoadWorkflow(InstancePersistenceContext context,
            Guid instanceId)
        {
            if (instanceId != Guid.Empty)
            {
                IDictionary<XName, InstanceValue> instanceData = null;
                IDictionary<XName, InstanceValue> instanceMetadata = null;
                _dataStore.LoadInstance(instanceId,
                    out instanceData, out instanceMetadata);
                if (context.InstanceView.InstanceId == Guid.Empty)
                {
                    context.BindInstance(instanceId);
                }
                context.LoadedInstance(InstanceState.Initialized,
                    instanceData, instanceMetadata, null, null);
            }
            else
            {
                throw new InstanceNotReadyException(
                    String.Format("Unable to load instance: {0}", instanceId));
            }
        }

The ProcessSaveWorkflow is invoked to persist a workflow instance. It has a number of tasks that it must complete. First, if the CompleteInstance property of the command is true, it means that the command is signaling the completion of the workflow instance. This means that the data that was previously persisted can be deleted. Second, if instance data or instance metadata is available, it is persisted. Finally, if there are any instance keys to associate with the instance, they are persisted. The instance keys are correlation keys that are later used to retrieve the correct workflow instance.

        private Exception ProcessSaveWorkflow(
            InstancePersistenceContext context,
            SaveWorkflowCommand command)
        {
            try
            {
                if (command.CompleteInstance)
                {
                    _dataStore.DeleteInstance(
                        context.InstanceView.InstanceId);
                    _dataStore.DeleteInstanceAssociation(
                        context.InstanceView.InstanceId);
                    return null;
                }

                if (command.InstanceData.Count > 0 ||
                    command.InstanceMetadataChanges.Count > 0)
                {
                    if (!_dataStore.SaveAllInstanceData(
                        context.InstanceView.InstanceId, command))
                    {
                        _dataStore.SaveAllInstanceMetaData(
                            context.InstanceView.InstanceId, command);
                    }
                }

                if (command.InstanceKeysToAssociate.Count > 0)
                {
                    foreach (var entry in command.InstanceKeysToAssociate)
                    {
                        _dataStore.SaveInstanceAssociation(
                            context.InstanceView.InstanceId, entry.Key, false);
                    }
                }
                return null;
            }
            catch (InstancePersistenceException exception)
            {
                Console.WriteLine(
                    "ProcessSaveWorkflow exception: {0}", exception.Message);
                return exception;
            }
        }

        #endregion

        #region Private types

        private class InstanceStoreAsyncResult : IAsyncResult
        {
            public InstanceStoreAsyncResult(
                IAsyncResult ar, Exception exception)
            {
                AsyncWaitHandle = ar.AsyncWaitHandle;
                AsyncState = ar.AsyncState;
                IsCompleted = true;
                Exception = exception;
            }

            public bool IsCompleted { get; private set; }
            public Object AsyncState { get; private set; }
            public WaitHandle AsyncWaitHandle { get; private set; }
            public bool CompletedSynchronously { get; private set; }
            public Exception Exception { get; private set; }
        }

        #endregion
    }
}

Implementing the FileSystemInstanceStoreIO Class

Add another class to the ActivityLibrary project, and name it FileSystemInstanceStoreIO. This class is referenced by the FileSystemInstanceStore class and implements the logic to persist workflow instances to the file system. Here is the code for this class:

using System;
using System.Activities.DurableInstancing;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.DurableInstancing;
using System.Runtime.Serialization;
using System.Text;
using System.Xml;
using System.Xml.Linq;

namespace ActivityLibrary
{
    internal class FileSystemInstanceStoreIO
    {
        private String _dataDirectory = String.Empty;

        public FileSystemInstanceStoreIO()
        {
            CreateDataDirectory();
        }

        #region Save Methods

The SaveAllInstanceData method is designed to persist the instance data of a workflow instance. The instance data can be found in the InstanceData property of the SaveWorkflowCommand that is passed to this method. This property is a collection of named elements that are individually serialized by calling the private SaveSingleEntry method. All of this instance data is saved to a single XML file that uses the workflow instance ID Guid as the file name.

        public Boolean SaveAllInstanceData(Guid instanceId,
            SaveWorkflowCommand command)
        {
            Boolean isExistingInstance = false;
            try
            {
                String fileName = String.Format("{0}.xml", instanceId);
                String fullPath = Path.Combine(_dataDirectory, fileName);
                isExistingInstance = File.Exists(fullPath);

                XElement root = new XElement("Instance");
                root.Add(new XAttribute("InstanceId", instanceId));
                XDocument xml = new XDocument(root);

                NetDataContractSerializer serializer =
                    new NetDataContractSerializer();

                XElement section = new XElement("InstanceData");
                root.Add(section);
                foreach (var entry in command.InstanceData)
                {
                    SaveSingleEntry(serializer, section, entry);
                }
                SaveInstanceDocument(fullPath, xml);
            }
            catch (IOException exception)
            {
                Console.WriteLine(
                    "SaveAllInstanceData Exception: {0}", exception.Message);
                throw exception;
            }
            return isExistingInstance;
        }

The SaveAllInstanceMetaData method is similar to the SaveAllInstanceData method just above this. The difference is that this method saves workflow instance metadata rather than the instance data. The metadata includes elements such as the workflow type that are needed by the infrastructure.

The metadata is saved to a separate file from the instance data. This separate file is necessary because of the differences in the way instance data and metadata are saved. Each time instance data is saved, it is a complete replacement of the previously saved instance data. However, metadata is made available as changes instead of a complete replacement. Each time metadata changes are available to be saved, they should be merged with any previously saved metadata.

However, this custom instance store takes a few shortcuts and saves the metadata only the first time a workflow instance is saved. No subsequent changes to metadata are actually saved. This is sufficient to produce a working instance store since no metadata changes actually occur in these examples. Saving the metadata to a separate file allows this code to completely replace the instance data without the need to merge previously saved metadata.

        public void SaveAllInstanceMetaData(Guid instanceId,
            SaveWorkflowCommand command)
        {
            try
            {
                String fileName = String.Format("{0}.meta.xml", instanceId);
                String fullPath = Path.Combine(_dataDirectory, fileName);

                XElement root = new XElement("Instance");
                root.Add(new XAttribute("InstanceId", instanceId));
                XDocument xml = new XDocument(root);

                NetDataContractSerializer serializer =
                    new NetDataContractSerializer();

                XElement section = new XElement("InstanceMetadata");
                root.Add(section);
                foreach (var entry in command.InstanceMetadataChanges)
                {
                    SaveSingleEntry(serializer, section, entry);
                }
                SaveInstanceDocument(fullPath, xml);
            }
            catch (IOException exception)
            {
                Console.WriteLine(
                    "SaveAllMetaData Exception: {0}", exception.Message);
                throw exception;
            }
        }

The SaveSingleEntry method serializes an individual data element. This shared code is invoked when instance data and metadata are saved. Each data element is represented by an InstanceValue object along with a fully qualified name. The InstanceValue class includes an Options property that is serialized along with the key and the actual data.

        private void SaveSingleEntry(NetDataContractSerializer serializer,
            XElement section, KeyValuePair<XName, InstanceValue> entry)
        {
            if (entry.Value.IsDeletedValue)
            {
                return;
            }

            XElement entryElement = new XElement("Entry");
            section.Add(entryElement);
            Serialize(serializer, entryElement, "Key", entry.Key);
            Serialize(serializer, entryElement, "Value", entry.Value.Value);
            Serialize(serializer, entryElement, "Options", entry.Value.Options);
        }

The SaveInstanceDocument method physically writes a completed XML document to a file.

        private static void SaveInstanceDocument(String fullPath, XDocument xml)
        {
            using (FileStream stream =
                new FileStream(fullPath, FileMode.Create))
            {
                XmlWriterSettings settings = new XmlWriterSettings();
                settings.Encoding = Encoding.UTF8;
                using (XmlWriter writer = XmlWriter.Create(stream, settings))
                {
                    writer.WriteRaw(xml.ToString());
                }
            }
        }

        #endregion

        #region Load Methods

The LoadInstance method retrieves and deserializes a workflow instance. The instance data and metadata for the workflow instance are both retrieved. This method invokes the LoadSingleEntry method to deserialize each individual data element.

        public Boolean LoadInstance(Guid instanceId,
            out IDictionary<XName, InstanceValue> instanceData,
            out IDictionary<XName, InstanceValue> instanceMetadata)
        {
            Boolean result = false;
            try
            {
                instanceData = new Dictionary<XName, InstanceValue>();
                instanceMetadata = new Dictionary<XName, InstanceValue>();

                String fileName = String.Format("{0}.xml", instanceId);
                String fullPath = Path.Combine(_dataDirectory, fileName);
                if (!File.Exists(fullPath))
                {
                    return result;
                }

                NetDataContractSerializer serializer =
                    new NetDataContractSerializer();

                //load instance data
                XElement xml = XElement.Load(fullPath);
                var entries =
                    (from e in xml.Element("InstanceData").Elements("Entry")
                     select e).ToList();
                foreach (XElement entry in entries)
                {
                    LoadSingleEntry(serializer, instanceData, entry);
                }

                //load instance metadata
                fileName = String.Format("{0}.meta.xml", instanceId);
                fullPath = Path.Combine(_dataDirectory, fileName);
                xml = XElement.Load(fullPath);
                entries =
                    (from e in xml.Element(
                         "InstanceMetadata").Elements("Entry")
                     select e).ToList();
                foreach (XElement entry in entries)
                {
                    LoadSingleEntry(serializer, instanceMetadata, entry);
                }

                result = true;
            }
            catch (IOException exception)
            {
                Console.WriteLine(
                    "LoadInstance Exception: {0}", exception.Message);
                throw exception;
            }
            return result;
        }

The LoadSingleEntry method deserializes a single data element. It is used to deserialize instance data and metadata elements. Notice that the InstanceValue options that were previously persisted with the data element are checked in this method. If the value of the option indicates that the data is WriteOnly, it is not loaded. Data that is flagged with the WriteOnly option should be persisted but not loaded.

        private void LoadSingleEntry(NetDataContractSerializer serializer,
            IDictionary<XName, InstanceValue> instanceData, XElement entry)
        {
            XName key =
                (XName)Deserialize(serializer, entry.Element("Key"));
            Object value =
                Deserialize(serializer, entry.Element("Value"));
            InstanceValue iv = new InstanceValue(value);
            InstanceValueOptions options =
                (InstanceValueOptions)Deserialize(
                    serializer, entry.Element("Options"));
            if (!options.HasFlag(InstanceValueOptions.WriteOnly))
            {
                instanceData.Add(key, iv);
            }
        }

        #endregion

        #region Delete Methods

The DeleteInstance method is used to remove the instance and metadata files for an instance. It is invoked when a workflow instance is completed and can be removed from the file system.

        public void DeleteInstance(Guid instanceId)
        {
            String fileName = String.Format("{0}.xml", instanceId);
            String fullPath = Path.Combine(_dataDirectory, fileName);
            if (File.Exists(fullPath))
            {
                File.Delete(fullPath);
            }

            fileName = String.Format("{0}.meta.xml", instanceId);
            fullPath = Path.Combine(_dataDirectory, fileName);
            if (File.Exists(fullPath))
            {
                File.Delete(fullPath);
            }
        }

        #endregion

        #region Association Methods

The SaveInstanceAssociation method saves a file that associates an instance key with an instance ID. The instance key represents a correlation key that is later used to look up the correct workflow instance ID. The association between the key and the instance ID is maintained by the file name itself. The first node of the name is the instance key, and the second node is the instance ID. This mechanism may be simple, but it does support multiple instance keys for an instance ID.

        public void SaveInstanceAssociation(Guid instanceId,
            Guid instanceKeyToAssociate, Boolean isDelete)
        {
            try
            {
                String fileName = String.Format("Key.{0}.{1}.xml",
                    instanceKeyToAssociate, instanceId);
                String fullPath = Path.Combine(_dataDirectory, fileName);
                if (!isDelete)
                {
                    if (!File.Exists(fullPath))
                    {
                        File.Create(fullPath);
                    }
                }
                else
                {
                    if (File.Exists(fullPath))
                    {
                        File.Delete(fullPath);
                    }
                }
            }
            catch (IOException exception)
            {
                Console.WriteLine(
                    "PersistInstanceAssociation Exception: {0}",
                    exception.Message);
                throw exception;
            }
        }

The GetInstanceAssociation method retrieves an instance ID based on an instance key that was provided. It does this by finding any files with the requested instance key and parsing the file name to determine the instance ID.

        public Guid GetInstanceAssociation(Guid instanceKey)
        {
            Guid instanceId = Guid.Empty;
            try
            {
                String[] files = Directory.GetFiles(_dataDirectory,
                    String.Format("Key.{0}.*.xml", instanceKey));
                if (files != null && files.Length > 0)
                {
                    String[] nodes = files[0].Split('.'),
                    if (nodes.Length == 4)
                    {
                        instanceId = Guid.Parse(nodes[2]);
                    }
                }
            }
            catch (IOException exception)
            {
                Console.WriteLine(
                    "GetInstanceAssociation Exception: {0}",
                    exception.Message);
                throw exception;
            }
            return instanceId;
        }

The DeleteInstanceAssociation method removes a specific instance key. This method is invoked when a workflow instance is completed.

        public void DeleteInstanceAssociation(Guid instanceKey)
        {
            try
            {
                String[] files = Directory.GetFiles(_dataDirectory,
                    String.Format("Key.*.{0}.xml", instanceKey));
                if (files != null && files.Length > 0)
                {
                    foreach (String file in files)
                    {
                        File.Delete(file);
                    }
                }
            }
            catch (IOException exception)
            {
                Console.WriteLine(
                    "DeleteInstanceAssociation Exception: {0}",
                    exception.Message);
                throw exception;
            }
        }

        #endregion

        #region Private methods

        private void CreateDataDirectory()
        {
            _dataDirectory = Path.Combine(
                Environment.CurrentDirectory, "InstanceStore");
            if (!Directory.Exists(_dataDirectory))
            {
                Directory.CreateDirectory(_dataDirectory);
            }
        }

        private XElement Serialize(NetDataContractSerializer serializer,
            XElement parent, String name, Object value)
        {
            XElement element = new XElement(name);
            using (MemoryStream stream = new MemoryStream())
            {
                serializer.Serialize(stream, value);
                stream.Position = 0;
                using (StreamReader reader = new StreamReader(stream))
                {
                    element.Add(XElement.Load(stream));
                }
            }
            parent.Add(element);
            return element;
        }

        private Object Deserialize(NetDataContractSerializer serializer,
            XElement element)
        {
            Object result = null;
            using (MemoryStream stream = new MemoryStream())
            {
                using (XmlDictionaryWriter writer =
                    XmlDictionaryWriter.CreateTextWriter(stream))
                {
                    foreach (XNode node in element.Nodes())
                    {
                        node.WriteTo(writer);
                    }

                    writer.Flush();
                    stream.Position = 0;
                    result = serializer.Deserialize(stream);
                }
            }
            return result;
        }

        #endregion
    }
}

You should rebuild the solution at this point to verify that all the code for the custom instance store builds correctly.

Modifying the ServiceHost Project

To test the new instance store, you can modify the ServiceHost project to load it instead of the SqlWorkflowInstanceStoreBehavior. You need to completely replace the existing CreateServiceHost method with a new implementation that uses the custom instance store instead of the SqlWorkflowInstanceStore. If you like, you can make a copy of the method under a different method name before you completely replace it. This allows you to later swap back to using the SqlWorkflowInstanceStore. Here are the changes that you need to make to the Program.cs file of the ServiceHost project:

namespace ServiceHost
{
    class Program
    {

        private static WorkflowServiceHost CreateServiceHost(
            String xamlxName, IItemSupport extension)
        {
            WorkflowService wfService = LoadService(xamlxName);
            WorkflowServiceHost host = new WorkflowServiceHost(wfService);

            InstanceStore store = new FileSystemInstanceStore();
            host.DurableInstancingOptions.InstanceStore = store;

            WorkflowIdleBehavior idleBehavior = new WorkflowIdleBehavior()
            {
                TimeToUnload = TimeSpan.FromSeconds(0)
            };
            host.Description.Behaviors.Add(idleBehavior);

            if (extension != null)
            {
                host.WorkflowExtensions.Add(extension);
            }

            _hosts.Add(host);

            return host;
        }

    }
}

Testing the Custom Instance Store

You can now rebuild the solution and run the ServiceHost and OrderEntryConsoleClient projects to test the new instance store. The observable results that you see from the client application should be consistent with your previous tests that used the SqlWorkflowInstanceStoreBehavior. Here is a representative set of results from the client application:


Commands: start | add | complete | query | cancel | exit

start

Enter an OrderId (int) for the order

1

New order 1 started

Commands: start | add | complete | query | cancel | exit

add

Enter ItemId and Quantity (Ex: 101 1)

101 1

Ordered 1 of ItemId 101 for OrderId 1


Commands: start | add | complete | query | cancel | exit

add

Enter ItemId and Quantity (Ex: 101 1)

202 2

Ordered 2 of ItemId 202 for OrderId 1


Commands: start | add | complete | query | cancel | exit

complete

Order 1 Is Completed


Ordered Items:

ItemId=101, Quantity=1, UnitPrice=1.23, Total=1.23

ItemId=202, Quantity=2, UnitPrice=2.34, Total=4.68


Commands: start | add | complete | query | cancel | exit

And here are the results from the ServiceHost using the new instance store:


Item inventory Before Execution:

ItemId=101, QtyAvailable=10

ItemId=202, QtyAvailable=20

ItemId=303, QtyAvailable=30


BeginTryCommand: CreateWorkflowOwnerCommand

Contract: IOrderEntry

    at http://localhost:9000/OrderEntry

Contract: IWorkflowInstanceManagement

    at http://localhost:9000/OrderEntryControl

Press any key to stop hosting and exit

BeginTryCommand: LoadWorkflowCommand

BeginTryCommand: SaveWorkflowCommand

BeginTryCommand: SaveWorkflowCommand

BeginTryCommand: LoadWorkflowByInstanceKeyCommand

Update: ItemId=101, QtyBefore=10, QtyAfter=9

BeginTryCommand: SaveWorkflowCommand

BeginTryCommand: LoadWorkflowByInstanceKeyCommand

Update: ItemId=202, QtyBefore=20, QtyAfter=18

BeginTryCommand: SaveWorkflowCommand

BeginTryCommand: LoadWorkflowByInstanceKeyCommand

BeginTryCommand: SaveWorkflowCommand

Item inventory After Execution:

ItemId=101, QtyAvailable=9

ItemId=202, QtyAvailable=18

ItemId=303, QtyAvailable=30


Closing services...

BeginTryCommand: DeleteWorkflowOwnerCommand

Services closed

The XML files that are maintained by the instance store are saved to the indebugInstanceStore folder under the ServiceHost project. This assumes that you are building and running a debug version of the project. If you take a look in this folder before you complete an order, you should see files similar to these:


4db8abc8-b7f6-40b4-910c-12e2970d1551.xml

4db8abc8-b7f6-40b4-910c-12e2970d1551.meta.xml

Key.07f41faa-52f0-ce5f-8f26-85b7e0299515.4db8abc8-b7f6-40b4-910c-12e2970d1551.xml

The first file is the instance data, the second is the metadata, and the third file is the association file for a correlation key. Once you complete an order, the custom instance store deletes these files.

Summary

The focus of this chapter was extending and customizing workflow persistence. The chapter built upon the examples that were first presented in Chapter 11.

The chapter included coverage of mechanisms to extend the standard SQL Server persistence that is supported by WF. Included was an example that used the PersistenceParticipant class to persist additional data when an instance was persisted. The ability to promote selected properties in order to make them externally queryable was also demonstrated.

Although it is not directly related to the topic of persistence, use of the WorkflowControlEndpoint was demonstrated. This endpoint supports the management of active workflow instances.

The chapter concluded with the implementation of a custom instance store. This instance store uses the file system for persistence rather than a database.

In the next chapter, you will learn how WF implements transaction support, compensation, and error handling.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.176.72