Fault Handling & Compensation

Summary

Fault Handling in Workflows

Fault handling in workflows is aysnchronous. This means that exceptions that are thrown in an activity (explicitly or implicitly) are caught by the workflow runtime engine and then scheduled in a queue to be handled at a later time.

Workflow exceptions are generally generated under the following circumstances:

  1. A .NET Framework exception thrown from the handlers of the code activity or code-beside of custom activities.
  2. An explicit exception thrown by the workflow using the ThrowActivity activity.
  3. Transactions in TransactionScopeActivity or CompensatableTransactionScopeActivity time out.
  4. An exception thrown from external code, such as libraries or components that are used in the workflow.

Catching Exceptions

Handling workflow exceptions is done by the FaultHandlerActivity. Each FaultHandlerActivity activity is associated with a .NET Framework exception type and further contains a set of activities that are executed if  the exception raised matches the exception type. In other words, FaultHanlderActivity is equivalent to the catch statement.

Note that the completion of the FaultHandlerActivity activity is never considered a successful completion of its associated activity. This means that while the FaultHandlerActivity activity is executing, the activity that threw the exception is put into a faulting state. When the FaultHandlerActivity activity has completed, the associated activity is put into the closed state.

Fault Handling and Compensation

The difference between fault handling and compensation is that compensation can only be performed on an activity that has successfully completed, not one that has thrown an exception and is in a faulting state; however, the CompensateActivity activity can be executed inside a FaultHandlerActivity activity that is associated with an activity that has thrown an exception.

Relevant activities

The following explains activities that are relevant to fault and fault handling:

ThrowActivity

The purpose of the ThrowActivity activity is to raise exceptions declaratively, in response to exceptional conditions in a workflow. ThrowActivity is functionally equivalent to a CodeActivity activity whose code-beside handler throws the indicated exception. ThrowActivity is typically used as follows:

// Define an exception object
private Exception _exInvalidProductID = new Exception("Product ID " + _nID + " is invalid" );

// Create a property to get/set the above exception object
public Exception InvalidIDExcpetion
{
    get {return _exInvalidProductID;}
    set {_exInvalidProductID = value;}
}

// The following code is often found in the Designer file:

ActivityBind activitybind1 = new System.Workflow.ComponentModel.ActivityBind();
activitybind1.Name = "SomeActivityName";
activitybind1.Path = "InvalidIDExcpetion";
//
this.throwActivity1 = new System.Workflow.ComponentModel.ThrowActivity();
this.throwActivity1.Name = "throwActivity1";
this.throwActivity1.SetBinding(ThrowActivity.FaultProperty, ((ActivityBind)(activitybind1)));

The execution of the throwActivity1 instance will throw the _exInvalidProductID Exception object in the .NET Framework sense.

FaultHandlerActivity

Use a FaultHandlerActivity activity to handle a specific exception type. You typically specify a type derived from Exception to catch any exception of that type. You can then specify a local variable to store the exception and make it available in code. To add fault handler activities, switch to the Workflow Exceptions view by selecting the Workflow | View Fault Handlers menu. The Workflow Exceptions designer typically looks like this showing the FaultHandlersActivity activity:
 

Here you can drop a one or more FaultHandlerActivity activities in the small rounded rectangle surrounded by the blue arrow buttons, i.e., in the FaultHandlersActivity activity. The following shows adding three FaultHandlerActivity activities and setting activities within the first fault handler:

For each dropped FaultHandlerActivity activity, the FaultType property in the Properties window can then be used to specify the a System.Exception-dervied type to catch exceptions from that type. In the following figure, the second FaultHandlerActivity activity is added but no child activities have been added yet:

Compensation

Compensation is the act of undoing any actions that were performed by a successfully completed activity because of an exception that occurred elsewhere in a workflow. Only activities that implement the ICompensatableActivity interface can be compensated. WWF provides two such compensatable activities: CompensatableTransactionScopeActivity and CompensatableSequenceActivity.

