Autofac: резолв зависимости типа ICommandHandlerAsync<T> без указания типа T

170
04 июня 2022, 21:00

сразу выделю вопрос, ниже будет подробное описание для чего мне это надо.

В Autofac зарегистрированы зависимости следующего вида

ICommandHandlerAsync<ChangeEmailCi> -> UpdateEmailHandler
ICommandHandlerAsync<ChangeEmailCi> -> SendNotificationUpdateEmailHandler
ICommandHandlerAsync<UpdateUserCi> -> UpdateUserHandler
ICommandHandlerAsync<UpdateUserCi> -> SendNotificationUpdateUserHandler

мне нужно без указания конкретного типа Tcommand разрезолвить список всех зависимостей. Т.е. хочу получить все 4 зависимости.

var resolvedHandlers = ctx.Resolve(typeof(IList<ICommandHandlerAsync<>>));  //ТУТ Exception.

Используя IServiceCollection из встроенного IoC можно получить массив ServiceDescriptor[] и на базе его получить все зависимости типа ICommandHandlerAsync<>

var handlerTypes = new Dictionary<Type, IEnumerable<object>>();
ServiceDescriptor[] descriptors = services
            .Where(t => t.ServiceType.IsGenericType && t.ServiceType.GetGenericTypeDefinition() == typeof(ICommandHandlerAsync<>))
            .ToArray();
ServiceProvider? provider = services.BuildServiceProvider();
foreach (var serviceDescriptor in descriptors) 
{
    handlerTypes[serviceDescriptor.ServiceType] = provider.GetServices(serviceDescriptor.ServiceType);
}

Использую на проекте Диспетчер команд, который по типу переданной команды ICommandHandlerAsync<TCommand> находит список обработчиков в словаре.

public class CommandDispatcher 
{
    private readonly Dictionary<Type, IEnumerable<object>> _handlers;
    private readonly ILogger<CommandDispatcher>? _logger;
    public CommandDispatcher(
        Dictionary<Type, IEnumerable<object>> handlers,
        ILogger<CommandDispatcher>? logger = null
        ) 
    {
        _handlers = handlers;
        _logger = logger;
    }

    public async Task<Result> DispatchAsync<TCommand>(TCommand command) where TCommand : class 
    {
        _logger?.LogInformation($"Receiving command {command.GetType()}");
        // Извлечем тип переданной команды
        Type handlerType = typeof(ICommandHandlerAsync<>).MakeGenericType(command.GetType());
        _handlers.TryGetValue(handlerType, out IEnumerable<object>? concreteHandlers);
        if (concreteHandlers is null) throw new ArgumentException($"No handler registered for {command.GetType()}");
        var resultList= new List<Result>();
        foreach (object concreteHandler in concreteHandlers) 
        {
            if (!(concreteHandler is ICommandHandlerAsync<TCommand> handler)) throw new ArgumentException($"NOT VALID handler registered for {command.GetType()}");
            _logger?.LogInformation($"Invoking handler {handler.GetType()}");
            var res= await handler.HandleAsync(command);
            resultList.Add(res);
        }
        var combinedResult= Result.Combine(resultList);
        return combinedResult;
    }
}

Команда и обработчик команды

public class ChangeEmailCi
{
    public ChangeEmailCi(string oldEmail, string newEmail)
    {
        if (string.IsNullOrEmpty(oldEmail) || string.IsNullOrEmpty(newEmail))
            throw new ArgumentException();
        OldEmail = oldEmail;
        NewEmail = newEmail;
    }
    public string OldEmail { get; }
    public string NewEmail { get; }
}

public class UpdateEmailHandler : ICommandHandlerAsync<ChangeEmailCi>
{
    private readonly IUserRepository _repository;
    public UpdateEmailHandler(IUserRepository repository)
    {
        _repository = repository;
    }
    public async Task<Result> HandleAsync(ChangeEmailCi command)
    {
        //Update Email in _repository
    }
}

