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

fra
  • 3,488
  • 5
  • 38
  • 61

1 Answers1

14

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);
}
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 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 – fra Jun 06 '13 at 15:12
  • 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`. – Stephen Cleary Jun 06 '13 at 15:27
  • 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 – fra Jun 07 '13 at 09:08
  • 1
    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` – Kris Ivanov May 19 '14 at 18:23
  • @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. – Stephen Cleary May 19 '14 at 19:12
  • @KrisIvanov: Found that activity context problem. See this update: http://stackoverflow.com/a/26061482/263693 – Stephen Cleary Sep 26 '14 at 14:04