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:
Now there are two likely reasons the framework designers didn’t actually include an AsyncNativeActivity in the framework itself.
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!
Let’s start by understanding the key requirements of AsyncCodeActivity that we would need to emulate.
Let’s examine the Bookmarking behavior first.
Creating a bookmark is easy. We call NativeActivityContext.CreateBookmark(). Notes:
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.
(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:
The other half of this is AsyncCompletionCallback():
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.
During Execute() we must Enter() the NoPersistHandle:
And during the BookmarkResumption function we Exit() it.
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.
[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!]