Я использовал встроенный Ioc контейнер и вот так регистрировал CommandDispatcher с словарем всех команд и их обработчиков.

    public static IServiceCollection AddCommandDispatcher(this IServiceCollection services) 
    {
        var loggerFactory = services.BuildServiceProvider().GetRequiredService<ILoggerFactory>();
        ILogger logger = loggerFactory.CreateLogger(typeof(ServiceInjector));
        
        logger.LogInformation("Initiating discovery process for command handlers.");
        
        Type[] handlers = Assembly.GetExecutingAssembly().GetTypes()
            // Find all types where
            .Where(t => t.IsClass && // the type is a class
                        t.GetInterfaces() // and the type implements the generic type definition ICommandHandlerAsync
                            .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICommandHandlerAsync<>)))
            .ToArray();
        
        logger.LogInformation($"Found {handlers.Length} command handlers.");
        // Register the interface with the concrete handler.
        // Note: the current limitation is, every handler must be in a separate class.
        // This won't work with a single class implementing the ICommandHandlerAsync<> multiple times.
        var stringBuilder = new StringBuilder();
        stringBuilder.AppendLine("--- Registered handlers ---");
        foreach (Type concreteHandler in handlers) {
            Type? handlerInterface = concreteHandler
                .GetInterfaces()
                .First(i => i.GetGenericTypeDefinition() == typeof(ICommandHandlerAsync<>));
            stringBuilder.AppendLine($"{concreteHandler.Name} -> {handlerInterface}");
            services.AddScoped(handlerInterface, concreteHandler);
        }
        
        logger.LogInformation(stringBuilder.ToString());
        var handlerTypes = new Dictionary<Type, IEnumerable<object>>();
        ServiceDescriptor[] descriptors = services
            .Where(t => t.ServiceType.IsGenericType && t.ServiceType.GetGenericTypeDefinition() == typeof(ICommandHandlerAsync<>))
            .ToArray();
        ServiceProvider? provider = services.BuildServiceProvider();
        foreach (var serviceDescriptor in descriptors) 
        {
            handlerTypes[serviceDescriptor.ServiceType] = provider.GetServices(serviceDescriptor.ServiceType);
        }
        
        // this factory method is called every time a CommandDispatcher is required
        services.AddScoped(provider => new CommandDispatcher(handlerTypes, provider.GetRequiredService<ILogger<CommandDispatcher>>()));
        
        return services;
    }

Перешел на Autofac и нужно повторить регистрацию AddCommandDispatcher.

public class CommandDispatcherAutofacModule : Module
{
    private readonly ILogger _logger;
    public CommandDispatcherAutofacModule(ILogger logger)
    {
        _logger = logger;
    }
        
    protected override void Load(ContainerBuilder builder)
    {
        _logger?.LogInformation("Initiating discovery process for command handlers.");
        
        Type[] handlers = Assembly.GetExecutingAssembly().GetTypes()
            // Find all types where
            .Where(t => t.IsClass && // the type is a class
                        t.GetInterfaces() // and the type implements the generic type definition ICommandHandlerAsync
                            .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICommandHandlerAsync<>)))
            .ToArray();
        _logger?.LogInformation($"Found {handlers.Length} command handlers.");
        
        //РЕГИСТРАЦИЯ ВСЕХ ОБРАБОТЧИКОВ КОММАНД ПОД ИНТЕРФЕЙСАМИ ВИДА ICommandHandlerAsync<>
        var stringBuilder = new StringBuilder();
        stringBuilder.AppendLine("--- Registered handlers ---");
        foreach (Type concreteHandler in handlers) 
        {
            Type? handlerInterface = concreteHandler.GetInterfaces().First(i => i.GetGenericTypeDefinition() == typeof(ICommandHandlerAsync<>));
            stringBuilder.AppendLine($"{concreteHandler.Name} -> {handlerInterface}");
            builder.RegisterType(concreteHandler).As(handlerInterface).InstancePerLifetimeScope();
        }
        _logger?.LogInformation(stringBuilder.ToString());
        
        //РЕГИСТРАЦИЯ CommandDispatcher. 
        builder.RegisterType<CommandDispatcher>()
            .WithParameters(new List<ResolvedParameter>
            {
                new ResolvedParameter(
                    (pi, ctx) => (pi.ParameterType == typeof(Dictionary<Type, IEnumerable<object>>) && (pi.Name == "handlers")),
                    (pi, ctx) =>
                    {
                        var handlersDict = new Dictionary<Type, IEnumerable<object>>();
                        
                        //ХОЧУ РЕЗОЛВИТЬ ВЕСЬ СПИСОК ЗАВИСИМОСТЕЙ ТИПА ICommandHandlerAsync<>
                        var resolvedHandlers = ctx.Resolve(typeof(IList<ICommandHandlerAsync<>>)); 
                        //Но так не работает, нужно указать точный тип, например typeof(IList<ICommandHandlerAsync<AcceptApplicationCi>>)
                        
                        //После того как я получу все зависимости типа ICommandHandlerAsync<>, я сгруппирую их по типу и мапну в словарь.
                        //Таким образом будет готова фабрика по резолву CommandDispatcher. 
                        
                        return handlersDict;
                    })
            }).InstancePerLifetimeScope();        
    }
}

P.S. Реализация если в диспетчере под одной командой один обработчик.

builder.RegisterType<CommandDispatcher>()
.WithParameters(new List<ResolvedParameter>
{
    new ResolvedParameter(
        (pi, ctx) => (pi.ParameterType == typeof(Dictionary<Type, IEnumerable<object>>) && (pi.Name == "handlers")),
        (pi, ctx) =>
        {
            var allDescriptors = new List<object>();
            foreach (var concreteHandler in handlers)
            {
                var handlerInterface = concreteHandler.GetInterfaces().First(i => i.GetGenericTypeDefinition() == typeof(ICommandHandlerAsync<>));
                object resolvedCh = ctx.Resolve(handlerInterface); //Тут резолв первой зарегистрированной зависисоти, а нужен список зависимостей
                allDescriptors.Add(resolvedCh);
                
                //Если резолвить в ручную список то все РАБОТАЕТ.
                // dynamic descriptors1 = ctx.Resolve(typeof(IList<ICommandHandlerAsync<ChangeEmailCi>>));
                // dynamic descriptors2 = ctx.Resolve(typeof(IList<ICommandHandlerAsync<UpdateUserCi>>));
                //allDescriptors.AddRange(descriptors1);
                //allDescriptors.AddRange(descriptors2);
            }
            var handlersDict= allDescriptors
                .GroupBy(
                    k => k.GetType().GetInterfaces().First(i => i.GetGenericTypeDefinition() == typeof(ICommandHandlerAsync<>)),
                    o => o
                    )
                .ToDictionary(gr => gr.Key, gr => gr.AsEnumerable());
                
            return handlersDict;                
        })
}).InstancePerLifetimeScope();
Answer 1

