Здесь есть несколько вариантов:
Прежде всего, если опция общего аргумента типа является опцией, вы можете уменьшить сложность метода до следующего:
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);
}
}
Обратите внимание на две вещи:
- Разрешенный обработчик хранится в переменной
dynamic
. Поэтому его метод Handle
является динамическим вызовом, где Handle
разрешается во время выполнения.
- Поскольку
ICommandHandler<{commandType}>
не содержит метод Handle(ICommand)
, аргумент command
необходимо привести к dynamic
. Это указывает привязке C #, что она должна искать любой метод с именем Handle
метод с одним единственным аргументом, который соответствует предоставленному типу времени выполнения command
.
Этот параметр работает довольно хорошо, но у этого «динамического» подхода есть два недостатка:
- Отсутствие поддержки времени компиляции позволит любому рефакторингу интерфейса
ICommandHandler<T>
остаться незамеченным. Вероятно, это не очень большая проблема, так как она может быть легко протестирована модулем.
- Любой декоратор, который применяется к любой реализации
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<>));