Given the following code:
public delegate Task AsyncEventHandler<in TEventArgs>(object sender, TEventArgs eventArgs);
public static Task InvokeAsync(this AsyncEventHandler eventHandler, object sender, EventArgs eventArgs)
{
if (eventHandler == null) return Task.CompletedTask;
var delegates = eventHandler.GetInvocationList().Cast<AsyncEventHandler>();
var tasks = delegates.Select(it => it.Invoke(sender, eventArgs));
return Task.WhenAll(tasks);
}
I have a test function whereby the faulty handlers should throw exceptions, and the workinghanlder should run - currently only the first FaultyHandler1 is called and no others event handlers.
private class NonGenericNotifier
{
public event AsyncEventHandler SomethingHappened;
public Task OnSomethingHappening() => SomethingHappened.InvokeAsync(this, EventArgs.Empty);
}
public async Task Multiple_Exceptions_That_Occur_During_Event_Handling_Should_Be_Propagated()
{
var isHandler1Called = false;
var isHandler2Called = false;
var isWorkingHandlerCalled = false;
var notifier = new NonGenericNotifier();
Task FaultyHandler1(object sender, EventArgs eventArgs)
{
isHandler1Called = true;
throw new InvalidOperationException();
}
Task FaultyHandler2(object sender, EventArgs eventArgs)
{
isHandler2Called = true;
throw new InvalidOperationException();
}
Task WorkingHandler(object sender, EventArgs eventArgs)
{
isWorkingHandlerCalled = true;
return Task.CompletedTask;
}
notifier.SomethingHappened += FaultyHandler1;
notifier.SomethingHappened += FaultyHandler2;
notifier.SomethingHappened += WorkingHandler;
await Should.ThrowAsync<InvalidOperationException>(async () => await notifier.OnSomethingHappening());
isHandler1Called.ShouldBe(true);
isHandler2Called.ShouldBe(true);
isWorkingHandlerCalled.ShouldBe(true);
}
Assuming a single exception can be thrown I beleive this should be an AggregateException
containing an exception for each Task, and most importantly the InvokeAsync
method above should bail on the first exception encountered.
I have started to create a List<Exception>
within the InvokeAsync
extension method, and wrap each it => it.Invoke(sender, eventArgs)
with a try/catch
construct and within the catch add the exception to the exception list.
However I am lost on how to collate this list of exceptions and then send on as an AggregateException
.
UPDATE (FIX?)
Thanks to Artur for pointing me in the right direction. I changed the InvokeAsync
extension method to the below, and it works - no longer halting on the first task. We have gone from var tasks = delegates.Select(it => it.Invoke(sender, eventArgs));
to the below using the code here:
public static Task InvokeAsync(this AsyncEventHandler eventHandler, object sender, EventArgs eventArgs)
{
if (eventHandler == null) return Task.CompletedTask;
var delegates = eventHandler.GetInvocationList().Cast<AsyncEventHandler>();
var tasks = delegates.Select(async it => await it.Invoke(sender, eventArgs));
return Task.WhenAll(tasks).WithAggregatedExceptions();
}
static Task WithAggregatedExceptions(this Task task)
{
// using AggregateException.Flatten as a bonus
return task.ContinueWith(
continuationFunction: priorTask =>
priorTask.IsFaulted &&
priorTask.Exception is AggregateException ex && (ex.InnerExceptions.Count > 1 || ex.InnerException is AggregateException) ? Task.FromException(ex.Flatten()) : priorTask,
cancellationToken: CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
scheduler: TaskScheduler.Default).Unwrap();
}
My issue is subscribers of this event, writing synchronous handlers over which I have no control - this would stop the other event handlers (sync and async) running that are attached to the same event.
I also appreciate this is the designed function of Task.WhenAll
if you were mixing async and non-async handlers... if there is one reason to not write synchronous code in an async function without await Task.Yield()
this is it.
Question
Can we say that wrapping the delegates.Select(async it => await it.Invoke(sender, eventArgs)
with async/await allows synchronous method to run, and at worst(?) wrap twice an async method (which is the same as nesting async/await function calls) so is actually a non-issue?
Are there any side effects that have been introduced?
With the bounty looking for authorative guidance on how this would be implemented, one answer (much appreciated for contributing to the discussion) says to avoid async events, yet in other places like the discord c# client they have embraced async events (with timeout wrappers etc).