Modeling Cancellation Behavior in Workflows

Activities can be canceled inside a workflow, for example by a Parallel activity canceling incomplete branches when its CompletionCondition evaluates to true, or from outside the workflow, if the host calls Cancel. To provide cancellation handling, workflow authors can use the CancellationScope activity, the CompensableActivity activity, or create custom activities that provide cancellation logic. This topic provides an overview of cancellation in workflows.

Cancellation, Compensation, and Transactions

Transactions give your application the ability to abort (roll back) all changes executed within the transaction if any errors occur during any part of the transaction process. However, not all work that may need to be canceled or undone is appropriate for transactions, such as long-running work or work that does not involve transactional resources. Compensation provides a model for undoing previously completed non-transactional work if there is a subsequent failure in the workflow. Cancellation provides a model for workflow and activity authors to handle non-transactional work that was not completed. If an activity has not completed its execution and it is canceled, its cancellation logic will be invoked if it is available.

Note

For more information about transactions and compensation, see Transactions and Compensation.

Using CancellationScope

The CancellationScope activity has two sections that can contain child activities: Body and CancellationHandler. The Body is where the activities that make up the logic of the activity are placed, and the CancellationHandler is where the activities that provide cancellation logic for the activity are placed. An activity can be canceled only if it has not completed. In the case of the CancellationScope activity, completion refers to the completion of the activities in the Body. If a cancellation request is scheduled and the activities in the Body have not completed, then the CancellationScope will be marked as Canceled and the CancellationHandler activities will be executed.

Canceling a Workflow from the Host

A host can cancel a workflow by calling the Cancel method of the WorkflowApplication instance that is hosting the workflow. In the following example a workflow is created that has a CancellationScope. The workflow is invoked, and then the host makes a call to Cancel. The main execution of the workflow is stopped, the CancellationHandler of the CancellationScope is invoked, and then the workflow completes with a status of Canceled.

Activity wf = new CancellationScope
{
    Body = new Sequence
    {
        Activities =
        {
            new WriteLine
            {
                Text = "Starting the workflow."
            },
            new Delay
            {
                Duration = TimeSpan.FromSeconds(5)
            },
            new WriteLine
            {
                Text = "Ending the workflow."
            }
        }
    },
    CancellationHandler = new WriteLine
    {
        Text = "CancellationHandler invoked."
    }
};

// Create a WorkflowApplication instance.
WorkflowApplication wfApp = new WorkflowApplication(wf);

// Subscribe to any desired workflow lifecycle events.
wfApp.Completed = delegate (WorkflowApplicationCompletedEventArgs e)
{
    if (e.CompletionState == ActivityInstanceState.Faulted)
    {
        Console.WriteLine("Workflow {0} Terminated.", e.InstanceId);
        Console.WriteLine("Exception: {0}\n{1}",
            e.TerminationException.GetType().FullName,
            e.TerminationException.Message);
    }
    else if (e.CompletionState == ActivityInstanceState.Canceled)
    {
        Console.WriteLine("Workflow {0} Canceled.", e.InstanceId);
    }
    else
    {
        Console.WriteLine("Workflow {0} Completed.", e.InstanceId);
    }
};

// Run the workflow.
wfApp.Run();

Thread.Sleep(TimeSpan.FromSeconds(1));

wfApp.Cancel();

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

Starting the workflow.
CancellationHandler invoked. Workflow b30ebb30-df46-4d90-a211-e31c38d8db3c Canceled.

Note

When a CancellationScope activity is canceled and the CancellationHandler invoked, it is the responsibility of the workflow author to determine the progress that the canceled activity made before it was canceled in order to provide the appropriate cancellation logic. The CancellationHandler does not provide any information about the progress of the canceled activity.

A workflow can also be canceled from the host if an unhandled exception bubbles up past the root of the workflow and the OnUnhandledException handler returns Cancel. In this example the workflow starts and then throws an ApplicationException. This exception is unhandled by the workflow and so the OnUnhandledException handler is invoked. The handler instructs the runtime to cancel the workflow, and the CancellationHandler of the currently executing CancellationScope activity is invoked.

Activity wf = new CancellationScope
{
    Body = new Sequence
    {
        Activities =
        {
            new WriteLine
            {
                Text = "Starting the workflow."
            },
            new Throw
            {
                 Exception = new InArgument<Exception>((env) =>
                     new ApplicationException("An ApplicationException was thrown."))
            },
            new WriteLine
            {
                Text = "Ending the workflow."
            }
        }
    },
    CancellationHandler = new WriteLine
    {
        Text = "CancellationHandler invoked."
    }
};

// Create a WorkflowApplication instance.
WorkflowApplication wfApp = new WorkflowApplication(wf);

