Wrapping event-based asynchronous pattern (EAP) into task-based asynchronous pattern (TAP)
After my presentation this week on WUG I got an interesting question. How to wrap event-based asynchronous pattern (EAP) into task-based asynchronous pattern (TAP). Obviously my mind immediately picked up the TaskCompletetionSource<T>
class as a viable solution.
And indeed it’s not difficult. The original question used ReverseGeocodeQuery
class, but because I don’t have Windows Phone SDK etc. installed I went to good old SmtpClient
(just to make sure, SmtpClient
does support TAP from .NET 4.5). 😃 I first tried to write some generic wrapper that would work no matter what class you’re using, but it turned out to be kind of not nice method to call – at least for me. Too much clutter.
Anyway here’s now to wrap SmtpClient
’s EAP into TAP.
using (var client = new SmtpClient())
{
var tcs = new TaskCompletionSource<object>();
var handler = default(SendCompletedEventHandler);
handler = new SendCompletedEventHandler((sender, eventArgs) =>
{
var tcsLocal = (TaskCompletionSource<object>)eventArgs.UserState;
try
{
if (eventArgs.Error != null)
{
tcsLocal.SetException(eventArgs.Error);
return;
}
if (eventArgs.Cancelled)
{
tcsLocal.SetCanceled();
return;
}
// set result, if any
tcsLocal.SetResult(null);
return;
}
finally
{
client.SendCompleted -= handler;
}
});
client.SendCompleted += handler;
client.SendAsync(new MailMessage("foo@example.com", "bar@example.com", "Subject", "Body"), tcs);
await tcs.Task;
// ...
}
I’ll walk through the code. First I create the TaskCompletionSource<object>
(object
because SmtpClient
does not return any value and object
minimal “object” to put something into the generic parameter) and then I create handler to “finish” the operation. If there’s and error, I’ll call SetException
with appropriate exception from the property and I’m done. If not, I’ll check whether the operation was cancelled and if so I’ll call SetCanceled
. If there was no error nor cancellation I’m ready to SetResult
(in this case I simply set null
, because I don’t have anything better). And that’s it. You’re done.
As I said above I initially thought about creating some wrapper. I wrote one, but I don’t like how it turned out. I feel kind of there’s too much noise – generic parameters, factories to create some type (because there’s not a common base type); and also calling isn’t looking nice. Below is the code anyway. 😎 Maybe some reader will find a nicer way to wrap it into C# language.
static Task WrapEapToTap<TObject, TEventHandler, TEventArgs, TResult>(TObject obj,
Func<TEventArgs, TResult> resultSelector,
Action<TObject, object> eapAction,
Action<TObject, TEventHandler> addEventHandler,
Action<TObject, TEventHandler> removeEventHandler,
Func<Action<object, TEventArgs>, TEventHandler> eventHandlerFactory)
where TEventArgs : AsyncCompletedEventArgs
{
var tcs = new TaskCompletionSource<object>();
var handler = default(TEventHandler);
handler = eventHandlerFactory((sender, eventArgs) =>
{
var tcsLocal = (TaskCompletionSource<object>)eventArgs.UserState;
try
{
if (eventArgs.Error != null)
{
tcsLocal.SetException(eventArgs.Error);
return;
}
if (eventArgs.Cancelled)
{
tcsLocal.SetCanceled();
return;
}
tcsLocal.SetResult(resultSelector(eventArgs));
return;
}
finally
{
removeEventHandler(obj, handler);
}
});
addEventHandler(obj, handler);
eapAction(obj, tcs);
return tcs.Task;
}
If you’d like to use this method to wrap the SmtpClient
example it looks like this.
using (var client = new SmtpClient())
{
await WrapEapToTap<SmtpClient, SendCompletedEventHandler, AsyncCompletedEventArgs, object>(
client,
eventArgs => null,
(c, o) => client.SendAsync(new MailMessage("foo@example.com", "bar@example.com", "Subject", "Body"), o),
(c, handler) => client.SendCompleted += handler,
(c, handler) => client.SendCompleted -= handler,
handler => new SendCompletedEventHandler(handler));
// ...
}
Anyway I believe you should first try wrapping asynchronous programming model (APM) (that’s the one with BeginXxx
and EndXxx
methods) using FromAsync
method. This model is kind of less hacky and closer to the metal. And any good component should have first and foremost APM and maybe EAP.
If you’re stuck with EAP, I hope the code above helps.