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