Как отправить в универсальный обработчик из нестандартного метода?

#c# #reflection #dependency-injection

#c# #отражение #внедрение зависимостей

Вопрос:

У меня есть метод, который нуждается в доработке, в частности, мне нужно удалить универсальный параметр в подписи. Метод получает один параметр, который всегда реализует определенный интерфейс.

Это метод:

 public void SendCommand<T>(T command) where T : ICommand
{   
    using (var scope = services.CreateScope())
    {
        var commandType = command.GetType();
        var handlerType = typeof(ICommandHandler<>).MakeGenericType(commandType);

        var service = scope.ServiceProvider.GetService(handlerType);
        (service as ICommandHandler<T>).Handle(command);
    }
}
 

Камнем преткновения является (service as ICommandHandler<T>).Handle(command) строка , которая получает параметр типа объекта, который реализует ICommand . В зависимости от фактического типа параметра получаемая служба отличается.

Есть ли какой-либо способ удалить общий параметр и использовать фактический тип параметра в качестве общего параметра ICommandHandler<T> строки?

Редактировать:

Эта переделка делает свое дело, но она приводит к довольно странному, возможно, ошибочному поведению.

 public void SendCommand(ICommand command)
{   
    using (var scope = services.CreateScope())
    {
        var commandType = command.GetType();
        var handlerType = typeof(ICommandHandler<>).MakeGenericType(commandType);

        dynamic cmd = command;
        dynamic service = scope.ServiceProvider.GetService(handlerType);

        var method = handlerType.GetMethods().Single(s => s.Name == "Handle");
        method.Invoke(service, new[] { command });

        service.Handle(cmd);
    }
}
 

Извлечение Handle метода из объекта service и вызов его вручную делают свое дело. Но использование service.Handle(cmd) вызова метода вызывает исключение (для объекта нет определения Handle ).

Это чертовски странно, потому что извлечение метода действительно работает.

Кто-нибудь может пролить свет на эту странность?

Комментарии:

1. Почему вы хотите удалить этот универсальный параметр? Вы пробовали использовать just SendCommand(ICommand command) ?

2. Я полагаю, что этот метод является реализацией интерфейса. Если это так, вы не сможете изменить сигнатуру метода, если вы также не измените этот интерфейс.

3. Камило, я пробовал SendCommand(команда ICommand), в этом случае метод не работает, потому что для ICommandHandler<T> требуется тип. Кроме того, ICommand — это пустой интерфейс, предназначенный для классов POCO, который просто служит своего рода «тегом». Эми, метод не является реализацией интерфейса.

Ответ №1:

Здесь есть несколько вариантов:

Прежде всего, если сохранение аргумента универсального типа является опцией, вы можете уменьшить сложность метода до следующего:

 public void SendCommand<T>(T command) where T : ICommand
{   
    using (var scope = services.CreateScope())
    {
        var handler = scope.ServiceProvider
            .GetRequiredService<ICommandHandler<T>>();
        handler.Handle(command);
    }
}
 

Это, конечно, не то, о чем ваш вопрос. Удаление аргумента универсального типа позволяет использовать более динамичный способ отправки команд, что полезно, когда типы команд неизвестны во время компиляции. В этом случае вы можете использовать динамическую типизацию следующим образом:

 public void SendCommand(ICommand command)
{   
    using (var scope = services.CreateScope())
    {
        var commandType = command.GetType();
        var handlerType =
            typeof(ICommandHandler<>).MakeGenericType(commandType);

        dynamic handler = scope.ServiceProvider
            .GetRequiredService(handlerType);
        handler.Handle((dynamic)command);
    }
}
 

Обратите внимание на две вещи здесь:

  1. Разрешенный обработчик хранится в dynamic переменной. Handle Следовательно, его метод является динамическим вызовом, который Handle разрешается во время выполнения.
  2. Поскольку ICommandHandler<{commandType}> не содержит Handle(ICommand) метода, command аргумент должен быть приведен к a dynamic . Это указывает привязке C #, что она должна искать любой метод с именем Handle method с одним единственным аргументом, который соответствует предоставленному типу среды выполнения command .

Этот вариант работает довольно хорошо, но у этого «динамического» подхода есть два недостатка:

  1. Отсутствие поддержки во время компиляции позволит любому рефакторингу ICommandHandler<T> интерфейса остаться незамеченным. Вероятно, это не такая уж большая проблема, поскольку ее можно легко протестировать.
  2. Любой декоратор, который применяется к любой ICommandHandler<T> реализации, должен убедиться, что он определен как общедоступный класс. Динамический вызов Handle метода (как ни странно) завершится неудачей, если класс является внутренним, поскольку связующее средство C # не обнаружит, что Handle метод ICommandHandler<T> интерфейса общедоступен.

