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 some rationales the framework designers probably thought of for not actually including an AsyncNativeActivity in the framework itself.
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.
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.
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.
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)
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.
protected override bool CanInduceIdle
get
return true; // we create bookmarks
protected abstract IAsyncResult BeginExecute(
NativeActivityContext context,
AsyncCallback callback, object state);
protected abstract void EndExecute(
IAsyncResult result);
protected override void Execute(NativeActivityContext context)
IAsyncResult asyncResult = this.BeginExecute(context, AsyncCompletionCallback, resumeBookmarkAction);
if (asyncResult.CompletedSynchronously)
context.RemoveBookmark(bookmark);
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!]