Safety Net Event Handlers

CSharp · Console

/Program.cs

// -----
// 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);
	}
}
;