6

Since long time I'm writing custom AsyncCodeActivity classes using the following template:

public sealed class MyActivity : AsyncCodeActivity<T>
{
    protected override IAsyncResult BeginExecute(AsyncCodeActivityContext context, AsyncCallback callback, object state)
    {
        var task = new Task<T>(this.Execute, state, CancellationToken.None, TaskCreationOptions.AttachedToParent);
        task.ContinueWith(s => callback(s));
        task.Start();
        return task;
    }

    protected override T EndExecute(AsyncCodeActivityContext context, IAsyncResult result)
    {
        var task = result as Task<T>;
        if (task.Exception != null)
        {
            // Error handling. Rethrow? Cancel?
        }

        return task.Result;
    }

    private T Execute(object state)
    {
        // Logic here
        return default(T);
    }
}

I have some questions about it:

  1. Which is the right way to handle exceptions? Rethrowing? Setting the context as canceled?
  2. Is there an elegant way to write it with async/await syntax available now?

Thanks

1 Answer 1

15

1) You should rethrow the exception from your EndExecute method.

2) I recommend you create your own base type. I wrote one up called AsyncTaskCodeActivity<T> below:

public abstract class AsyncTaskCodeActivity<T> : AsyncCodeActivity<T>
{
    protected sealed override IAsyncResult BeginExecute(AsyncCodeActivityContext context, AsyncCallback callback, object state)
    {
        var task = ExecuteAsync(context);
        var tcs = new TaskCompletionSource<T>(state);
        task.ContinueWith(t =>
        {
            if (t.IsFaulted)
                tcs.TrySetException(t.Exception.InnerExceptions);
            else if (t.IsCanceled)
                tcs.TrySetCanceled();
            else
                tcs.TrySetResult(t.Result);

            if (callback != null)
                callback(tcs.Task);
        });

        return tcs.Task;
    }

    protected sealed override T EndExecute(AsyncCodeActivityContext context, IAsyncResult result)
    {
        var task = (Task<T>)result;
        try
        {
            return task.Result;
        }
        catch (AggregateException ex)
        {
            ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
            throw;
        }
    }

    protected abstract Task<T> ExecuteAsync(AsyncCodeActivityContext context);
}

If you use my AsyncEx library, this wrapper becomes much simpler:

public abstract class AsyncTaskCodeActivity<T> : AsyncCodeActivity<T>
{
    protected sealed override IAsyncResult BeginExecute(AsyncCodeActivityContext context, AsyncCallback callback, object state)
    {
        var task = ExecuteAsync(context);
        return AsyncFactory<T>.ToBegin(task, callback, state);
    }

    protected sealed override T EndExecute(AsyncCodeActivityContext context, IAsyncResult result)
    {
        return AsyncFactory<T>.ToEnd(result);
    }

    protected abstract Task<T> ExecuteAsync(AsyncCodeActivityContext context);
}

Once you have the base type, you can define your own derived type. Here's one that uses async/await:

public sealed class MyActivity : AsyncTaskCodeActivity<int>
{
    protected override async Task<int> ExecuteAsync(AsyncCodeActivityContext context)
    {
        await Task.Delay(100);
        return 13;
    }
}

And here's one that schedules CPU-bound work to the thread pool (similar to your current template):

public sealed class MyCpuActivity : AsyncTaskCodeActivity<int>
{
    protected override Task<int> ExecuteAsync(AsyncCodeActivityContext context)
    {
        return Task.Run(() => 13);
    }
}

Update from comments: Here's one that uses cancellation. I'm not 100% sure it's correct, because cancellation is itself asynchronous, and the semantics for AsyncCodeActivity<T>.Cancel are underdocumented (i.e., Is Cancel supposed to wait for the activity to complete in a canceled state? Is it acceptable for an activity to complete successfully after Cancel is called?).

public abstract class AsyncTaskCodeActivity<T> : AsyncCodeActivity<T>
{
    protected sealed override IAsyncResult BeginExecute(AsyncCodeActivityContext context, AsyncCallback callback, object state)
    {
        var cts = new CancellationTokenSource();
        context.UserState = cts;
        var task = ExecuteAsync(context, cts.Token);
        return AsyncFactory<T>.ToBegin(task, callback, state);
    }

    protected sealed override T EndExecute(AsyncCodeActivityContext context, IAsyncResult result)
    {
        try
        {
            return AsyncFactory<T>.ToEnd(result);
        }
        catch (OperationCanceledException)
        {
            if (context.IsCancellationRequested)
                context.MarkCanceled();
            else
                throw;
            return default(T); // or throw?
        }
    }

    protected override void Cancel(AsyncCodeActivityContext context)
    {
        var cts = (CancellationTokenSource)context.UserState;
        cts.Cancel();
    }

    protected abstract Task<T> ExecuteAsync(AsyncCodeActivityContext context, CancellationToken cancellationToken);
}
Sign up to request clarification or add additional context in comments.

6 Comments

One small additional requirement: the async activity I need is supposed to be a possible trigger in a pick activity, therefore I also need (graceful) cancellation...how could it be extended to support such scenario? thanks
I'm not familiar with WF cancellation, but I expect you could create a CancellationTokenSource in BeginExecute (and save it in the context), passing the token to ExecuteAsync. Then override Cancel to get the CancellationTokenSource from the context, cancel it, and call MarkCanceled.
Thanks for the updated example. In my tests, it works very well in a pick activity immediately canceling all branches other than the "winner" one
I am getting an exception "An ActivityContext can only be accessed within the scope of the function it was passed into." when trying to use the context inside the ExecuteAsync method after executing await
@KrisIvanov: I have not seen that exception, but I haven't used asynchronous activities a great deal, either. I suggest you look through the code and ensure that you are not using async void anywhere. If that's not the problem, post a minimal repro as a question.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.