CSharp · Console
// -----
// The corresponding blog posts:
// https://www.devleader.ca/2023/02/14/async-eventhandlers-a-simple-safety-net-to-the-rescue/
// https://www.devleader.ca/2023/02/18/task-eventhandlers-the-little-secret-you-didnt-know
//
// The corresponding videos for this solution
// https://www.youtube.com/watch?v=QVGjpRz0JZU
// https://www.youtube.com/watch?v=R3T2COPv5Dg
// -----
// History:
// 2023-02-12: Adding follow up code that can demonstrate that when you have multiple handlers,
// the Task EventHandler can stop working as expected when an exception is thrown.
using System;
using System.Threading.Tasks;
var someEventRaisingObject = new SomeEventRaisingObject();
// notice the async void. BEHOLD!!
someEventRaisingObject.TheEvent += EventHandlers.TryAsync<EventArgs>(
//async (s, e) =>
//{
// Console.WriteLine("Entering the event handler...");
// await TaskThatIsReallyImportantAsync();
// Console.WriteLine("Exiting the event handler...");
//},
SomeEventRaisingObject_TheEventAsync,
ex => Console.WriteLine($"[TryAsync Error Handler] Exception handler caught: {ex}"));
// If we wire up an event handler before one that throws, the Task EventHandler solution
// will still work if the final handler throws an exception (the surrounding try catch
// will catch the exception)
//someEventRaisingObject.ExplicitAsyncEvent += async (s, e) =>
//{
// Console.WriteLine("Handler 1!");
//};
someEventRaisingObject.ExplicitAsyncEvent += SomeEventRaisingObject_TheEventAsync;
// If we wire up an event handler AFTER one that throws, the Task EventHandler solution
// will mysteriously no longer work properly. The surrounding try/catch around the
// event invocation no longer seems to catch the exception!
//someEventRaisingObject.ExplicitAsyncEvent += async (s, e) =>
//{
// Console.WriteLine("Handler 3!");
//};
try
{
// forcefully fire our event!
//someEventRaisingObject.Invoke(EventArgs.Empty);
await someEventRaisingObject.InvokeExplicitAsync(EventArgs.Empty);
}
catch (Exception ex)
{
Console.WriteLine($"Caught an exception in our handler: {ex.Message}");
}
// just block so we can wait for async operations to complete
Console.WriteLine("Press Enter to exit");
Console.ReadLine();
async Task SomeEventRaisingObject_TheEventAsync(object sender, EventArgs e)
{
Console.WriteLine("Entering the event handler...");
await TaskThatIsReallyImportantAsync();
Console.WriteLine("Exiting the event handler...");
}
async Task TaskThatIsReallyImportantAsync()
{
// switch between these two lines (comment one or the other out) to play with the behavior
throw new Exception("This is an expected exception");
//await Task.Run(() => Console.WriteLine("Look at us writing to the console from a task!"));
}
delegate Task AsyncEventHandler<TArgs>(object sender, TArgs e)
where TArgs : EventArgs;
static class EventHandlers
{
public static EventHandler<TArgs> TryAsync<TArgs>(
Func<object, TArgs, Task> callback,
Action<Exception> errorHandler)
where TArgs : EventArgs
=> TryAsync<TArgs>(
callback,
ex =>
{
errorHandler.Invoke(ex);
return Task.CompletedTask;
});
public static EventHandler<TArgs> TryAsync<TArgs>(
Func<object, TArgs, Task> callback,
Func<Exception, Task> errorHandler)
where TArgs : EventArgs
{
return new EventHandler<TArgs>(async (object s, TArgs e) =>
{
try
{
await callback.Invoke(s, e);
}
catch (Exception ex)
{
await errorHandler.Invoke(ex);
}
});
}
}
class SomeEventRaisingObject
{
// you could in theory have your own event args here
public event EventHandler<EventArgs> TheEvent;
public event AsyncEventHandler<EventArgs> ExplicitAsyncEvent;
public void Invoke(EventArgs e)
{
TheEvent?.Invoke(this, e);
}
public async Task InvokeExplicitAsync(EventArgs e)
{
await ExplicitAsyncEvent?.Invoke(this, e);
}
}