Таким образом, вместо использования dynamic вы также можете использовать старые добрые дженерики, похожие на ваш подход:

 public void SendCommand(ICommand command)
{   
    using (var scope = services.CreateScope())
    {
        var commandType = command.GetType();
        var handlerType =
            typeof(ICommandHandler<>).MakeGenericType(commandType);

        object handler = scope.ServiceProvider.GetRequiredService(handlerType);

        var handleMethod = handlerType.GetMethods()
            .Single(s => s.Name == nameof(ICommandHandler<ICommand>.Handle));

        handleMethod.Invoke(handler, new[] { command });
    }
}
 

Это предотвращает проблемы с предыдущим подходом, поскольку это приведет к повторному рефакторингу интерфейса обработчика команд, и он может вызывать Handle метод, даже если обработчик является внутренним.

С другой стороны, это создает новую проблему. В случае, если обработчик выдает исключение, вызов MethodBase.Invoke приведет к тому, что это исключение будет обернуто в InvocationException . Это может вызвать проблемы в стеке вызовов, когда потребляющий уровень улавливает определенные исключения. В этом случае исключение должно быть сначала развернуто, что означает SendCommand утечку деталей реализации его потребителям.

Есть несколько способов исправить это, например:

 public void SendCommand(ICommand command)
{   
    using (var scope = services.CreateScope())
    {
        var commandType = command.GetType();
        var handlerType =
            typeof(ICommandHandler<>).MakeGenericType(commandType);

        object handler = scope.ServiceProvider.GetRequiredService(handlerType);

        var handleMethod = handlerType.GetMethods()
            .Single(s => s.Name == nameof(ICommandHandler<ICommand>.Handle));

        try
        {        
            handleMethod.Invoke(handler, new[] { command });
        }
        catch (InvocationException ex)
        {
            throw ex.InnerException;
        }
    }
}
 

Недостатком этого подхода, однако, является то, что вы теряете трассировку стека исходного исключения, поскольку это исключение повторно отбрасывается (что обычно не является хорошей идеей). Итак, вместо этого вы можете сделать следующее:

 public void SendCommand(ICommand command)
{   
    using (var scope = services.CreateScope())
    {
        var commandType = command.GetType();
        var handlerType =
            typeof(ICommandHandler<>).MakeGenericType(commandType);

        object handler = scope.ServiceProvider.GetRequiredService(handlerType);

        var handleMethod = handlerType.GetMethods()
            .Single(s => s.Name == nameof(ICommandHandler<ICommand>.Handle));

        try
        {        
            handleMethod.Invoke(handler, new[] { command });
        }
        catch (InvocationException ex)
        {
            ExceptionDispatchInfo.Capture(ex.InnerException).Throw();
        }
    }
}
 

При этом используется .NET 4.5 ExceptionDispatchInfo , который также доступен в .NET Core 1.0 и выше и .NET Standard 1.0.

В качестве последнего варианта вы также можете вместо разрешения ICommandHandler<T> разрешить тип оболочки, который реализует универсальный интерфейс. Это делает типы кода безопасными, но заставляет вас регистрировать дополнительный универсальный тип оболочки. Это выглядит следующим образом:

 public void SendCommand(ICommand command)
{   
    using (var scope = services.CreateScope())
    {
        var commandType = command.GetType();
        var wrapperType =
            typeof(CommandHandlerWrapper<>).MakeGenericType(commandType);

        var wrapper = (ICommandHandlerWrapper)scope.ServiceProvider
            .GetRequiredService(wrapperType);

        wrapper.Handle(command);
    }
}

public interface ICommandHandlerWrapper
{
    void Handle(ICommand command);
}

public class CommandHandlerWrapper<T> : ICommandHandlerWrapper
    where T : ICommand
{
    private readonly ICommandHandler<T> handler;
    public CommandHandlerWrapper(ICommandHandler<T> handler) =>
        this.handler = handler;

    public Handle(ICommand command) => this.handler.Handle((T)command);
}

// Extra registration
services.AddTransient(typeof(CommandHandlerWrapper<>));
 

Комментарии:

1. Очень информативный пост, спасибо! Теперь вот в чем дело: ваше первое предложенное решение (с использованием dynamics) завершается неудачей с моей стороны, фактически это одно из неудачных решений, которые я пытался выполнить перед публикацией в StackOverflow. Каким-то образом связующему не удается распознать, что рассматриваемый объект содержит метод дескриптора. Часы распознают объект как имеющий правильный тип (AddUserCommandHandler), но каким-то образом вызов метода вызывает исключение при вызове (именно поэтому мне пришлось извлечь метод и вызвать его вручную)

2. Обновление, я нашел причину сбоя динамического вызова. Классы Command и CommandHandler были определены как частные. Изменение их на общедоступные делает свое дело. (определение их как внутренних по-прежнему вызывает исключение)

3. @JorgeLuisRodriguezPerez это второй недостаток, который я отметил для динамического подхода.