我有一个方法需要修改,特别是我需要删除签名中的通用参数。该方法接收单个参数,该参数始终实现特定的接口。
这是方法:
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.Handle(cmd)
方法调用会引发异常(对象没有Handle
的定义)。
这很奇怪,因为提取方法确实是可行的。
任何人都可以发现这种怪异吗?
答案 0 :(得分:2)
这里有一些选择:
首先,如果保留泛型类型参数是一种选择,则可以将方法的复杂度降低到以下水平:
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
方法的 any 方法,该方法具有一个与提供的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<>));