joelmdev
joelmdev

Reputation: 11773

awaitable event handler delegate for ASP.NET

I have an ASP.NET MVC/WebAPI application where the domain logic depends on events in certain circumstances to decouple concerns. It's becoming increasingly difficult to avoid using async methods in the event handlers, but being that this is a web application I want to avoid using async void as these are not top level events that we're dealing with. I've seen some solutions that seem overly complex for dealing with this- I'd like to keep the solution to this simple. My solution is to ditch EventHandler delegates and use a Func that returns a Task instead, e.g.:

public event EventHandler<MyEventArgs> SomethingHappened;

would be refactored to:

public event Func<object, MyEventArgs, Task> SomethingHappened;

so in my code I can do this:

if (SomethingHappened != null)
{
    await SomethingHappened.Invoke(this, new MyEventArgs());
}

We're the only one consuming these projects, so using the standard convention of EventHandler isn't absolutely necessary. While using this pattern implies knowledge that the handler is async, I'm not sure that's necessarily a bad thing as more and more libraries are dropping their synchronous API methods or simply not including them to begin with. To some extent I'm surprised this isn't supported as a first class concept within .NET with async/await becoming increasingly prevalent in many libraries that are commonly used by web applications.

This seems like an elegant solution. I've tested this in a real application with multiple subscribers to the event, with different delays for each handler, and Invoke() awaits them all. Yet, it feels like a trap. What am I missing?

Upvotes: 2

Views: 356

Answers (1)

Stephen Cleary
Stephen Cleary

Reputation: 457217

Yet, it feels like a trap. What am I missing?

This part isn't correct:

Invoke() awaits them all.

From the docs:

Invocation of a delegate instance whose invocation list contains multiple entries proceeds by invoking each of the methods in the invocation list, synchronously, in order... If the delegate invocation includes output parameters or a return value, their final value will come from the invocation of the last delegate in the list.

So, the Invoke will invoke all the handlers, but only return the Task from the last handler. The other returned tasks are ignored. Which is Very Bad (try throwing an exception from one of the ignored tasks).

Instead, you should call GetInvocationList to get the list of handlers, invoke each one, and then await them all using Task.WhenAll:

var args = new MyEventArgs();
var tasks = SomethingHappened.GetInvocationList()
    .Cast<Func<object, MyEventArgs, Task>>()
    .Select(handler => handler(this, args))
    .ToList();
await Task.WhenAll(tasks);

Upvotes: 5

Related Questions