如何注册通用 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>();
我自己的一些悬而未决的问题是:
- 是否可以将多个 ActionHandlers 注册到一个动作..
- 是否可以在此基础上实现装饰器模式,比如我想要一个装饰原始 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]:
为 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;
}
}
我正在寻找一种方法来实现以下内容。
我希望能够在我的 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>();
我自己的一些悬而未决的问题是:
- 是否可以将多个 ActionHandlers 注册到一个动作..
- 是否可以在此基础上实现装饰器模式,比如我想要一个装饰原始 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]: 为 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;
}
}