Как отправить в универсальный обработчик из неуниверсального метода? - PullRequest
1 голос
/ 16 апреля 2019

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

Это метод:

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>?

EDIT:

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

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.Handle(cmd) вызывает исключение (объект не имеет определения для Handle).

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

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

1 Ответ

2 голосов
/ 17 апреля 2019

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

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

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 необходимо привести к dynamic. Это указывает привязке C #, что она должна искать любой метод с именем Handle метод с одним единственным аргументом, который соответствует предоставленному типу времени выполнения command.

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

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

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

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<>));
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...