Creating Asynchronous Activities in WF

AsyncCodeActivity provides activity authors a base class to use that enables derived activities to implement asynchronous execution logic. This is useful for custom activities that must perform asynchronous work without holding the workflow scheduler thread and blocking any activities that may be able to run in parallel. This topic provides an overview of how to create custom asynchronous activities using AsyncCodeActivity.

Using AsyncCodeActivity

System.Activities provides custom activity authors with different base classes for different activity authoring requirements. Each one carries a particular semantic and provides a workflow author (and the activity runtime) a corresponding contract. An AsyncCodeActivity based activity is an activity that performs work asynchronously relative to the scheduler thread and whose execution logic is expressed in managed code. As a result of going asynchronous, an AsyncCodeActivity may induce an idle point during execution. Due to the volatile nature of asynchronous work, an AsyncCodeActivity always creates a no persist block for the duration of the activity's execution. This prevents the workflow runtime from persisting the workflow instance in the middle of the asynchronous work, and also prevents the workflow instance from unloading while the asynchronous code is executing.

AsyncCodeActivity Methods

Activities that derive from AsyncCodeActivity can create asynchronous execution logic by overriding the BeginExecute and EndExecute methods with custom code. When called by the runtime, these methods are passed an AsyncCodeActivityContext. AsyncCodeActivityContext allows the activity author to provide shared state across BeginExecute/ EndExecute in the context's UserState property. In the following example, a GenerateRandom activity generates a random number asynchronously.

public sealed class GenerateRandom : AsyncCodeActivity<int>
{
    static Random r = new Random();

    protected override IAsyncResult BeginExecute(AsyncCodeActivityContext context, AsyncCallback callback, object state)
    {
        // Create a delegate that references the method that implements
        // the asynchronous work. Assign the delegate to the UserState,
        // invoke the delegate, and return the resulting IAsyncResult.
        Func<int> GetRandomDelegate = new Func<int>(GetRandom);
        context.UserState = GetRandomDelegate;
        return GetRandomDelegate.BeginInvoke(callback, state);
    }

    protected override int EndExecute(AsyncCodeActivityContext context, IAsyncResult result)
    {
        // Get the delegate from the UserState and call EndInvoke
        Func<int> GetRandomDelegate = (Func<int>)context.UserState;
        return (int)GetRandomDelegate.EndInvoke(result);
    }

    int GetRandom()
    {
        // This activity simulates taking a few moments
        // to generate the random number. This code runs
        // asynchronously with respect to the workflow thread.
        Thread.Sleep(5000);

        return r.Next(1, 101);
    }
}

The previous example activity derives from AsyncCodeActivity<TResult>, and has an elevated OutArgument<int> named Result. The value returned by the GetRandom method is extracted and returned by the EndExecute override, and this value is set as the Result value. Asynchronous activities that do not return a result should derive from AsyncCodeActivity. In the following example, a DisplayRandom activity is defined which derives from AsyncCodeActivity. This activity is like the GetRandom activity but instead of returning a result it displays a message to the console.

public sealed class DisplayRandom : AsyncCodeActivity
{
    static Random r = new Random();

    protected override IAsyncResult BeginExecute(AsyncCodeActivityContext context, AsyncCallback callback, object state)
    {
        // Create a delegate that references the method that implements
        // the asynchronous work. Assign the delegate to the UserState,
        // invoke the delegate, and return the resulting IAsyncResult.
        Action GetRandomDelegate = new Action(GetRandom);
        context.UserState = GetRandomDelegate;
        return GetRandomDelegate.BeginInvoke(callback, state);
    }

    protected override void EndExecute(AsyncCodeActivityContext context, IAsyncResult result)
    {
        // Get the delegate from the UserState and call EndInvoke
        Action GetRandomDelegate = (Action)context.UserState;
        GetRandomDelegate.EndInvoke(result);
    }

    void GetRandom()
    {
        // This activity simulates taking a few moments
        // to generate the random number. This code runs
        // asynchronously with respect to the workflow thread.
        Thread.Sleep(5000);

        Console.WriteLine("Random Number: {0}", r.Next(1, 101));
    }
}

Note that because there is no return value, DisplayRandom uses an Action instead of a Func<T,TResult> to invoke its delegate, and the delegate returns no value.

AsyncCodeActivity also provides a Cancel override. While BeginExecute and EndExecute are required overrides, Cancel is optional, and can be overridden so the activity can clean up its outstanding asynchronous state when it is being canceled or aborted. If clean up is possible and AsyncCodeActivity.ExecutingActivityInstance.IsCancellationRequested is true, the activity should call MarkCanceled. Any exceptions thrown from this method are fatal to the workflow instance.

