Search code examples
c#asp.net-coregenericsdesign-patternsevent-handling

How to design events and event handlers system using generic in C#?


I want to create an event system in my ASP.NET Core backend using generics for ease of creation and registration of handlers.

The main problem I encountered while trying several solutions was generic conflicts when trying to store retrieve and execute event handlers.

I have a base for the events like this:

internal interface IEvent 
{
    // Required to get with which tag/key event is stored in an external system
    public static abstract string GetEventName();
}

public class TestEvent : IEvent 
{
    public static string GetEventName() => "test_event";
}

And a base for event handlers like this:

public interface IEventHandler 
{
}

public interface IEventHandler<TEvent> : IEventHandler where TEvent : IEvent 
{
    public void HandleEvent(TEvent e);
}

public class TestEventHandler : IEventHandler<TestEvent> 
{
    public void HandleEvent(TestEvent e) 
    {
        // Doing something
    }
}

I want to be able to register event handlers in a service container or somewhere easily, so they would load up automatically. Registering them in some kind of EventHandlerFactory or EventHandlerRegistrator is good too, but that doesn't change the main problem.

I am getting events at runtime from some source, they are deserialized from JSON payload.

So I don't know their exact type.

I can determine their type at runtime with reflection, but this would not suffice since I am in need of high event handling throughput, and reflection (if I'm not mistaken) is slow.

I am also having problems with storing event handlers because their generic types are lost during runtime and casting at runtime with specific events produces null references and other kinds of exceptions.


Solution

  • The basic idea is to use reflection only to build handlers and dictionary from type to handler. Something along these lines (leveraging expression trees):

    public static class Helper
    {
        private static readonly Dictionary<Type, IEventHandler> Handlers = new()
        {
            {typeof(TestEvent), new TestEventHandler()}
        };
        
        // not an Action<IEvent> due to static abstract member of the interface
        private static Dictionary<Type, Action<object>> HandlerFuncs = new()
        {
            {typeof(TestEvent), CreateHandler<TestEvent>()}
        };
    
        public static void Handle(IEvent @event) => HandlerFuncs[@event.GetType()](@event);
    
        private static Action<object> CreateHandler<T>()
        {
            var genericMethod = typeof(Helper)
                .GetMethod(nameof(Helper.HandleGeneric), BindingFlags.Static | BindingFlags.NonPublic)
                .MakeGenericMethod(typeof(T));
    
            var parameterExpression = Expression.Parameter(typeof(object));
            var expression = Expression.Lambda<Action<object>>(Expression.Call(genericMethod, parameterExpression), parameterExpression);
            return expression.Compile();
        }
    
        private static void HandleGeneric<T>(object @event) where T : class, IEvent
        {
            var eventHandler = Handlers[typeof(T)] as IEventHandler<T>;
            eventHandler.HandleEvent((T)@event);
        }
    }
    

    With call looking like: Helper.Handle(new TestEvent());.

    Both Handlers and HandlerFuncs can be build via reflection and/or service provider, I use manually build dictionaries for demonstration purposes only. You also can use some 3rd party library like Scrutor to reduce the amount of reflection needed to be written manually.

    This can be relatively easily upgrade to work with DI. For example:

    public class HandlerInvoker
    {
        private static readonly ConcurrentDictionary<Type, Action<IServiceProvider, object>> handlerFuncs = new();
        
        private readonly IServiceProvider _serviceProvider;
    
        public HandlerInvoker(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }
        public void HandleEvent(IEvent e)
        {
            var handler = handlerFuncs.GetOrAdd(e.GetType(), CreateHandler);
            handler(_serviceProvider, e);
        }
        
        private static Action<IServiceProvider, object> CreateHandler(Type t)
        {
            var genericMethod = typeof(HandlerInvoker)
                .GetMethod(nameof(HandleGeneric), BindingFlags.Static | BindingFlags.NonPublic)
                .MakeGenericMethod(t);
    
            var p1 = Expression.Parameter(typeof(IServiceProvider));
            var p2 = Expression.Parameter(typeof(object));
            var expression = Expression.Lambda<Action<IServiceProvider, object>>(Expression.Call(genericMethod, p1, p2), p1, p2);
            return expression.Compile();
        }
        
        private static void HandleGeneric<T>(IServiceProvider sp, object @event) where T : class, IEvent
        {
            var eventHandler =sp.GetRequiredService<IEventHandler<T>>();
            eventHandler.HandleEvent((T)@event);
        }
    }
    

    And sample usage:

    var services = new ServiceCollection();
    services.AddTransient<IEventHandler<TestEvent>, TestEventHandler>();
    services.AddTransient<HandlerInvoker>();
    var sp = services.BuildServiceProvider();
    
    var handlerInvoker = sp.GetService<HandlerInvoker>();
    handlerInvoker.HandleEvent(new TestEvent());
    

    Depended on the use case the reflection and expression trees can be substituted with source generators.