AsyncCodeActivity is a nice class for wrapping calls to asynchronous APIs and turning them into activities that can run super-efficiently. But! There are a few limitations to being a subclass of AsyncCodeActivity when compared to NativeActivity.

Such as? Here’s a rough list:

  • you can’t have any child activities
  • you can’t create Execution Properties 
  • you can’t create additional bookmarks

Now there are two likely reasons the framework designers didn’t actually include an AsyncNativeActivity in the framework itself.

  1. It’s redundant, because you can do the exact same thing by composing an AsyncActivity with other activities. Probably.
  2. It’s redundant, because you can implement something that works exactly like an AsyncNativeActivity would work yourself by inheriting NativeActivity.

Either way it’s because it’s redundant. Sadly, just because you can do 1 or 2 doesn’t mean it’s really easy and obvious how. So, that’s what today’s post is going to be all about!

Requirements

Let’s start by understanding the key requirements of AsyncCodeActivity that we would need to emulate.

  1. It must use a Bookmark to yield control to other activities until its asynchronous work is all done
  2. The bookmark must be automatically resumed upon completion of the asynchronous work
  3. It probably* must prevent itself from being persisted while the asynchronous work is happening
    [*For the typical everyday scenarios of calling .NET IAsyncResult based APIs this is true anyway.]
  4. For optimalness of implementation we should also return synchronously if the API we called returns synchronously instead of executing Asynchronously
  5. We should probably also try to support cancellation of the activity by cancelling the asynchronous operation, but I may not have time for this bit in just one writing and posting session.
  6. We want to allow easy subclassability like AsyncCodeActivity where the subclasser need implement only BeginExecute() and EndExecute()

Implementation

Let’s examine the Bookmarking behavior first.

Creating a bookmark is easy. We call NativeActivityContext.CreateBookmark().
Notes:

  • We want default BookmarkOptions without making it non-blocking (which could let our activity complete too early) or multiple resume (which would not let our activity complete at all, until we remove the bookmark).
  • We will need a BookmarkCompletionCallback - especially if we want to do anything interesting like schedule more work after the async operation is completed, or update a ‘Result’ OutArgument. (Maybe you’re implementing AsyncNativeActivity<T>.)
  • We don’t want to specify a name.

The next behavior after creating a bookmark is to make the bookmark get resumed. This bit presents a little bit of a puzzle, because there are only so many APIs for resuming bookmarks and they’re all a couple steps away from being easy to use to solve our problem.

The one I have ended up relying on is WorkflowInstanceProxy.BeginResumeBookmark().

It might seem a weird or ironic that our activity has to use yet another Asynchronous API in order just to resume itself. However, this is not exactly accurate, in fact I feel it’s based on a misunderstanding. In terms of call stacks, the caller of BeginResumeBookmark() is not going to be called from the workflow execution runtime, it’s usually going to be some external event, probably from a completely different thread that tells us that our async operation completed right now. By responding to that by making an async way, we are being a good citizen from the point of view of that event’s thread and returning control in a timely fashion.

The code to get this working ends up relying on a WorkflowInstanceExtension, since that is the way to get the WorkflowInstanceProxy. The extension looks like this.

public class BookmarkResumptionHelper : IWorkflowInstanceExtension
{
    private WorkflowInstanceProxy instance;
 
    public void ResumeBookmark(Bookmark bookmark, object value)
    {
        this.instance.EndResumeBookmark(
            this.instance.BeginResumeBookmark(bookmark, value, null, null));
    }
 
    IEnumerable<object> IWorkflowInstanceExtension.GetAdditionalExtensions()
    {
        yield break;
    }
 
    void IWorkflowInstanceExtension.SetInstance(WorkflowInstanceProxy instance)
    {
        this.instance = instance;
    }
}
 

(I do feel like there should be an easier way to do this that doesn’t require a full workflow instance exception, is there some more direct way to get a WorkflowInstanceProxy?)

Next, inside Execute() we need to plumb our bookmark resumption data up to the AsyncResult returned by BeginExecute(). I’ve done this quite simply:

    var bookmark = context.CreateBookmark(BookmarkResumptionCallback);
    this.Bookmark.Set(context, bookmark);
 
    BookmarkResumptionHelper helper = context.GetExtension<BookmarkResumptionHelper>();
    Action<IAsyncResult> resumeBookmarkAction = (result) =>
    {
        helper.ResumeBookmark(bookmark, result);
    };
 
    IAsyncResult asyncResult = this.BeginExecute(
        context, AsyncCompletionCallback, resumeBookmarkAction);
 

The other half of this is AsyncCompletionCallback():

private void AsyncCompletionCallback(IAsyncResult asyncResult)
{
    if (!asyncResult.CompletedSynchronously)
    {
        Action<IAsyncResult> resumeBookmark = asyncResult.AsyncState as Action<IAsyncResult>;
        resumeBookmark.Invoke(asyncResult);
    }
}

The last bit we need to get working is our no-persist-zone. The way to do this is a bit of magic code involving a NoPersistHandle.

