如何注册通用 Action 或 Command 处理程序,然后在运行时确定类型时调用正确的处理程序?

How do I register generic Action or Command handlers and then call the right one when the type is determined at runtime?

我正在寻找一种方法来实现以下内容。

我希望能够在我的 DI 容器中将“ActionHandlers”注册为服务,为此我制作了以下接口:

public interface IActionHandler {
  Task HandleAsync(IAction action);
}

public interface IActionHandler<T> : IActionHandler where T : IAction, new() {
  Task HandleAsync(T action);
}

当时我的想法是创建一个名为 ActionHandlerContainer 的派生类型:

public class ActionHandlerContainer : IActionHandler {
  private readonly Dictionary<Type, IActionHandler> _handlers;


  public ActionHandlerContainer(IEnumerable<IActionHandler<??T??>>) {

   // What to do here?.
   // As this ActionHandlerContainer class is registered as a service,
   // I expect the DI container in return to inject all my ActionHandler services here.
  }
  public Task HandleAsync(IAction action) {

    // Fetch appropriate handler from the map.
    _handlers.TryGetValue(action.getType(), out var actionHandler);
    
    if(actionHandler == null) {
      throw new Exception($"No handler could be found for the Action of type: {action.GetType()}");
    }

    // Invoke the correct handler.
    return actionHandler.HandleAsync(action);

  }
}

它将执行任何操作并将其委托给正确的处理程序,示例处理程序可能如下所示:

public class SetColorActionHandler : IActionHandler<SetColorAction> {

public async Task HandleAsync(SetColorAction action) {
  return await ComponentManager.SetComponentColor(action);
}

}

DI 容器服务注册看起来像这样(我想)

builder.Services.AddTransient<IActionHandler<SetColorAction>, SetColorActionHandler>();
builder.Services.AddSingleton<ActionHandlerContainer>();

我自己的一些悬而未决的问题是:

我现在遇到的一个问题是,如果 IActionHandler 实现了 IActionHandler,那么任何 IActionHandler 实现都必须实现看起来重复的 code async Task HandleAsync(IAction action) 方法。

所以我的问题是,鉴于代码示例和解释,我该如何正确实施?

在此先感谢您的帮助, 尼基.

[编辑1]: 我在 ActionHandlerContainer::HandleAsync

中尝试了以下操作
public Task HandleAsync(IAction action) {
  Type runtimeType = action.GetType();
  var _handler = _serviceProvider.GetService(typeof(IActionHandler<runtimeType>));
}

但这似乎不起作用。

[编辑2]: 为 vendettamit 提供一些背景信息:

public class MqttClientWrapper {

...<omitted>

private Task ClientOnApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs arg) {

Console.WriteLine("The client received an application message.");
        arg.DumpToConsole();
        
        // Create the ActionPayload from the MQTT Application Message's Payload.
        var actionPayload = ActionPayload.FromPayload(arg.ApplicationMessage.Payload);
        
        // Grab the correct action type from the map according to the identifier in the payload.
        var actionType = ActionMap.ActionIdentifierToActionType[actionPayload.ActionIdentifier];
        
        // Now instruct the factory to instantiate that type of action.
        var action = _actionFactory.CreateAction(actionPayload.ActionData, actionType);
        
        // Finally, pass on the action to the correct handler.
        _actionHandlerContainer.HandleAsync(action);

        return Task.CompletedTask;

}

}

所以我喜欢什么:

你在想“Open/Closed原则”。不错。

