Search code examples
c#generics.net-coredependency-injectionautofac

Dependency Injection of IEnumerable of Open Generic


I am trying to optimize my code for the injection of a list of classes, that implement an interface of IEventHandler<TEvent>.

I have the following structure:

public interface IEventHandlerMarker    {   }

public interface IEventHandler<in TEvent> : IEventHandlerMarker where TEvent : IEvent
{
    Task Handle(TEvent eventItem);
}

public interface IEvent
{
    public DateTime Timestamp { get; set; }
}

I register the marker interface IEventHandlerMarker in DI and when accessing the handlers, I currently do the following:

public EventPublisherService(IEnumerable<IEventHandlerMarker> eventHandlers)
{
    // Check and and all event handlers
    foreach (IEventHandlerMarker item in eventHandlers)
    {
        AddEventHandler(item);
    }
}

In my AddEventHandler method, I filter those to IEventHandler<> like this:

Type handlerType = eventHandlerMarker.GetType().GetInterfaces()
    .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEventHandler<>));

So far everything works, but I'd like to get rid of the marker interface and the filter logic. So I changed the registration of handlers to the following method and this seems to work as expected:

public static IServiceCollection AddEventHandlers(this IServiceCollection serviceDescriptors, params Assembly[] handlerAssemblies)
{
    Type genericHandlerType = typeof(IEventHandler<>);
    foreach (var implementationType in genericHandlerType.GetTypesWithGenericInterfacesInAssemblies(handlerAssemblies))
    {
        Type interfaceType = implementationType.GetGenericInterfaceType(genericHandlerType);
        serviceDescriptors.AddSingleton(interfaceType, implementationType);
    }
    return serviceDescriptors;
}

public static List<Type> GetTypesWithGenericInterfacesInAssemblies(this Type source, params Assembly[] assemblies)
{
    return assemblies
        .SelectMany(currentAssembly => currentAssembly.GetTypes()
        .Where(type => type.GetInterfaces().Any(
                interfaceItem => interfaceItem.IsGenericType
                && interfaceItem.GetGenericTypeDefinition().Equals(source))))
        .ToList();
}

I changed the constructor of EventPublisherService to the following:

public EventPublisherService(IServiceProvider serviceProvider)
{
    Type ienumerableOfIEventHandlerType = typeof(IEnumerable<>).MakeGenericType(typeof(IEventHandler<>));
    object result = serviceProvider.GetService(ienumerableOfIEventHandlerType);
}

But result always turns out to be null. I googled and checked some articles on Stackoverflow and came across the following article: https://stackoverflow.com/a/51504151/1099519

I am not sure if this is the same case, as I am not using a factory.

Versions used: .NET Core 3.1 and Autofac 4.9.4 for the Dependency Injection management.


Solution

  • Register all handlers automatically as shown in this question/answer:

    builder.RegisterAssemblyTypes(AppDomain.CurrentDomain.GetAssemblies())
       .AsClosedTypesOf(typeof (IEventHandler<>)).AsImplementedInterfaces();
    

    When you have TEvent and want to find all handlers, get them by constructing the concrete interface type as follows:

    Type generic = typeof(IEnumerable<IEventHandler<>>);
    Type[] typeArgs = { typeof(TEvent) }; // you might get the Type of TEvent in a different way
    Type constructed = generic.MakeGenericType(typeArgs);
    

    You should cache this in an dictionary to avoid doing reflection at every event dispatch.

    Once you have the constructed concrete interface type, you can ask Autofac for all implementations of that interface:

    var handlers = container.Resolve(constructed);
    

    Now, the problem is that with the handler instances you can only call the Handle method using Invoke (reflection). This is a performance issue, but it's unrelated to how you register and resolve the handlers. It's related to the fact that you need to call a concrete method from an object. To avoid using reflection you need compiled code that calls the concrete method (note that using Generics also creates concrete compiled code for each Type).

    I can think of two ways of getting compiled code to do this calls:

    1. Manually writing a delegate which casts your object handler instance into a concrete type for every TEvent type that you have. Then store all these delegates in a dictionary so you can find them at runtime based on TEvent type and call it passing the handler instance and the event instance. With this approach, for every new TEvent that you create, you need to create a matching delegate.

    2. Doing a similar thing as before but by emitting the code at startup. So, same thing, but the whole creation of the delegates is automatic.


    Update

    Based on the repository shared by the OP, I created a working version. The main code, to resolve the handlers and call them is in the EventPublisherService class

    public class EventPublisherService : IEventPublisherService
    {
        private readonly ILifetimeScope _lifetimeScope;
    
        public EventPublisherService(ILifetimeScope lifetimeScope)
        {
            _lifetimeScope = lifetimeScope;
        }
    
        public async Task Emit(IEvent eventItem)
        {
            Type[] typeArgs = { eventItem.GetType() };
            Type handlerType = typeof(IEventHandler<>).MakeGenericType(typeArgs);
            Type handlerCollectionType = typeof(IEnumerable<>).MakeGenericType(handlerType);
    
            var handlers = (IEnumerable<object>)_lifetimeScope.Resolve(handlerCollectionType);
    
            var handleMethod = handlerType.GetMethod("Handle");
    
            foreach (object handler in handlers)
            {
                await ((Task)handleMethod.Invoke(handler, new object[] { eventItem }));
            }
        }
    }
    

    Note that as specified in the original answer, this solution does not include the very necessary performance optimisations. The minimum that should be done is to cache all the types and MethodInfos so they don't need to be constructed every time. The second optimisation would be, as explained, to avoid using Invoke, but it's more complicated to achieve and IMO it requires a separate question.