The CompensateActivity activity triggers the compensation of a completed activity that implements the ICompensatableActivity interface. You do not have to use a CompensateActivity activity if there is no other compensation code in an outer compensatable activity. The compensation code of any nested successfully-completed compensatable-activities is run automatically if there is an unhandled exception in the workflow. You should only use the CompensateActivity activity when you need something other than default compensation. Default compensation invokes compensation of all nested ICompensatableActivity children in the reverse order of their completion. If this ordering is not what you need, or you want to selectively invoke compensation of completed ICompensatableActivity children, you should use the CompensateActivity activity.

CompensatableSequenceActivity

Recall that a SequenceActivity is a set of child activities that are run according to a single defined ordering. The SequenceActivity is completed when the final child activity is finished. CompensatableSequenceActivity activity defines a compensatable version of the SequenceActivity activity.

CompensatableTransactionScopeActivity

Before discussing CompensatableTransactionScopeActivity, TransactionScopeActivity activity must be explained. With a TransactionScopeActivity activity, a new Transaction is started when this activity begins executing, and the transaction commits when the activity closes successfully. In other words, TransactionScopeActivity denotes a section of workflow which demarcates a transaction boundary.

A TransactionScopeActivity activity provides a convenient way to wrap .NET Framework System.Transactions, which automatically roll back their actions if an error occurs. The most important property is TransactionOptions which is used to set isolation level and time out duration. To tie all this together, CompensatableTransactionScopeActivity is a compensatable version of TransactionScopeActivity.

Persistence

The workflow runtime engine manages workflow execution and allows workflows to remain active for long periods of time and even survive computer restarts. This is accomplished using the persistence service of WWF. The workflow runtime engine uses a persistence service, if one is loaded in the runtime, to persist workflow state information. The conditions under which a workflow is persisted are:

If one of these conditions is met and a persistence service is added to the runtime engine, the runtime engine calls methods that are supplied by the persistence service to save state information about the workflow instance. Note that the workflow runtime engine determines when persistence should occur, but it is up to a persistence service to perform the necessary persistence operations.

You can add persistence service to the workflow runtime engine by calling AddService or by making an appropriate entry in the application configuration file. WWF provides the SqlWorkflowPersistenceService class, an out-of-box persistence service, which you can use as is or extend.

Note that persistence is built into the CompensatableTransactionScopeActivity activity, and a persistence point will occur automatically whenever the transaction completes. If you run a workflow which does not have a persistence service, the following error will be generated: "The workflow hosting environment does not have a persistence service as required by an operation on the workflow instance". Again, the solution is to add a persistence service to the workflow.

A possible scenario for deployment of WWF solutions is to create multiple host applications, each with a different set of services that are running on different desktop and server configurations. Under such a scenario, a requirement may be that some workflows that are defined in the solution can only execute on certain systems. The out-of-box services in WWF such as the SqlWorkflowPersistenceService service, do not support this kind of configuration. To control which workflow instances load on which systems, you must create a custom persistence service. For more information, see Creating Custom Persistence Services in MSDN.

Creating a persistence database

-- Create a persistence database
CREATE DATABASE WorkflowPersistenceStore

-- Execute C:\WINDOWS\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\EN, execute the following SQL Scripts:
exec SqlPersistenceService_Schema.sql
exec SqlPersistenceService_Logic.sql

Example

This example shows how to use fault handlers, persistence and compensation in a simple order-processing sequential workflow. The workflow is shown below:

The logic is simple: the client starts the workflow by passing it a product ID. If product is unavailable, a ThrowActivity (AbortOrder) is used to throw the ProductUnavailableException exception. If a product is available, a CompensatableTransactionScropeActivity (ProcessPayment) is used to withdraw money from the seller's account to the buyer's account, and then the product is shipped.

Fault Handling

ThrowActivity

When setting properties for the AbortOrder ThrowActivity activity, the relevant properties are Fault and FaultType. Note that there is no associated code-beside with a ThrowActivity activity. FaultType is used to specify the type of the exception to throw, in this case, ProductUnavailableException, which is just derived from System.Exception. The Fault property is then set to point to the UnavailableException property which sets/gets the ProductUnavailableException exception:

