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:
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.
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.
The following explains activities that are relevant to fault and fault handling:
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.
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 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.
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.
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.
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.
-- 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
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.
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:
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):
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:
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
}
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