Composing Windows Workflow Foundation with .NET Framework Task Parallel Library

At first glance Windows
Workflow
(WF) and .NET Task Parallel
Library
(TPL) may appear to be competing execution engines. Both emphasize
composing work and parallel execution. While the tools share some
characteristics, each tool is very different. TPL is a lower-level library for
managing class level workloads and WF is architected around Activity building
blocks with User Interface design features as well as work composition. In
fact, had TPL been available when WF was built the WF team could have seriously
considered building WF on top of TPL.

Even though WF was not built on TPL, there are TPL features
that can make running WF workflows easier. In self-hosted scenarios, for
example, a desktop application may be running a workflow. Code examples in
this article will demonstrate how TPL and WF can work together.

Why a Workflow and a Task?

WF was built for a variety of hosts. So, a developer is not
confined to a Server Host like, for example, AppFabric to run WF workflows. The WF Windows
Presentation Foundation
(WPF) based design experience means a developer can
build a design experience around a customizable feature inside, for example, a
desktop application.

Because WF includes a design experience as well as customizable
Activities; developers requiring application user customization find that
workflows are ideal. For example: an application that must perform custom
printing could include a set of Printing Activities. Application users could
mix and match the set of Print activities depending on the need.

Starting with .NET 4.0 Tasks underpin the Concurrency and
Asynchronous .NET
Framework
unit work standard. Tasks enable capabilities like Continuations. Tasks can loosely couple the result of a Task completion to some other
components in an application. For example, in the printing scenario above; a Task
can tie a User Interface update to a completed workflow.

As stated earlier, outside of using a server like AppFabric;
WF includes a number of options for running a workflow.

Self-Hosting WF

Unless a developer is self-hosting a Workflow Services
workflow; there are two techniques for invoking a workflow inside an
application.

WorkflowInvoker is the simplest technique. Code utilizing
the WorkflowInvoker appears below.

WorkflowInvoker.Invoke(new Workflow1());

The technique runs the workflow synchronously. Invoke
exits when the workflow completes. WorkflowInvoker is great for unit testing a
workflow. Most self-hosted applications will need more control over an
executing workflow. For example: most applications would probably require some
sort of workflow abort. A more powerful approach involves the
WorkflowApplication class.

WorkflowApplication

WorkflowApplication gives a developer almost complete
control over a running workflow. A developer can hook events like workflow
exceptions and completions. Workflows can be aborted and workflow exceptions
can be handled gracefully.

The sample code below leverages the WorkflowApplication
class along with a number of other classes.

enum WorkflowExecResult
{
    Success,
    Failure
}

sealed class WorkflowApplicationWrapper
{
    private WorkflowApplication _instance = null;
    private TaskCompletionSource _task = null;
    private WorkflowExecResult _result = WorkflowExecResult.Failure;
    private Activity _definition = null;

    public WorkflowApplicationWrapper(string fileName)
    {
        using (var fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read))
        {
            _definition = ActivityXamlServices.Load(fileStream);
        }

        _task = new TaskCompletionSource();
    }

    public void Run(CancellationToken token)
    {
        try
        {
            //create a new instance from this definition
            _instance = new WorkflowApplication(_definition);

            _instance.Aborted += this.OnAborted;
            _instance.Completed += this.OnCompleted;
            _instance.OnUnhandledException += this.OnException;

            token.Register(this.AbortWorkflow);

            _instance.Run();

        }
        catch (Exception ex)
        {
            Console.WriteLine("Workflow execution failed " + ex.Message);
            _result = WorkflowExecResult.Failure;
        }
    }

    private void AbortWorkflow()
    {
_instance.Abort("Workflow aborted!!");
    }


    public Task WorkflowTask
    {
        get { return _task.Task; }
    }

    private void OnAborted(WorkflowApplicationAbortedEventArgs args)
    {

    }

    private void OnCompleted(WorkflowApplicationCompletedEventArgs args)
    {
        _result = WorkflowExecResult.Success;

        _task.SetResult(_result);
    }

    private UnhandledExceptionAction OnException(WorkflowApplicationUnhandledExceptionEventArgs args)
    {
        _task.SetResult(_result);

        return UnhandledExceptionAction.Abort;
    }
}

In the code above; the interface to the WorkflowApplication
has been wrapped in a helper class. Code using the helper class appears below.

            WorkflowApplicationWrapper wf = null;

            for (var n = 0; n < 3; ++n)
            {
                wf = new WorkflowApplicationWrapper("Workflow1.xaml");
                var tokenSource = new CancellationTokenSource();

                wf.Run(tokenSource.Token);

                if (n == 2) { tokenSource.Cancel(); }
                else { wf.WorkflowTask.Wait(); }

                Console.WriteLine("Workflow completed " + wf.WorkflowTask.Result.ToString());

            }

WF workflows are Trees of Activity classes. WorkflowApplication requires a reference to the root of the workflow Tree.

Workflows can be compiled and included in a .NET assembly or
the workflow can be loaded directly from an XML file. Sticking with the
Printing scenario introduced earlier in the article, an Application user-generated
workflow would likely be saved in an XML format. ActivityXamlServices includes
methods to parse and load the workflow.

The WF runtime executes the workflow in a separate Thread.

Fundamental TPL classes called TaskCompletionSource and the
CancellationToken give a developer control over a Task class Result property.

TPL Classes

Using TaskCompletionSource and the appropriate WorkflowApplication
Event the sample code populates the Task.Result with a value. A complete
review of TaskCompletionSource is beyond the scope of this article, but there
are helpful resources at the end of the article.

The sample code demonstrates Waiting on a Task. However,
making a Task available to a developer gives a class consumer a range of
composition options. For example: Task.WaitAll could wait for a group of executing
workflows to complete and, as mentioned earlier, a Continuation can update a
component on the User Interface.

Aborting a workflow is handled by a CancellationToken
class
. When the CancellationTokenSource.Cancel is called a delegate registered
with the token aborts the workflow instance.

Conclusion

The Task class can improve self-hosted workflow execution. Tasks can be manipulated with the TaskCompletionSource and CancellationToken. Making a Task available to a component consumer gives a developer a range of composition options.

Resources

"Windows Workflow
Foundation 4 from the Inside Out
"

"The
Nature of TaskCompletionSource
"

"Using
WorkflowInvoker and WorkflowApplication
"

"Tasks
and the APM Pattern
"

"Understanding
.NET Framework Task Parallel Library Cancellations
"

More by Author

Must Read