// Subscribe to any desired workflow lifecycle events.
wfApp.OnUnhandledException = delegate (WorkflowApplicationUnhandledExceptionEventArgs e)
{
    // Display the unhandled exception.
    Console.WriteLine("OnUnhandledException in Workflow {0}\n{1}",
        e.InstanceId, e.UnhandledException.Message);

    // Instruct the runtime to cancel the workflow.
    return UnhandledExceptionAction.Cancel;
};

wfApp.Completed = delegate (WorkflowApplicationCompletedEventArgs e)
{
    if (e.CompletionState == ActivityInstanceState.Faulted)
    {
        Console.WriteLine("Workflow {0} Terminated.", e.InstanceId);
        Console.WriteLine("Exception: {0}\n{1}",
            e.TerminationException.GetType().FullName,
            e.TerminationException.Message);
    }
    else if (e.CompletionState == ActivityInstanceState.Canceled)
    {
        Console.WriteLine("Workflow {0} Canceled.", e.InstanceId);
    }
    else
    {
        Console.WriteLine("Workflow {0} Completed.", e.InstanceId);
    }
};

// Run the workflow.
wfApp.Run();

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

Starting the workflow.
OnUnhandledException in Workflow 6bb2d5d6-f49a-4c6d-a988-478afb86dbe9 An ApplicationException was thrown. CancellationHandler invoked. Workflow 6bb2d5d6-f49a-4c6d-a988-478afb86dbe9 Canceled.

Canceling an Activity from Inside a Workflow

An activity can also be canceled by its parent. For example, if a Parallel activity has multiple executing branches and its CompletionCondition evaluates to true then its incomplete branches will be canceled. In this example a Parallel activity is created that has two branches. Its CompletionCondition is set to true so the Parallel completes as soon as any one of its branches is completed. In this example branch 2 completes, and so branch 1 is canceled.

Activity wf = new Parallel
{
    CompletionCondition = true,
    Branches =
    {
        new CancellationScope
        {
            Body = new Sequence
            {
                Activities =
                {
                    new WriteLine
                    {
                        Text = "Branch 1 starting."
                    },
                    new Delay
                    {
                         Duration = TimeSpan.FromSeconds(2)
                    },
                    new WriteLine
                    {
                        Text = "Branch 1 complete."
                    }
                }
            },
            CancellationHandler = new WriteLine
            {
                Text = "Branch 1 canceled."
            }
        },
        new WriteLine
        {
            Text = "Branch 2 complete."
        }
    }
};

// Create a WorkflowApplication instance.
WorkflowApplication wfApp = new WorkflowApplication(wf);

wfApp.Completed = delegate (WorkflowApplicationCompletedEventArgs e)
{
    if (e.CompletionState == ActivityInstanceState.Faulted)
    {
        Console.WriteLine("Workflow {0} Terminated.", e.InstanceId);
        Console.WriteLine("Exception: {0}\n{1}",
            e.TerminationException.GetType().FullName,
            e.TerminationException.Message);
    }
    else if (e.CompletionState == ActivityInstanceState.Canceled)
    {
        Console.WriteLine("Workflow {0} Canceled.", e.InstanceId);
    }
    else
    {
        Console.WriteLine("Workflow {0} Completed.", e.InstanceId);
    }
};

// Run the workflow.
wfApp.Run();

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

Branch 1 starting.
Branch 2 complete. Branch 1 canceled. Workflow e0685e24-18ef-4a47-acf3-5c638732f3be Completed. Activities are also canceled if an exception bubbles up past the root of the activity but is handled at a higher level in the workflow. In this example, the main logic of the workflow consists of a Sequence activity. The Sequence is specified as the Body of a CancellationScope activity which is contained by a TryCatch activity. An exception is thrown from the body of the Sequence, is handled by the parent TryCatch activity, and the Sequence is canceled.

Activity wf = new TryCatch
{
    Try = new CancellationScope
    {
        Body = new Sequence
        {
            Activities =
            {
                new WriteLine
                {
                    Text = "Sequence starting."
                },
                new Throw
                {
                     Exception = new InArgument<Exception>((env) =>
                         new ApplicationException("An ApplicationException was thrown."))
                },
                new WriteLine
                {
                    Text = "Sequence complete."
                }
            }
        },
        CancellationHandler = new WriteLine
        {
            Text = "Sequence canceled."
        }
    },
    Catches =
    {
        new Catch<ApplicationException>
        {
            Action = new ActivityAction<ApplicationException>
            {
                Handler  = new WriteLine
                {
                    Text = "Exception caught."
                }
            }
        }
    }
};

// Create a WorkflowApplication instance.
WorkflowApplication wfApp = new WorkflowApplication(wf);

wfApp.Completed = delegate (WorkflowApplicationCompletedEventArgs e)
{
    if (e.CompletionState == ActivityInstanceState.Faulted)
    {
        Console.WriteLine("Workflow {0} Terminated.", e.InstanceId);
        Console.WriteLine("Exception: {0}\n{1}",
            e.TerminationException.GetType().FullName,
            e.TerminationException.Message);
    }
    else if (e.CompletionState == ActivityInstanceState.Canceled)
    {
        Console.WriteLine("Workflow {0} Canceled.", e.InstanceId);
    }
    else
    {
        Console.WriteLine("Workflow {0} Completed.", e.InstanceId);
    }
};

// Run the workflow.
wfApp.Run();

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

Sequence starting.
Sequence canceled. Exception caught. Workflow e3c18939-121e-4c43-af1c-ba1ce977ce55 Completed.

Throwing Exceptions from a CancellationHandler

Any exceptions thrown from the CancellationHandler of a CancellationScope are fatal to the workflow. If there is a possibility of exceptions escaping from a CancellationHandler, use a TryCatch in the CancellationHandler to catch and handle these exceptions.

Cancellation using CompensableActivity

Like the CancellationScope activity, the CompensableActivity has a CancellationHandler. If a CompensableActivity is canceled, any activities in its CancellationHandler are invoked. This can be useful for undoing partially completed compensable work. For information about how to use CompensableActivity for compensation and cancellation, see Compensation.

Cancellation using Custom Activities

Custom activity authors can implement cancellation logic into their custom activities in several different ways. Custom activities that derive from Activity can implement cancellation logic by placing a CancellationScope or other custom activity that contains cancellation logic in the body of the activity. AsyncCodeActivity and NativeActivity derived activities can override their respective Cancel method and provide cancellation logic there. CodeActivity derived activities do not provide any provision for cancellation because all their work is performed in a single burst of execution when the runtime calls the Execute method. If the execute method has not yet been called and a CodeActivity based activity is canceled, the activity closes with a status of Canceled and the Execute method is not called.

Cancellation using NativeActivity

NativeActivity derived activities can override the Cancel method to provide custom cancellation logic. If this method is not overridden, then the default workflow cancellation logic is applied. Default cancellation is the process that occurs for a NativeActivity that does not override the Cancel method or whose Cancel method calls the base NativeActivity Cancel method. When an activity is canceled, the runtime flags the activity for cancellation and automatically handles certain cleanup. If the activity only has outstanding bookmarks, the bookmarks will be removed and the activity will be marked as Canceled. Any outstanding child activities of the canceled activity will in turn be canceled. Any attempt to schedule additional child activities will result in the attempt being ignored and the activity will be marked as Canceled. If any outstanding child activity completes in the Canceled or Faulted state, then the activity will be marked as Canceled. Note that a cancellation request can be ignored. If an activity does not have any outstanding bookmarks or executing child activities and does not schedule any additional work items after being flagged for cancellation, it will complete successfully. This default cancellation suffices for many scenarios, but if additional cancellation logic is needed, then the built-in cancellation activities or custom activities can be used.

In the following example, the Cancel override of a NativeActivity based custom ParallelForEach activity is defined. When the activity is canceled, this override handles the cancellation logic for the activity. This example is part of the Non-Generic ParallelForEach sample.

protected override void Cancel(NativeActivityContext context)  
{  
    // If we do not have a completion condition then we can just  
    // use default logic.  
    if (this.CompletionCondition == null)  
    {  
        base.Cancel(context);  
    }  
    else  
    {  
        context.CancelChildren();  
    }  
}  

NativeActivity derived activities can determine if cancellation has been requested by inspecting the IsCancellationRequested property, and mark themselves as canceled by calling the MarkCanceled method. Calling MarkCanceled does not immediately complete the activity. As usual, the runtime will complete the activity when it has no more outstanding work, but if MarkCanceled is called the final state will be Canceled instead of Closed.

Cancellation using AsyncCodeActivity

AsyncCodeActivity based activities can also provide custom cancellation logic by overriding the Cancel method. If this method is not overridden, then no cancellation handling is performed if the activity is canceled. In the following example, the Cancel override of an AsyncCodeActivity based custom ExecutePowerShell activity is defined. When the activity is canceled, it performs the desired cancellation behavior.

// Called by the runtime to cancel the execution of this asynchronous activity.
protected override void Cancel(AsyncCodeActivityContext context)
{
    Pipeline pipeline = context.UserState as Pipeline;
    if (pipeline != null)
    {
        pipeline.Stop();
        DisposePipeline(pipeline);
    }
    base.Cancel(context);
}

AsyncCodeActivity derived activities can determine if cancellation has been requested by inspecting the IsCancellationRequested property, and mark themselves as canceled by calling the MarkCanceled method.