protected override void Cancel(AsyncCodeActivityContext context)
{
    // Implement any cleanup as a result of the asynchronous work
    // being canceled, and then call MarkCanceled.
    if (context.IsCancellationRequested)
    {
        context.MarkCanceled();
    }
}

Invoking Asynchronous Methods on a Class

Many of the classes in the .NET Framework provide asynchronous functionality, and this functionality can be asynchronously invoked by using an AsyncCodeActivity based activity. In the following example, an activity is created that asynchronously creates a file by using the FileStream class.

public sealed class FileWriter : AsyncCodeActivity
{
    public FileWriter()
        : base()
    {
    }

    protected override IAsyncResult BeginExecute(AsyncCodeActivityContext context, AsyncCallback callback, object state)
    {
        string tempFileName = Path.GetTempFileName();
        Console.WriteLine("Writing to file: " + tempFileName);

        FileStream file = File.Open(tempFileName, FileMode.Create);

        context.UserState = file;

        byte[] bytes = UnicodeEncoding.Unicode.GetBytes("123456789");
        return file.BeginWrite(bytes, 0, bytes.Length, callback, state);
    }

    protected override void EndExecute(AsyncCodeActivityContext context, IAsyncResult result)
    {
        FileStream file = (FileStream)context.UserState;

        try
        {
            file.EndWrite(result);
            file.Flush();
        }
        finally
        {
            file.Close();
        }
    }
}

Sharing State Between the BeginExecute and EndExecute Methods

In the previous example, the FileStream object that was created in BeginExecute was accessed in the EndExecute. This is possible because the file variable was passed in the AsyncCodeActivityContext.UserState property in BeginExecute. This is the correct method for sharing state between BeginExecute and EndExecute. It is incorrect to use a member variable in the derived class (FileWriter in this case) to share state between BeginExecute and EndExecute because the activity object may be referenced by multiple activity instances. Attempting to use a member variable to share state can result in values from one ActivityInstance overwriting or consuming values from another ActivityInstance.

Accessing Argument Values

The environment of an AsyncCodeActivity consists of the arguments defined on the activity. These arguments can be accessed from the BeginExecute/EndExecute overrides using the AsyncCodeActivityContext parameter. The arguments cannot be accessed in the delegate, but the argument values or any other desired data can be passed in to the delegate using its parameters. In the following example, a random number-generating activity is defined that obtains the inclusive upper bound from its Max argument. The value of the argument is passed in to the asynchronous code when the delegate is invoked.

public sealed class GenerateRandomMax : AsyncCodeActivity<int>
{
    public InArgument<int> Max { get; set; }

    static Random r = new Random();

    protected override IAsyncResult BeginExecute(AsyncCodeActivityContext context, AsyncCallback callback, object state)
    {
        // Create a delegate that references the method that implements
        // the asynchronous work. Assign the delegate to the UserState,
        // invoke the delegate, and return the resulting IAsyncResult.
        Func<int, int> GetRandomDelegate = new Func<int, int>(GetRandom);
        context.UserState = GetRandomDelegate;
        return GetRandomDelegate.BeginInvoke(Max.Get(context), callback, state);
    }

    protected override int EndExecute(AsyncCodeActivityContext context, IAsyncResult result)
    {
        // Get the delegate from the UserState and call EndInvoke
        Func<int, int> GetRandomDelegate = (Func<int, int>)context.UserState;
        return (int)GetRandomDelegate.EndInvoke(result);
    }

    int GetRandom(int max)
    {
        // This activity simulates taking a few moments
        // to generate the random number. This code runs
        // asynchronously with respect to the workflow thread.
        Thread.Sleep(5000);

        return r.Next(1, max + 1);
    }
}

Scheduling Actions or Child Activities Using AsyncCodeActivity

AsyncCodeActivity derived custom activities provide a method for performing work asynchronously with regard to the workflow thread, but do not provide the ability to schedule child activities or actions. However, asynchronous behavior can be incorporated with scheduling of child activities through composition. An asynchronous activity can be created, and then composed with an Activity or NativeActivity derived activity to provide asynchronous behavior and scheduling of child activities or actions. For example, an activity could be created that derives from Activity, and has as its implementation a Sequence containing the asynchronous activity as well the other activities that implement the logic of the activity. For more examples of composing activities using Activity and NativeActivity, see How to: Create an Activity and Activity Authoring Options.

See also