Andrei Khotko помог найти недостающий пазл. Используя MakeGenericType можно задавать составные типы дженериков динамически.

 typeof(IList<>).MakeGenericType(handlerInterface)

Как регистрировать Диспетчер комманд в Autofac

public class CommandDispatcherAutofacModule : Module
{
    private readonly ILogger _logger;
    public CommandDispatcherAutofacModule(ILogger logger)
    {
        _logger = logger;
    }
    
    
    protected override void Load(ContainerBuilder builder)
    {
        _logger?.LogInformation("Initiating discovery process for command handlers.");
        
        Type[] handlers = Assembly.GetExecutingAssembly().GetTypes()
            // Find all types where
            .Where(t => t.IsClass && // the type is a class
                        t.GetInterfaces() // and the type implements the generic type definition ICommandHandlerAsync
                            .Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICommandHandlerAsync<>)))
            .ToArray();
        _logger?.LogInformation($"Found {handlers.Length} command handlers.");
        
        //РЕГИСТРАЦИЯ ВСЕХ ОБРАБОТЧИКОВ КОММАНД ПОД ИНТЕРФЕЙСАМИ ВИДА ICommandHandlerAsync<>
        var stringBuilder = new StringBuilder();
        stringBuilder.AppendLine("--- Registered handlers ---");
        foreach (Type concreteHandler in handlers) 
        {
            Type? handlerInterface = concreteHandler.GetInterfaces().First(i => i.GetGenericTypeDefinition() == typeof(ICommandHandlerAsync<>));
            stringBuilder.AppendLine($"{concreteHandler.Name} -> {handlerInterface}");
            builder.RegisterType(concreteHandler).As(handlerInterface).InstancePerLifetimeScope();
        }
        _logger?.LogInformation(stringBuilder.ToString());
        
        //Выделим уникальные типы интересов обработчиков и преобразуем их в тип коллекции для резолва, чтобы при резолве извлечь ИМЕНЕНО список зависимостей.
        var interfaces4Resolve = handlers
            .Select(h => h.GetInterfaces().First(i => i.GetGenericTypeDefinition() == typeof(ICommandHandlerAsync<>)))
            .Distinct()
            .Select(handlerInterface => typeof(IList<>).MakeGenericType(handlerInterface))
            .ToList();
        
        builder.RegisterType<CommandDispatcher>()
            .WithParameters(new List<ResolvedParameter>
            {
                new ResolvedParameter(
                    (pi, ctx) => (pi.ParameterType == typeof(Dictionary<Type, IEnumerable<object>>) && (pi.Name == "handlers")),
                    (pi, ctx) =>
                    {
                        //Резолвим интерфейсы обработчиков команд и получаем сами обработчики команд.
                        var realCommandHandlers = new List<object>();
                        foreach (dynamic commandHandlers in interfaces4Resolve.Select(hi => ctx.Resolve(hi))) //заказали резолв IList<> поэтому ждем список обработчиков
                        {
                            realCommandHandlers.AddRange(commandHandlers);
                        }
                        //Список обработчиков команд преобразуем в словарь.
                        var handlersDict= realCommandHandlers
                            .GroupBy(
                                k => k.GetType().GetInterfaces().First(i => i.GetGenericTypeDefinition() == typeof(ICommandHandlerAsync<>)),
                                o => o
                                )
                            .ToDictionary(gr => gr.Key, gr => gr.AsEnumerable());
                        return handlersDict;
                    })
            }).InstancePerLifetimeScope();
    }
}
READ ALSO
Как Entity Framework отслеживает изменение объектов?

Как Entity Framework отслеживает изменение объектов?

Как Entity Framework отслеживает изменение объектов? Еще когда через методы идет вызов понятно, а как он отслеживает объект который мы получили,...

305
Нельзя выбрать базу данных

Нельзя выбрать базу данных

Вообщем на практике работаю в c# windows формах (в vs 2019)Хотел подключить базу данных

205
Бинарный поиск правой границы c#

Бинарный поиск правой границы c#

Нужно написать эффективную программу бинарного поиска поиска правой границы в упорядоченном массивеПрограмма должна удовлетворять условиям:...

189
Как превратить поле точек в GraphicsPath

Как превратить поле точек в GraphicsPath

Вопрос скорее не из программирования, а из алгоритмов

267