简体   繁体   English

如何从非泛型方法分派到泛型处理程序?

[英]How to dispatch to generic handler from a non-generic method?

I have a method that needs a rework, specifically I need to remove the generic parameter in the signature.我有一个方法需要返工,特别是我需要删除签名中的通用参数。 The method receives a single parameter, which always implements a specific interface.该方法接收一个参数,该参数始终实现特定接口。

This is the method:这是方法:

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

The sticking point is the (service as ICommandHandler<T>).Handle(command) , line, which receives a type parameter of an object that implements ICommand .症结在于(service as ICommandHandler<T>).Handle(command)行,它接收实现ICommand的对象的类型参数。 Depending on the parameter actual type, the service retrieved is different.根据参数的实际类型,检索到的服务是不同的。

Is there any way to remove the generic parameter, and use the actual type of the parameter as the generic parameter of the ICommandHandler<T> line?有什么方法可以删除泛型参数,并使用参数的实际类型作为ICommandHandler<T>行的泛型参数?

EDIT:编辑:

This rework does the trick, but it exposes a pretty weird, perhaps buggy, behavior.这个返工可以解决问题,但它暴露了一个非常奇怪的,可能是错误的行为。

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

Extracting the Handle method from the service object and invoking it manually does the trick.从服务对象中提取Handle方法并手动调用它就可以了。 But using the service.Handle(cmd) method call throws an exception (object does not have a definition for Handle ).但是使用service.Handle(cmd)方法调用会引发异常(对象没有Handle的定义)。

This is weird as hell, because extracting the method does work.这太奇怪了,因为提取方法确实有效。

Anyone can shed light on this weirdness?任何人都可以阐明这种怪异现象吗?

There are a few options here:这里有几个选项:

First of all, if keeping the generic type argument is an option, you can reduce the method's complexity to the following:首先,如果保留泛型类型参数是一个选项,您可以将方法的复杂性降低到以下程度:

public void SendCommand<T>(T command) where T : ICommand
{   
    using (var scope = services.CreateScope())
    {
        var handler = scope.ServiceProvider
            .GetRequiredService<ICommandHandler<T>>();
        handler.Handle(command);
    }
}

This is, of course, not what your question is about.当然,这不是你的问题。 Removing the generic type argument allows a more dynamic way of dispatching commands, which is useful when command types are not known at compile time.删除泛型类型参数允许以更动态的方式分派命令,这在编译时未知命令类型时很有用。 In that case you can use dynamic typing, as follows:在这种情况下,您可以使用动态类型,如下所示:

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

Notice two things here:这里注意两件事:

  1. The resolved handler is stored in a dynamic variable.已解析的处理程序存储在dynamic变量中。 Its Handle method is, therefore, a dynamic invocation where Handle is resolved at runtime.因此,它的Handle方法是一个动态调用,其中Handle在运行时解析。
  2. Since ICommandHandler<{commandType}> does not contain a Handle(ICommand) method, the command argument needs to be cast to a dynamic .由于ICommandHandler<{commandType}>不包含Handle(ICommand)方法,因此command参数需要转换为dynamic的。 This instructs the C# binding that it should look for any method named Handle method with one single argument that matches the supplied runtime type of command .这指示 C# 绑定它应该查找任何名为Handle method 的方法,其中一个参数与提供的运行时类型的command相匹配。

This option works pretty well, but there are two downsides to this 'dynamic' approach:此选项效果很好,但这种“动态”方法有两个缺点:

  1. The lack of compile-time support will let any refactoring to the ICommandHandler<T> interface get unnoticed.缺少编译时支持将使对ICommandHandler<T>接口的任何重构都不会引起注意。 This is probably not a huge problem, as it can easily be unit tested.这可能不是一个大问题,因为它可以很容易地进行单元测试。
  2. Any decorator that gets applied to any ICommandHandler<T> implementation needs to ensure that it is defined as a public class.应用于任何ICommandHandler<T>实现的任何装饰器都需要确保将其定义为公共类。 The dynamic invocation of the Handle method will (weirdly) fail when the class is internal, as the C# binder won't spot that the Handle method of the ICommandHandler<T> interface is publicly accessible.当类是内部类时, Handle方法的动态调用将(奇怪地)失败,因为 C# 绑定器不会发现ICommandHandler<T>接口的Handle方法是可公开访问的。

So instead of using dynamic, you can also use good old generics, similar to your approach:因此,除了使用动态之外,您还可以使用良好的旧泛型,类似于您的方法:

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

This prevents the problems with the previous approach, as this will surfive refactoring of the command handler interface, and it can invoke the Handle method even if the handler is internal.这可以防止以前方法出现的问题,因为这将继续重构命令处理程序接口,并且即使处理程序是内部的,它也可以调用Handle方法。

On the other hand, it does introduce a new problem.另一方面,它确实引入了一个新问题。 In case a handler throws an exception, the call to MethodBase.Invoke will cause that exception to be wrapped in an InvocationException .如果处理程序抛出异常,则对MethodBase.Invoke的调用将导致将该异常包装在InvocationException中。 This can cause trouble up the call stack, when the consuming layer catches certain exceptions.当消费层捕获某些异常时,这可能会导致调用堆栈出现问题。 In that case the exception should first be unwrapped, which means SendCommand is leaking implementation details to its consumers.在那种情况下,应该首先解包异常,这意味着SendCommand正在向其消费者泄露实现细节。

There are several ways to fix this, for instance:有几种方法可以解决这个问题,例如:

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;
        }
    }
}

Downside of this approach, however, is that you lose the stack trace of the original exception, as this exception is rethrown (which is typically not a good idea).然而,这种方法的缺点是您丢失了原始异常的堆栈跟踪,因为该异常被重新抛出(这通常不是一个好主意)。 So instead, you can do the following:因此,您可以执行以下操作:

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();
        }
    }
}

This makes use of .NET 4.5's ExceptionDispatchInfo which is also available under .NET Core 1.0 and up and .NET Standard 1.0.这利用了 .NET 4.5 的ExceptionDispatchInfo ,它在 .NET Core 1.0 及更高版本和 .NET Standard 1.0 下也可用。

As a last option, you can also, instead of resolving ICommandHandler<T> , resolve a wrapper type that implements a non-generic interface.作为最后一个选项,您还可以解析实现非泛型接口的包装器类型,而不是解析ICommandHandler<T> This makes the code types safe, but does force you to register the extra generic wrapper type.这使代码类型安全,但确实会强制您注册额外的通用包装器类型。 This goes as follows:过程如下:

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<>));

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM