Compensation

This topic applies to Windows Workflow Foundation 4 (WF4).

Compensation in Windows Workflow Foundation (WF) is the mechanism by which previously completed work can be undone or compensated (following the logic defined by the application) when a subsequent failure occurs. This section describes how to use compensation in workflows.

Compensation vs. Transactions

A transaction allows you to combine multiple operations into a single unit of work. Using a transaction gives your application the ability to abort (roll back) all changes executed from within the transaction if any errors occur during any part of the transaction process. However, using transactions may not be appropriate if the work is long running. For example, a travel planning application is implemented as a workflow. The steps of the workflow may consist of booking a flight, waiting for manager approval, and then paying for the flight. This process could take many days and it is not practical for the steps of booking and paying for the flight to participate in the same transaction. In a scenario such as this, compensation could be used to undo the booking step of the workflow if there is a failure later in the processing.

Note

This topic covers compensation in workflows. For more information about transactions in workflows, see Workflow Transactions and TransactionScope. For more information about transactions, see System.Transactions and System.Transactions.Transaction.

Using CompensableActivity

CompensableActivity is the core compensation activity in WF. Any activities that perform work that may need to be compensated are placed into the Body of a CompensableActivity. In this example, the reservation step of purchasing a flight is placed into the Body of a CompensableActivity and the cancellation of the reservation is placed into the CompensationHandler. Immediately following the CompensableActivity in the workflow are two activities that wait for manager approval and then complete the purchasing step of the flight. If an error condition causes the workflow to be canceled after the CompensableActivity has successfully completed, then the activities in the CompensationHandler handler are scheduled and the flight is canceled.

Activity wf = new Sequence()
{
    Activities =
    {
        new CompensableActivity
        {
            Body = new ReserveFlight(),
            CompensationHandler = new CancelFlight()
        },
        new ManagerApproval(),
        new PurchaseFlight()
    }
};

The following example is the workflow in XAML.

<Sequence
   xmlns="https://schemas.microsoft.com/netfx/2009/xaml/activities"
   xmlns:c="clr-namespace:CompensationExample;assembly=CompensationExample"
   xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <CompensableActivity>
    <CompensableActivity.Result>
      <OutArgument
         x:TypeArguments="CompensationToken" />
    </CompensableActivity.Result>
    <CompensableActivity.CompensationHandler>
      <c:CancelFlight />
    </CompensableActivity.CompensationHandler>
    <c:ReserveFlight />
  </CompensableActivity>
  <c:ManagerApproval />
  <c:PurchaseFlight />
</Sequence>

When the workflow is invoked, the following output is displayed to the console.

ReserveFlight: Ticket is reserved. 
ManagerApproval: Manager approval received.
PurchaseFlight: Ticket is purchased.
Workflow completed successfully with status: Closed.

Note

The sample activities in this topic such as ReserveFlight display their name and purpose to the console to help illustrate the order in which the activities are executed when compensation occurs.

Default Workflow Compensation

By default, if the workflow is canceled, the compensation logic is run for any compensable activity that has successfully completely and has not already been confirmed or compensated.

Note

When a CompensableActivity is confirmed, compensation for the activity can no longer be invoked. The confirmation process is described later in this section.

In this example, an exception is thrown after the flight is reserved but before the manager approval step.

Activity wf = new Sequence()
{
    Activities =
    {
        new CompensableActivity
        {
            Body = new ReserveFlight(),
            CompensationHandler = new CancelFlight()
        },
        new SimulatedErrorCondition(),
        new ManagerApproval(),
        new PurchaseFlight()
    }
};

This example is the workflow in XAML.

<Sequence
   xmlns="https://schemas.microsoft.com/netfx/2009/xaml/activities"
   xmlns:c="clr-namespace:CompensationExample;assembly=CompensationExample"
   xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <CompensableActivity>
    <CompensableActivity.Result>
      <OutArgument
         x:TypeArguments="CompensationToken" />
    </CompensableActivity.Result>
    <CompensableActivity.CompensationHandler>
      <c:CancelFlight />
    </CompensableActivity.CompensationHandler>
    <c:ReserveFlight />
  </CompensableActivity>
  <c:SimulatedErrorCondition />
  <c:ManagerApproval />
  <c:PurchaseFlight />
</Sequence>
AutoResetEvent syncEvent = new AutoResetEvent(false);
WorkflowApplication wfApp = new WorkflowApplication(wf);

wfApp.Completed = delegate(WorkflowApplicationCompletedEventArgs e)
{
    if (e.TerminationException != null)
    {
        Console.WriteLine("Workflow terminated with exception:\n{0}: {1}",
            e.TerminationException.GetType().FullName,
            e.TerminationException.Message);
    }
    else
    {
        Console.WriteLine("Workflow completed successfully with status: {0}.",
            e.CompletionState);
    }

    syncEvent.Set();
};

wfApp.OnUnhandledException = delegate(WorkflowApplicationUnhandledExceptionEventArgs e)
{
    Console.WriteLine("Workflow Unhandled Exception:\n{0}: {1}",
        e.UnhandledException.GetType().FullName,
        e.UnhandledException.Message);

    return UnhandledExceptionAction.Cancel;
};

wfApp.Run();
syncEvent.WaitOne();

When the workflow is invoked, the simulated error condition exception is handled by the host application in OnUnhandledException, the workflow is canceled, and the compensation logic is invoked.

ReserveFlight: Ticket is reserved. 
SimulatedErrorCondition: Throwing an ApplicationException.
Workflow Unhandled Exception:
System.ApplicationException: Simulated error condition in the workflow.
CancelFlight: Ticket is canceled.
Workflow completed successfully with status: Canceled.

Cancellation and CompensableActivity

If the activities in the Body of a CompensableActivity have not completed and the activity is canceled, the activities in the CancellationHandler are executed.

Note

The CancellationHandler is only invoked if the activities in the Body of the CompensableActivity have not completed and the activity is canceled. The CompensationHandler is only executed if the activities in the Body of the CompensableActivity have successfully completed and compensation is subsequently invoked on the activity.

The CancellationHandler gives workflow authors the opportunity to provide any appropriate cancellation logic. In the following example, an exception is thrown during the execution of the Body, and then the CancellationHandler is invoked.

Activity wf = new Sequence()
{
    Activities =
    {
        new CompensableActivity
        {
            Body = new Sequence
            {
                Activities = 
                {
                    new ChargeCreditCard(),
                    new SimulatedErrorCondition(),
                    new ReserveFlight()
                }
            },
            CompensationHandler = new CancelFlight(),
            CancellationHandler = new CancelCreditCard()
        },
        new ManagerApproval(),
        new PurchaseFlight()
    }
};

This example is the workflow in XAML

<Sequence
   xmlns="https://schemas.microsoft.com/netfx/2009/xaml/activities"
   xmlns:c="clr-namespace:CompensationExample;assembly=CompensationExample"
   xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <CompensableActivity>
    <CompensableActivity.Result>
      <OutArgument
         x:TypeArguments="CompensationToken" />
    </CompensableActivity.Result>
    <Sequence>
      <c:ChargeCreditCard />
      <c:SimulatedErrorCondition />
      <c:ReserveFlight />
    </Sequence>
    <CompensableActivity.CancellationHandler>
      <c:CancelCreditCard />
    </CompensableActivity.CancellationHandler>
    <CompensableActivity.CompensationHandler>
      <c:CancelFlight />
    </CompensableActivity.CompensationHandler>
  </CompensableActivity>
  <c:ManagerApproval />
  <c:PurchaseFlight />
</Sequence>

When the workflow is invoked, the simulated error condition exception is handled by the host application in OnUnhandledException, the workflow is canceled, and the cancellation logic of the CompensableActivity is invoked. In this example, the compensation logic and the cancellation logic have different goals. If the Body completed successfully, then this means the credit card was charged and the flight booked, so the compensation should undo both steps. (In this example, canceling the flight automatically cancels the credit card charges.) However, if the CompensableActivity is canceled, this means the Body did not complete and so the logic of the CancellationHandler needs to be able to determine how to best handle the cancellation. In this example, the CancellationHandler cancels the credit card charge, but since ReserveFlight was the last activity in the Body, it does not attempt to cancel the flight. Since ReserveFlight was the last activity in the Body, if it had successfully completed then the Body would have completed and no cancellation would be possible.

ChargeCreditCard: Charge credit card for flight. 
SimulatedErrorCondition: Throwing an ApplicationException.
Workflow Unhandled Exception:
System.ApplicationException: Simulated error condition in the workflow.
CancelCreditCard: Cancel credit card charges.
Workflow completed successfully with status: Canceled.

For more information about cancellation, see Modeling Cancellation Behavior in Workflows.

Explicit Compensation Using the Compensate Activity

In the previous section, implicit compensation was covered. Implicit compensation can be appropriate for simple scenarios, but if more explicit control is required over the scheduling of compensation handling then the Compensate activity can be used. To initiate the compensation process with the Compensate activity, the CompensationToken of the CompensableActivity for which compensation is desired is used. The Compensate activity can be used to initiate compensation on any completed CompensableActivity that has not been confirmed or compensated. For example, a Compensate activity could be used in the Catches section of a TryCatch activity, or any time after the CompensableActivity has completed. In this example, the Compensate activity is used in the Catches section of a TryCatch activity to reverse the action of the CompensableActivity.

Variable<CompensationToken> token1 = new Variable<CompensationToken>
{
    Name = "token1",
};

Activity wf = new TryCatch()
{
    Variables = 
    {
        token1
    },
    Try = new Sequence
    {
        Activities =
        {
            new CompensableActivity
            {
                Body = new ReserveFlight(),
                CompensationHandler = new CancelFlight(),
                ConfirmationHandler = new ConfirmFlight(),
                Result = token1
            },
            new SimulatedErrorCondition(),
            new ManagerApproval(),
            new PurchaseFlight()
        }
    },
    Catches =
    {
        new Catch<ApplicationException>()
        {
            Action = new ActivityAction<ApplicationException>()
            {
                Handler = new Compensate()
                {
                    Target = token1
                }
            }
        }
    }
};

This example is the workflow in XAML.

<TryCatch
   xmlns="https://schemas.microsoft.com/netfx/2009/xaml/activities"
   xmlns:c="clr-namespace:CompensationExample;assembly=CompensationExample"
   xmlns:s="clr-namespace:System;assembly=mscorlib"
   xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <TryCatch.Variables>
    <Variable
       x:TypeArguments="CompensationToken"
       x:Name="__ReferenceID0"
       Name="token1" />
  </TryCatch.Variables>
  <TryCatch.Try>
    <Sequence>
      <CompensableActivity>
        <CompensableActivity.Result>
          <OutArgument
             x:TypeArguments="CompensationToken">
            <VariableReference
               x:TypeArguments="CompensationToken"
               Variable="{x:Reference __ReferenceID0}">
              <VariableReference.Result>
                <OutArgument
                   x:TypeArguments="Location(CompensationToken)" />
              </VariableReference.Result>
            </VariableReference>
          </OutArgument>
        </CompensableActivity.Result>
        <CompensableActivity.CompensationHandler>
          <c:CancelFlight />
        </CompensableActivity.CompensationHandler>
        <CompensableActivity.ConfirmationHandler>
          <c:ConfirmFlight />
        </CompensableActivity.ConfirmationHandler>
        <c:ReserveFlight />
      </CompensableActivity>
      <c:SimulatedErrorCondition />
      <c:ManagerApproval />
      <c:PurchaseFlight />
    </Sequence>
  </TryCatch.Try>
  <TryCatch.Catches>
    <Catch
       x:TypeArguments="s:ApplicationException">
      <ActivityAction
         x:TypeArguments="s:ApplicationException">
        <Compensate>
          <Compensate.Target>
            <InArgument
               x:TypeArguments="CompensationToken">
              <VariableValue
                 x:TypeArguments="CompensationToken"
                 Variable="{x:Reference __ReferenceID0}">
                <VariableValue.Result>
                  <OutArgument
                     x:TypeArguments="CompensationToken" />
                </VariableValue.Result>
              </VariableValue>
            </InArgument>
          </Compensate.Target>
        </Compensate>
      </ActivityAction>
    </Catch>
  </TryCatch.Catches>
</TryCatch>

When the workflow is invoked, the following output is displayed to the console.

ReserveFlight: Ticket is reserved. 
SimulatedErrorCondition: Throwing an ApplicationException.
CancelFlight: Ticket is canceled.
Workflow completed successfully with status: Closed.

Confirming Compensation

By default, compensable activities can be compensated any time after they have completed. In some scenarios this may not be appropriate. In the previous example the compensation to reserving the ticket was to cancel the reservation. However, after the flight has been completed this compensation step is no longer valid. Confirming the compensable activity invokes the activity specified by the ConfirmationHandler. One possible use for this is to allow any resources that are necessary to perform the compensation to be released. After a compensable activity is confirmed it is not possible for it to be compensated, and if this is attempted an InvalidOperationException exception is thrown. When a workflow completes successfully, all non-confirmed and non-compensated compensable activities that completed successfully are confirmed in reverse order of completion. In this example the flight is reserved, purchased, and completed, and then the compensable activity is confirmed. To confirm a CompensableActivity, use the Confirm activity and specify the CompensationToken of the CompensableActivity to confirm.

Variable<CompensationToken> token1 = new Variable<CompensationToken>
{
    Name = "token1",
};

Activity wf = new Sequence()
{
    Variables = 
    {
        token1
    },
    Activities =
    {
        new CompensableActivity
        {
            Body = new ReserveFlight(),
            CompensationHandler = new CancelFlight(),
            ConfirmationHandler = new ConfirmFlight(),
            Result = token1
        },
        new ManagerApproval(),
        new PurchaseFlight(),
        new TakeFlight(),
        new Confirm()
        {
            Target = token1
        }
    }
};

This example is the workflow in XAML.

<Sequence
   xmlns="https://schemas.microsoft.com/netfx/2009/xaml/activities"
   xmlns:c="clr-namespace:CompensationExample;assembly=CompensationExample"
   xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
  <Sequence.Variables>
    <x:Reference>__ReferenceID0</x:Reference>
  </Sequence.Variables>
  <CompensableActivity>
    <CompensableActivity.Result>
      <OutArgument
         x:TypeArguments="CompensationToken">
        <VariableReference
           x:TypeArguments="CompensationToken">
          <VariableReference.Result>
            <OutArgument
               x:TypeArguments="Location(CompensationToken)" />
          </VariableReference.Result>
          <VariableReference.Variable>
            <Variable
               x:TypeArguments="CompensationToken"
               x:Name="__ReferenceID0"
               Name="token1" />
          </VariableReference.Variable>
        </VariableReference>
      </OutArgument>
    </CompensableActivity.Result>
    <CompensableActivity.CompensationHandler>
      <c:CancelFlight />
    </CompensableActivity.CompensationHandler>
    <CompensableActivity.ConfirmationHandler>
      <c:ConfirmFlight />
    </CompensableActivity.ConfirmationHandler>
    <c:ReserveFlight />
  </CompensableActivity>
  <c:ManagerApproval />
  <c:PurchaseFlight />
  <c:TakeFlight />
  <Confirm>
    <Confirm.Target>
      <InArgument
         x:TypeArguments="CompensationToken">
        <VariableValue
           x:TypeArguments="CompensationToken"
           Variable="{x:Reference __ReferenceID0}">
          <VariableValue.Result>
            <OutArgument
               x:TypeArguments="CompensationToken" />
          </VariableValue.Result>
        </VariableValue>
      </InArgument>
    </Confirm.Target>
  </Confirm>
</Sequence>

When the workflow is invoked, the following output is displayed to the console.

ReserveFlight: Ticket is reserved. 
ManagerApproval: Manager approval received.
PurchaseFlight: Ticket is purchased.
TakeFlight: Flight is completed.
ConfirmFlight: Flight has been taken, no compensation possible.
Workflow completed successfully with status: Closed.

A CompensableActivity that completes successfully is always subsequently compensated or confirmed. This compensation/confirmation can happen explicitly (via Compensate or Confirm activities) or implicitly when a workflow completes successfully, in which case all successfully completed compensable activities within the workflow are confirmed

Nesting Compensation Activities

A CompensableActivity can be placed into the Body section of another CompensableActivity. A CompensableActivity may not be placed into a handler of another CompensableActivity. It is the responsibility of a parent CompensableActivity to ensure that when it is canceled, confirmed, or compensated, all child compensable activities that have completed successfully and have not already been confirmed or compensated must be confirmed or compensated before the parent completes cancellation, confirmation, or compensation. If this is not modeled explicitly the parent CompensableActivity will implicitly compensate child compensable activities if the parent received the cancel or compensate signal. If the parent received the confirm signal the parent will implicitly confirm child compensable activities. If the logic to handle cancellation, confirmation, or compensation is explicitly modeled in the handler of the parent CompensableActivity, any child not explicitly handled will be implicitly confirmed.

See Also

Tasks

Compensable Activity Sample

Reference

CompensableActivity
Compensate
Confirm
CompensationToken