How do I await events in C#?
Peter's example is great, I've just simplified it a little using LINQ and extensions:
public static class AsynchronousEventExtensions
{
public static Task Raise<TSource, TEventArgs>(this Func<TSource, TEventArgs, Task> handlers, TSource source, TEventArgs args)
where TEventArgs : EventArgs
{
if (handlers != null)
{
return Task.WhenAll(handlers.GetInvocationList()
.OfType<Func<TSource, TEventArgs, Task>>()
.Select(h => h(source, args)));
}
return Task.CompletedTask;
}
}
It may be a good idea to add a timeout. To raise the event call Raise extension:
public event Func<A, EventArgs, Task> Shutdown;
private async Task SomeMethod()
{
...
await Shutdown.Raise(this, EventArgs.Empty);
...
}
But you have to be aware that, unlike synchronous evens, this implementation calls handlers concurrently. It can be an issue if handlers have to be executed strictly consecutively what they are often do, e.g. a next handler depends on results of the previous one:
someInstance.Shutdown += OnShutdown1;
someInstance.Shutdown += OnShutdown2;
...
private async Task OnShutdown1(SomeClass source, MyEventArgs args)
{
if (!args.IsProcessed)
{
// An operation
await Task.Delay(123);
args.IsProcessed = true;
}
}
private async Task OnShutdown2(SomeClass source, MyEventArgs args)
{
// OnShutdown2 will start execution the moment OnShutdown1 hits await
// and will proceed to the operation, which is not the desired behavior.
// Or it can be just a concurrent DB query using the same connection
// which can result in an exception thrown base on the provider
// and connection string options
if (!args.IsProcessed)
{
// An operation
await Task.Delay(123);
args.IsProcessed = true;
}
}
You'd better change the extension method to call handlers consecutively:
public static class AsynchronousEventExtensions
{
public static async Task Raise<TSource, TEventArgs>(this Func<TSource, TEventArgs, Task> handlers, TSource source, TEventArgs args)
where TEventArgs : EventArgs
{
if (handlers != null)
{
foreach (Func<TSource, TEventArgs, Task> handler in handlers.GetInvocationList())
{
await handler(source, args);
}
}
}
}
Personally, I think that having async
event handlers may not be the best design choice, not the least of which reason being the very problem you're having. With synchronous handlers, it's trivial to know when they complete.
That said, if for some reason you must or at least are strongly compelled to stick with this design, you can do it in an await
-friendly way.
Your idea to register handlers and await
them is a good one. However, I would suggest sticking with the existing event paradigm, as that will keep the expressiveness of events in your code. The main thing is that you have to deviate from the standard EventHandler
-based delegate type, and use a delegate type that returns a Task
so that you can await
the handlers.
Here's a simple example illustrating what I mean:
class A
{
public event Func<object, EventArgs, Task> Shutdown;
public async Task OnShutdown()
{
Func<object, EventArgs, Task> handler = Shutdown;
if (handler == null)
{
return;
}
Delegate[] invocationList = handler.GetInvocationList();
Task[] handlerTasks = new Task[invocationList.Length];
for (int i = 0; i < invocationList.Length; i++)
{
handlerTasks[i] = ((Func<object, EventArgs, Task>)invocationList[i])(this, EventArgs.Empty);
}
await Task.WhenAll(handlerTasks);
}
}
The OnShutdown()
method, after doing the standard "get local copy of the event delegate instance", first invokes all of the handlers, and then awaits all of the returned Tasks
(having saved them to a local array as the handlers are invoked).
Here's a short console program illustrating the use:
class Program
{
static void Main(string[] args)
{
A a = new A();
a.Shutdown += Handler1;
a.Shutdown += Handler2;
a.Shutdown += Handler3;
a.OnShutdown().Wait();
}
static async Task Handler1(object sender, EventArgs e)
{
Console.WriteLine("Starting shutdown handler #1");
await Task.Delay(1000);
Console.WriteLine("Done with shutdown handler #1");
}
static async Task Handler2(object sender, EventArgs e)
{
Console.WriteLine("Starting shutdown handler #2");
await Task.Delay(5000);
Console.WriteLine("Done with shutdown handler #2");
}
static async Task Handler3(object sender, EventArgs e)
{
Console.WriteLine("Starting shutdown handler #3");
await Task.Delay(2000);
Console.WriteLine("Done with shutdown handler #3");
}
}
Having gone through this example, I now find myself wondering if there couldn't have been a way for C# to abstract this a bit. Maybe it would have been too complicated a change, but the current mix of the old-style void
-returning event handlers and the new async
/await
feature does seem a bit awkward. The above works (and works well, IMHO), but it would have been nice to have better CLR and/or language support for the scenario (i.e. be able to await a multicast delegate and have the C# compiler turn that into a call to WhenAll()
).
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Example
{
// delegate as alternative standard EventHandler
public delegate Task AsyncEventHandler<TEventArgs>(object sender, TEventArgs e, CancellationToken token);
public class ExampleObject
{
// use as regular event field
public event AsyncEventHandler<EventArgs> AsyncEvent;
// invoke using the extension method
public async Task InvokeEventAsync(CancellationToken token) {
await this.AsyncEvent.InvokeAsync(this, EventArgs.Empty, token);
}
// subscribe (add a listener) with regular syntax
public static async Task UsageAsync() {
var item = new ExampleObject();
item.AsyncEvent += (sender, e, token) => Task.CompletedTask;
await item.InvokeEventAsync(CancellationToken.None);
}
}
public static class AsynEventHandlerExtensions
{
// invoke a async event (with null-checking)
public static async Task InvokeAsync<TEventArgs>(this AsyncEventHandler<TEventArgs> handler, object sender, TEventArgs args, CancellationToken token) {
var delegates = handler?.GetInvocationList();
if (delegates?.Length > 0) {
var tasks = delegates
.Cast<AsyncEventHandler<TEventArgs>>()
.Select(e => e.Invoke(sender, args, token));
await Task.WhenAll(tasks);
}
}
}
}