我不喜欢的地方。 您的词典将“类型”作为键。因此,您试图将每个类型存储到它的 1:N IActionHandler 的一个位置。 (顺便说一句,如果您尝试为单个类型注册 2:N IActionHandlers,您可能会收到 Key-Exists 错误(或者它只会覆盖单个错误)。

我会走向:

public class ActionHandlerContainer<T> : IActionHandler<T> { 
  private readonly IDictionary<int, IActionHandler<T>> _handlers;

并将您的具体信息“注入”到单个 T 1:N 具体处理程序。

注意,我已经删除了“Type”,并更改为 int?为什么是整数?因为如果你想控制注入项的顺序。您可以遍历 IDictionary (int) 键(按顺序)。

那么结果如何呢?

您没有注册单个 ActionHandlerContainer(包含所有类型)

你注册类似

(ioc注册以下)

  ActionHandlerContainer<Employee> 

你的构造函数将你的 1:N EmployeeHandler(s) 注入到上面。

然后你注册类似(不同的“类型”)

(ioc register the below)
ActionHandlerContainer<Candy>
   

您没有将泛型用作“一切类型的持有者”。您使用泛型来减少代码副本。 (所以你不必只为 Employee 写一份“副本”,而为“Candy”写另一份......

你需要在某处注入

   ActionHandlerContainer<Employee> ahce

...

public员工经理:IEmployeeManager

 private readonly ActionHandlerContainer<Employee> theAhce;

  public EmployeeManager(ActionHandlerContainer<Employee> ahce)

{ this.thheAhce = ahce; /* 简单的代码,你实际上应该在输入 ahce 上检查 null 以确保它不为 null */ }

public void UpdateEmployee(Employee emp)

{ this.theAhce.Invoke(雇员); /* << 这当然会 运行 你的 1:N EMPLOYEE 处理程序 */ }

类似的东西。

恕我直言,摆脱“拥有所有类型”的心态。

一个建议:使用MediatR。过去我曾反对它,但我已经软化了。它并不完美,但它是您要解决的问题的彻底实现。

或者,这里有一个详细的自定义方法。


正如您在问题中指出的那样,问题是如果您要有 IActionHandlers<T> 的 collection 但每个 T 都不同,那么什么是collection?

的类型

任何解决方案都会涉及某种反思或type-checking。 (这是不可避免的。您从 IAction 开始,但您不想要 IActionHandler<IAction> - 您想要针对 IAction 的特定实现的单独处理程序。)即使我不通常使用 object,只要我的代码确保 object 是预期的类型就可以了。也就是说,给定类型 T,您将得到一个 object 可以转换为 IActionHandler<T>.

这是一种方法。我使用术语“命令”和“命令处理程序”而不是“动作”和“动作处理程序”。涉及一些反思,但它完成了工作。即使它不是您所需要的,它也可能会给您一些想法。

首先是一些接口:

    public interface ICommand
    {
    }

    public interface ICommandHandler
    {
        Task HandleAsync(ICommand command);
    }

    public interface ICommandHandler<TCommand> where TCommand : ICommand
    {
        Task HandleAsync(TCommand command);
    }
  • ICommand标记了一个class,用作命令。
  • ICommandHandler 是 class 的接口,它接受任何 ICommand 并将其“路由”到特定的命令处理程序。它相当于你问题中的 IActionHandler
  • ICommandHandler<T> 是 type-specific 命令处理程序的接口。

下面是 ICommandHandler 的实现。这得

  • 收到指令
  • 解析该命令类型的具体处理程序实例
  • 调用处理程序,将命令传递给它。
    public class CommandHandler : ICommandHandler
    {
        private readonly Func<Type, object> _getHandler;

        public CommandHandler(Func<Type, object> getHandler)
        {
            _getHandler = getHandler;
        }

        public async Task HandleAsync(ICommand command)
        {
            var commandHandlerType = GetCommandHandlerType(command);
            var handler = _getHandler(commandHandlerType);
            await InvokeHandler(handler, command);
        }

        private Type GetCommandHandlerType(ICommand command)
        {
            return typeof(ICommandHandler<>).MakeGenericType(command.GetType());
        }

        // See the notes below. This reflection could be "cached"
        // in a Dictionary<Type, MethodInfo> so that once you find the "handle" 
        // method for a specific type you don't have to repeat the reflection.
        private async Task InvokeHandler(object handler, ICommand command)
        {
            var handlerMethod = handler.GetType().GetMethods()
                .Single(method => IsHandleMethod(method, command.GetType()));
            var task = (Task)handlerMethod.Invoke(handler, new object[] { command });
            await task.ConfigureAwait(false);
        }

        private bool IsHandleMethod(MethodInfo method, Type commandType)
        {
            if (method.Name != nameof(ICommandHandler.HandleAsync)
                || method.ReturnType != typeof(Task))
            {
                return false;
            }

            var parameters = method.GetParameters();
            return parameters.Length == 1 && parameters[0].ParameterType == commandType;
        }
    }

调用 public async Task HandleAsync(ICommand command) 时执行以下操作:

  • 确定命令处理程序的通用类型。如果命令类型是 FooCommand 那么通用命令处理程序类型是 ICommandHandler<FooCommand>.
  • 调用 Func<Type, object> _getHandler 以获取命令处理程序的具体实例。该函数被注入。该功能的实现是什么?稍后会详细介绍。但重点是,就这个 class 而言,它可以将处理程序类型传递给此函数并取回一个处理程序。
  • 找到处理程序类型的“handle”方法。
  • 调用具体处理程序的 handle 方法,传递命令。

这里还有改进的余地。一旦它找到一个类型的方法,它就可以将它添加到 Dictionary<Type, MethodInfo> 以避免再次反射。它甚至可以创建一个执行整个调用的函数并将其添加到 Dictionary<Type, Func<Object, Task>。这些中的任何一个都会提高性能。

(如果这听起来令人费解,那么这又是考虑使用 MediatR 的一个原因。有一些我不喜欢的细节,但是所有这些都起作用。它还处理更复杂的场景,例如处理程序return 使用 CancellationToken 的东西或处理程序。)

这就留下了问题 - 什么是 Func<Type, object> 采用命令类型并且 return 是正确的命令处理程序?

如果您使用的是 IServiceCollection/IServiceProvider,这些扩展会注册并提供所有内容。 (关键是注入函数意味着我们没有绑定到特定的 IoC 容器。)

    public static class CommandHandlerServiceCollectionExtensions
    {
        public static IServiceCollection AddCommandHandling(this IServiceCollection services)
        {
            services.AddSingleton<ICommandHandler>(provider => new CommandHandler(handlerType =>
                {
                    return provider.GetRequiredService(handlerType);
                }
            ));
            return services;
        }

        public static IServiceCollection AddHandlersFromAssemblyContainingType<T>(this IServiceCollection services)
            where T : class
        {
            var assembly = typeof(T).Assembly;
            IEnumerable<Type> types = assembly.GetTypes().Where(type => !type.IsAbstract && !type.IsInterface);
            foreach (Type type in types)
            {
                Type[] typeInterfaces = type.GetInterfaces();
                foreach (Type typeInterface in typeInterfaces)
                {
                    if (typeInterface.IsGenericType && typeInterface.GetGenericTypeDefinition() == typeof(ICommandHandler<>))
                    {
                        services.AddScoped(typeInterface, type);
                    }
                }
            }
            return services;
        }
    }

第一个方法注册CommandHandler作为ICommandHandler的实现。 Func<Type, object>的实现是

handlerType =>
{
    return provider.GetRequiredService(handlerType);
}

换句话说,无论处理程序类型是什么,都从 IServiceProvider 解析它。如果类型是 ICommandHander<FooCommand> 那么它将解析该接口的任何注册实现。

这种在运行时对 IServiceProvider 的依赖不是服务定位器。 (一切最终都取决于它在运行时。)CommandHandler 取决于抽象 - 函数 - 而不是 IServiceProvider。 IoC容器的使用都在composition root里面

您可以手动注册这些实现中的每一个:

serviceCollection.AddScoped<ICommandHander<FooCommand>, FooCommandHandler>();

...等第二个扩展为你做。它发现 ICommandHandler<T> 的实现并将它们注册到 IServiceCollection.

我在生产代码中使用过这个。如果我想让它更健壮,我会添加对取消标记和 return 类型的处理。 (我可能会懒惰并争辩说 return 类型违反了 command/query 分离。)它需要更新 InvokeHandler 如何在处理程序上选择要调用的方法。

因为没有测试是不完整的,这里有一个测试。 这很复杂。它创建一个包含列表和数字的命令。命令处理程序将数字添加到列表中。关键是它是可观察的。仅当处理程序被注册、解析和调用以便将号码添加到列表中时,测试才会通过。

    [TestClass]
    public class CommandHandlerTests
    {
        [TestMethod]
        public async Task CommandHandler_Invokes_Correct_Handler()
        {
            var serviceCollection = new ServiceCollection();
            serviceCollection
                .AddCommandHandling()
                .AddHandlersFromAssemblyContainingType<AddNumberToListCommand>();

            var serviceProvider = serviceCollection.BuildServiceProvider();
            var commandHandler = serviceProvider.GetRequiredService<ICommandHandler>();

            var list = new List<int>();
            var command = new AddNumberToListCommand(list, 1);

            // This is the non-generic ICommandHandler interface
            await commandHandler.HandleAsync(command);
            Assert.IsTrue(list.Contains(1));
        }
    }

    public class AddNumberToListCommand : ICommand
    {
        public AddNumberToListCommand(List<int> listOfNumbers, int numberToAdd)
        {
            ListOfNumbers = listOfNumbers;
            NumberToAdd = numberToAdd;
        }

        public List<int> ListOfNumbers { get; }
        public int NumberToAdd { get; }
    }

    public class AddNumberToListHandler : ICommandHandler<AddNumberToListCommand>
    {
        public Task HandleAsync(AddNumberToListCommand command)
        {
            command.ListOfNumbers.Add(command.NumberToAdd);
            return Task.CompletedTask;
        }
    }