private ProductUnavailableException _exProductUnavailable = new ProductUnavailableException();

public ProductUnavailableException UnavailableException
{
    get { return _exProductUnavailable; }
    set { _exProductUnavailable = value; }
}

The AbortOrder ThrowActivity activity will throw the ProductUnavailableException exception, but who will handle this throw exception? Because the AbortOrder activity will abort the entire workflow, the exception will be handled by the workflow's fault handler, which is accessible by invoking the context menu of the workflow and selecting View Fault Handlers (also accessible from the Workflow menu item):

The above view shows that the workflow's fault handler consists of a single FaultHandlersActivity which can contain multiple FaultHandlerActivity activities, with each FaultHandlerActivity handling a specific exception. In this case, there is only one FaultHandlerActivity  named faultAbortOrder that is configured to handle the ProductUnavailableException exception. faultAbortOrder handles the ProductUnavailableException exception through the InformUser code activity:


Workflow fault handlers

The previous section showed how to use ThrowActivity and set a fault handler for it at the workflow-level. Fault handlers can also be set at the activity level. To see fault handlers at the activity-level select an activity, and from its context menu select View Fault Handlers menu item. The following shows activity-level fault handlers (FaultHandlersActivity) for the ifAvailable and ifNotAvailable activities:

As the figure above shows, a FaultHandlerActivity named faultHandlerShipping has been added to the fault handlers associated with the ifAvailable activity to capture exceptions within the ifAvailable activity. Properties for faultHandlerShipping are shown below and indicate that the faultHandlerShipping is used to handle exceptions of type ShippingException.

Within the faultHandlerShipping activity, you will note that a CompensateActivity activity named CompensatePayment is used. The purpose of this activity is to refund the customer by reversing the effect of the ProcessPayment activity (of type CompensatableTransactionScopeActivity):

Compensation

Recall that ProcessPayment activity is a CompensatableTransactionScopeActivity activity. And as the figure below shows, you can use the activity's context menu to view the Compensation Handler which will be invoked if the ProcessPayment activity needs to be compensated:

The compensation handler is shown below. It consists of a single CodeActivity activity to perform a customer refund:

Code

The code is organized as shown below:

There are three main files: Exceptions.cs, Workflow1.cs, and Window1.xaml.cs. These files are shown below

// Exceptions.cs
[Serializable]
public class ProductUnavailableException : Exception
{
    public ProductUnavailableException ()
    { /* Empty implementation */ }

    public ProductUnavailableException (string message) : base(message)
    { /* Empty implementation */ }

    public ProductUnavailableException (string message, Exception innerException) : base(message, innerException)
    { /* Empty implementation */ }

    protected ProductUnavailableException(SerializationInfo info, StreamingContext context) : base(info, context)
    { /* Empty implementation */ }
}

[Serializable]
public class ShippingException : Exception
{
    public ShippingException () : base()
    { /* Empty implementation */ }

    public ShippingException (string message) : base(message)
    { /* Empty implementation */ }

    public ShippingException (string message, Exception innerException) : base(message, innerException)
    { /* Empty implementation */ }

    protected ShippingException(SerializationInfo info, StreamingContext context) : base(info, context)
    { /* Empty implementation */ }
}

// Workflow1.cs
public sealed partial class Workflow1: SequentialWorkflowActivity
{
    #region Data members
    private int _nProductID = -1;
    private ProductUnavailableException _exProductUnavailable = new ProductUnavailableException();
    #endregion
    public Workflow1()
    {
        InitializeComponent();
    }

    #region Properties
    public int ProductID
    {
        get { return _nProductID; }
        set { _nProductID = value; }
    }

    public ProductUnavailableException UnavailableException
    {
        get { return _exProductUnavailable; }
        set { _exProductUnavailable = value; }
    }
    #endregion

    #region Handlers
    private void ReceiveOrderHandler(object sender, EventArgs e)
    {
        Trace.WriteLine("Received order");
    }