Note that we are storing the NoPersistHandle and our Bookmark data inside workflow implementation variables, so that we have isolation from any other simultaneously running instances of the same workflow.

public abstract class AsyncNativeActivity : NativeActivity
{   
    private Variable<NoPersistHandle> NoPersistHandle { get; set; }
    private Variable<Bookmark> Bookmark { get; set; }

During Execute() we must Enter() the NoPersistHandle:

    //...
    var noPersistHandle = NoPersistHandle.Get(context);
    noPersistHandle.Enter(context);

And during the BookmarkResumption function we Exit() it.

private void BookmarkResumptionCallback(NativeActivityContext context, Bookmark bookmark, object value)
{
    var noPersistHandle = NoPersistHandle.Get(context);
    noPersistHandle.Exit(context);
    // unnecessary since it's not multiple resume:
    // context.RemoveBookmark(bookmark);
 
    IAsyncResult asyncResult = value as IAsyncResult;
    this.EndExecute(context, asyncResult);
}

While the NoPersistHandle exists the workflow cannot be persisted. This is a good thing because we need it to remain in memory so that our workflow instance extension can resume the bookmark!
So, from start to finish, that’s nearly the whole thing, except for overriding CacheMetadata(), and except that I really didn’t get into cancellation semantics at all.

Here’s a full code listing (minus usings and namespaces), but including all the bits from above, and ready to copy, paste, play with, and extend. Enjoy.

public class BookmarkResumptionHelper : IWorkflowInstanceExtension
{
    private WorkflowInstanceProxy instance;
 
    public void ResumeBookmark(Bookmark bookmark, object value)
    {
        this.instance.EndResumeBookmark(
            this.instance.BeginResumeBookmark(bookmark, value, null, null));
    }
 
    IEnumerable<object> IWorkflowInstanceExtension.GetAdditionalExtensions()
    {
        yield break;
    }
 
    void IWorkflowInstanceExtension.SetInstance(WorkflowInstanceProxy instance)
    {
        this.instance = instance;
    }
}
 
public abstract class AsyncNativeActivity : NativeActivity
{
    private Variable<NoPersistHandle> NoPersistHandle { get; set; }
    private Variable<Bookmark> Bookmark { get; set; }
    private Activity Body { get; set; }
 
    protected override bool CanInduceIdle
    {
        get
        {
            return true; // we create bookmarks
        }
    }
 
    protected abstract IAsyncResult BeginExecute(
        NativeActivityContext context,
        AsyncCallback callback, object state);
 
    protected abstract void EndExecute(
        NativeActivityContext context,
        IAsyncResult result);
 
    protected override void Execute(NativeActivityContext context)
    {
        var noPersistHandle = NoPersistHandle.Get(context);
        noPersistHandle.Enter(context);
 
        var bookmark = context.CreateBookmark(BookmarkResumptionCallback);
        this.Bookmark.Set(context, bookmark);
 
        BookmarkResumptionHelper helper = context.GetExtension<BookmarkResumptionHelper>();
        Action<IAsyncResult> resumeBookmarkAction = (result) =>
        {
            helper.ResumeBookmark(bookmark, result);
        };
 
        IAsyncResult asyncResult = this.BeginExecute(context, AsyncCompletionCallback, resumeBookmarkAction);
 
        if (asyncResult.CompletedSynchronously)
        {
            noPersistHandle.Exit(context);
            context.RemoveBookmark(bookmark);
            EndExecute(context, asyncResult);
        }
    }
 
    private void AsyncCompletionCallback(IAsyncResult asyncResult)
    {
        if (!asyncResult.CompletedSynchronously)
        {
            Action<IAsyncResult> resumeBookmark = asyncResult.AsyncState as Action<IAsyncResult>;
            resumeBookmark.Invoke(asyncResult);
        }
    }
 
    private void BookmarkResumptionCallback(NativeActivityContext context, Bookmark bookmark, object value)
    {
        var noPersistHandle = NoPersistHandle.Get(context);
        noPersistHandle.Exit(context);
        // unnecessary since it's not multiple resume:
        // context.RemoveBookmark(bookmark);
 
        IAsyncResult asyncResult = value as IAsyncResult;
        this.EndExecute(context, asyncResult);
    }
 
    protected override void CacheMetadata(NativeActivityMetadata metadata)
    {
        this.NoPersistHandle = new Variable<NoPersistHandle>();
        this.Bookmark = new Variable<Bookmark>();
        metadata.AddImplementationVariable(this.NoPersistHandle);
        metadata.AddImplementationVariable(this.Bookmark);
        metadata.RequireExtension<BookmarkResumptionHelper>();
        metadata.AddDefaultExtensionProvider<BookmarkResumptionHelper>(() => new BookmarkResumptionHelper());
    }
}

 

[Epilog: Apologies for the timing of this post. I’ve been meaning to write this post for a long time, but after doing the prototyping I forgot all about it. Comments welcome as always!]


Blog Post by: tilovell09