如何注册通用操作或命令处理程序,然后在运行时确定类型时调用正确的处理程序



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

我希望能够在我的 DI 容器中将"操作处理程序"注册为服务,为此,我创建了以下接口:

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

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

  • 如果多个操作处理程序能够注册到一个操作。
  • 是否可以在此之上实现装饰器模式,假设我想要一个装饰原始 SetColorActionHandler 的 ConsoleDebugLoggingSetColorActionHandler。

我现在遇到的一个问题是,如果 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]: 为仇杀提供一些背景信息:

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

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

或者,下面是一个详细的自定义方法。


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

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

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

首先,一些接口:

public interface ICommand
{
}
public interface ICommandHandler
{
Task HandleAsync(ICommand command);
}
public interface ICommandHandler<TCommand> where TCommand : ICommand
{
Task HandleAsync(TCommand command);
}
  • ICommand标记用作命令的类。
  • ICommandHandler是类的接口,该类采用任何ICommand并将其"路由"到特定的命令处理程序。这相当于你的问题中的IActionHandler
  • ICommandHandler<T>是特定于类型的命令处理程序的接口。

这是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以获取命令处理程序的具体实例。注入该函数。该函数的实现是什么?稍后会详细介绍。但关键是,就此类而言,它可以将处理程序类型传递给此函数并返回处理程序。
  • 查找处理程序类型的"句柄"方法。
  • 在具体处理程序上调用句柄方法,传递命令。

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

(如果这听起来很复杂,那么这再次成为考虑使用MediatR的原因。有一些关于它的细节我不喜欢,但做了所有这些工作。它还处理更复杂的方案,例如返回某些内容的处理程序或使用CancellationToken的处理程序。

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

如果您使用的是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 容器的使用全部在组合根中。

您可以手动注册其中每个实现:

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

。等。第二个扩展程序会为您执行此操作。它发现ICommandHandler<T>的实现并将其注册到IServiceCollection

我已经在生产代码中使用它。如果我希望它更健壮,我会添加取消令牌的处理,也许还会添加返回类型。(我可能会偷懒,并认为返回类型违反了命令/查询分离。这将需要更新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;
}
}

所以我喜欢:

您正在考虑"开放/封闭原则"。 很好。

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

我会转向:

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

并将您的特定"注入"单个 T 1:N 混凝土处理程序。

注意,我已经删除了"类型",并更改为 int? 为什么是整数? 因为如果你想控制注入项目的顺序。 您可以循环访问 IDictionary (int) 键(按顺序)。

那么这将如何发挥作用呢?

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

您注册了类似

(国际奥委会注册如下)

ActionHandlerContainer<Employee> 

并且您将构造函数将 1:N 员工处理程序注入上述内容。

然后你注册类似的东西(不同的"类型")

(ioc register the below)
ActionHandlerContainer<Candy>

您没有使用泛型作为"所有内容的类型持有者"。 使用泛型来减少代码副本。(所以你不必只为员工写一个"副本",为"糖果"写另一个副本.....

在您需要注入的地方

ActionHandlerContainer<Employee> ahce

公共员工经理 : IEmployee经理

private readonly ActionHandlerContainer<Employee> theAhce;
public EmployeeManager(ActionHandlerContainer<Employee> ahce)

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

public void UpdateEmployee(Employee emp)

{ this.theAhce.Invoke(emp);/* <<,这当然会运行您的 1:N 员工处理程序 */}

类似的东西。

恕我直言,摆脱"持有所有类型的"心态。

最新更新