(WF4) They have AsyncCodeActivity, why not AsyncNativeActivity?

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 or access Execution Properties (sometimes useful or important!)
  • you can’t create additional bookmarks

Now there are some rationales the framework designers probably thought of for not actually including 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.
  2. It’s redundant, because you can implement something that works exactly like an AsyncNativeActivity would work yourself by inheriting NativeActivity.

But either way it’s basically because it’s not adding new stuff to the set of stuff you can do with the framework. 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 implementation aspects of AsyncCodeActivity that we need to emulate in our activity.

  1. Bookmark is used 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. AsyncCodeActivity probably* must prevent itself from being persisted while the asynchronous work is happening
    [*For typical everyday scenarios of calling .NET IAsyncResult based APIs this is true anyway.]
  4. Execute() should return synchronously if the 'async' API we called actually returned synchronously (this happens pretty often by the way) - it would be pointless to create a bookmark and then resume it straight away.
  5. We should probably support cancellation of the workflow and use that as a trigger to cancel the asynchronous operation if it is in progress  - but I do not have time for this bit in just one writing and posting session.

Finally on top of all that, the key thing we want to replicate from AsyncCodeActivity is ease of subclassing so you can subclass it and implement only BeginExecute() and EndExecute(), and you don't have to worry about the implementation details of 1-5.

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; }

 

    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!]