Calling async methods within Episerver events
In the article Better event handling in Episerver, I wrote how to handle Episerver events. I was calling the async Publish method of mediator in the fire and forget manner. But it is not a good solution in an ASP.NET application as there is no warranty that the running task will finish.
As I mentioned, the first version was just calling an async method and not caring if it will finish executing.
contentEvents.LoadingContent += ContentEvents_LoadingContent;
// ...
private void ContentEvents_LoadingContent(
object sender, EPiServer.ContentEventArgs e)
{
RunMeAsync();
}
It is unlikely that you would want the method to start running but not completing.
Next reasonable solution which did work in the unit tests were async/await. I had to add async keyword to the event handling method and await to my async method call.
private async void ContentEvents_LoadingContent(
object sender, EPiServer.ContentEventArgs e)
{
await RunMeAsync();
}
While this is the right way to handle events, it doesn't work in the ASP.NET context. You will get this exception if you try:
An asynchronous operation cannot be started at this time. Asynchronous operations may only be started within an asynchronous handler or module or during certain events in the Page lifecycle. If this exception occurred while executing a Page, ensure that the Page is marked <%@ Page Async="true" %>. This exception may also indicate an attempt to call an "async void" method, which is generally unsupported within ASP.NET request processing. Instead, the asynchronous method should return a Task, and the caller should await it.
Maybe try waiting on the task completion?
private void ContentEvents_LoadingContent(
object sender, EPiServer.ContentEventArgs e)
{
RunMeAsync().Wait();
}
Initially, when I tried it, it did work, but the performance of the site was very low. I tried it on the site which was hosted on IIS. But then I tried it on the new Alloy Tech project running on the IIS Express, and it just never loaded. Unfortunately, waiting for an async method can cause deadlocks. Not sure where in this case.
After googling for some time, I found an answer on the Stack Overflow. It suggested using some AsyncHelpers class. I copied this over to my project, and it looked like working except when my async method threw an exception.
Some more googling and I found another similar answer. In this Stack Overflow thread, another Erik Philips gave an example of another AsyncHelper class which is used by Microsoft internally.
internal static class AsyncHelper
{
private static readonly TaskFactory _myTaskFactory = new
TaskFactory(CancellationToken.None,
TaskCreationOptions.None,
TaskContinuationOptions.None,
TaskScheduler.Default);
public static TResult RunSync<TResult>(Func<Task<TResult>> func)
{
return AsyncHelper._myTaskFactory
.StartNew<Task<TResult>>(func)
.Unwrap<TResult>()
.GetAwaiter()
.GetResult();
}
public static void RunSync(Func<Task> func)
{
AsyncHelper._myTaskFactory
.StartNew<Task>(func)
.Unwrap()
.GetAwaiter()
.GetResult();
}
}
Now calling my async method from the event handler method works perfectly.
private void ContentEvents_LoadingContent(
object sender, EPiServer.ContentEventArgs e)
{
AsyncHelper.RunSync(RunMeAsync);
}
Conclusion
When calling an async method from the event handler method, use the solution from this Stack Overflow answer and don't try anything else :)
But in all other cases (controller's action method, for example) if you can, use async/await.