    private void CheckProductAvailability(object sender, ConditionalEventArgs e)
    {
        // Product ID 999 is assumed to be not avialable. If _nProductID is 999,
        // setting e.Result to false will cause ifAvailable branch to be executed
        Trace.WriteLine("Checking product availability for product " + _nProductID);
        e.Result = (_nProductID == 999)? false : true;
    }

    private void ShipProductHandler(object sender, EventArgs e)
    {
        Trace.WriteLine("Shipping order ...");

        // Throw exception to fail the shipping activity
        throw new InvalidOperationException("There was an error shipping the product");
    }

    private void TrasferMoneyHandler(object sender, EventArgs e)
    {
        Trace.WriteLine("Money withdrawn from customer account");
        Trace.WriteLine("Money added to company's account");
    }

    private void ProcessInvalidOrderHandler(object sender, EventArgs e)
    {
        Trace.WriteLine("Informing user that product is no longer available");
    }
    #endregion

    private void RefundHandler(object sender, EventArgs e)
    {
        Trace.WriteLine("Refunding Money");
    }

    private void InformUserHandler(object sender, EventArgs e)
    {
        Trace.WriteLine("Order aborted");
    }
}

// Window1.xaml.cs
public partial class Window1 : System.Windows.Window
{
    // This event is used to determine when a workflow has aborted/completed/terminated
   
private AutoResetEvent _eventWorkflowFinished = new AutoResetEvent(false);
    private WorkflowRuntime _wfr = null;
    public Window1()
    {
        InitializeComponent();
    }

    #region Event handlers
    private void Workflow_Handler(object sender, RoutedEventArgs args)
    {
        using (_wfr = new WorkflowRuntime())
        {
            try
            {
                // Add persistence service
               
string _strConnection = @"Data Source=asln-db03\ldf_dev;Initial Catalog=WorkflowPersistenceStore;User Id=sa;";

                _wfr.AddService(new SqlWorkflowPersistenceService(_strConnection));

                // Setup events
               
_wfr.WorkflowCompleted += new EventHandler<WorkflowCompletedEventArgs>(wf_WorkflowCompleted);
                _wfr.WorkflowTerminated += new EventHandler<WorkflowTerminatedEventArgs>(wf_WorkflowTerminated);
                _wfr.WorkflowAborted += new EventHandler<WorkflowEventArgs>(wf_WorkflowAborted);

                // Initialize workflow runtime
               
_wfr.StartRuntime();

                ProcessProduct(999);
                ProcessProduct(10);
            }
            finally
            {
                _wfr.StopRuntime();
            }
        }
    }

    void wf_WorkflowAborted(object sender, WorkflowEventArgs e)
    {
        Trace.WriteLine("Workflow aborted");
        _eventWorkflowFinished.Set();
    }

    void wf_WorkflowTerminated(object sender, WorkflowTerminatedEventArgs e)
    {
        Trace.WriteLine("Workflow terminated. Message: " + e.Exception.Message);
        _eventWorkflowFinished.Set();
    }

    void wf_WorkflowCompleted(object sender, WorkflowCompletedEventArgs e)
    {
        Trace.WriteLine("Workflow Completed successfully");
        _eventWorkflowFinished.Set();
    }
    #endregion

    #region Helpers
    private void ProcessProduct(int nID)
    {
        // Start work flow with product id = 999 (throws product not found exception)
       
Dictionary<string, object> _dictArgumentValues = new Dictionary<string, object>();
        _dictArgumentValues.Add("ProductID", nID);
        WorkflowInstance wfi = _wfr.CreateWorkflow(typeof(Workflow1), _dictArgumentValues);
        wfi.Start();

        // Wait until workflow is done
       
_eventWorkflowFinished.WaitOne();
    }

    #endregion
}

Output from example

Received order
Checking product availability for product 999
Order aborted
Workflow Completed successfully

Received order
Checking product availability for product 10
Money withdrawn from customer account
Money added to company's account
Shipping order ...
A first chance exception of type 'System.InvalidOperationException' occurred in FaultHandlingWorkflow.dll
Refunding Money
Workflow terminated. Message: There was an error shipping the product