如何从非泛型方法调度到泛型处理程序?

时间:2019-04-16 18:29:13

标签: c# reflection dependency-injection

我有一个方法需要修改,特别是我需要删除签名中的通用参数。该方法接收单个参数,该参数始终实现特定的接口。

这是方法:

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的定义)。

这很奇怪,因为提取方法确实是可行的。

任何人都可以发现这种怪异吗?

1 个答案:

答案 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);
    }
}

请注意以下两点:

  1. 已解析的处理程序存储在dynamic变量中。因此,其Handle方法是一种动态调用,其中Handle是在运行时解析的。
  2. 由于ICommandHandler<{commandType}>不包含Handle(ICommand)方法,因此需要将command参数强制转换为dynamic。这指示C#绑定应查找名为Handle方法的 any 方法,该方法具有一个与提供的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<>));