CQRS架构下如何避免用Switch Case调用命令处理器?
替代Azure Function中Switch-Case的CQRS命令处理器方案
这是个非常典型的CQRS场景——当命令处理器数量超过10个后,switch-case会快速变得臃肿、难以维护,每次新增命令都要修改核心逻辑,完全违反了开闭原则。我给你推荐两种适配Azure Function环境的优雅替代方案,核心思路都是依赖注入+策略模式:
方案一:依赖注入(DI)+ 动态类型解析(推荐)
如果你的Azure Function使用的是.NET Core 3.1+版本(支持内置DI),这是最优解。它能让你新增命令处理器时,只需要实现接口并注册,完全不需要修改Function的核心代码。
步骤1:注册所有命令处理器到DI容器
首先创建Startup.cs配置DI(如果还没有的话):
using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection; [assembly: FunctionsStartup(typeof(YourAppNamespace.Startup))] namespace YourAppNamespace { public class Startup : FunctionsStartup { public override void Configure(IFunctionsHostBuilder builder) { // 注册所有命令处理器(示例) builder.Services.AddScoped<ICommandHandler<CardBlockCommand>, CardBlockCommandHandler>(); builder.Services.AddScoped<ICommandHandler<CardCancelledCommand>, CardCancelledCommandHandler>(); builder.Services.AddScoped<ICommandHandler<CardUnBlockCommand>, CardUnBlockCommandHandler>(); // 剩下的12+处理器依次注册... // 注册处理器解析服务 builder.Services.AddScoped<ICommandHandlerResolver, CommandHandlerResolver>(); } } }
步骤2:实现处理器解析器
创建一个通用的解析器,用来根据命令类型动态获取对应的处理器:
public interface ICommandHandlerResolver { // 动态处理任意命令 void HandleCommand(ICommand command); } public class CommandHandlerResolver : ICommandHandlerResolver { private readonly IServiceProvider _serviceProvider; // 缓存命令类型与处理器类型的映射,避免重复反射 private static readonly Dictionary<Type, Type> _handlerTypeCache = new(); public CommandHandlerResolver(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public void HandleCommand(ICommand command) { var commandType = command.GetType(); // 从缓存获取或构建处理器类型 if (!_handlerTypeCache.TryGetValue(commandType, out var handlerType)) { handlerType = typeof(ICommandHandler<>).MakeGenericType(commandType); _handlerTypeCache[commandType] = handlerType; } // 从DI获取处理器实例并调用Handle方法 var handler = _serviceProvider.GetRequiredService(handlerType); var handleMethod = handlerType.GetMethod(nameof(ICommandHandler<ICommand>.Handle)); handleMethod?.Invoke(handler, new[] { command }); } }
步骤3:修改Azure Function代码替换Switch-Case
现在只需要注入解析器,一行代码就能处理所有命令:
[FunctionName("ReceiveEvent")] public static async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req, ILogger log, // 注入处理器解析器 ICommandHandlerResolver handlerResolver) { IActionResult actionResult = null; string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); var command = await _commandMapper.Map(requestBody); if (_commandValidator.Validate(req, command, ref actionResult)) { // 替换所有switch-case的核心代码 handlerResolver.HandleCommand(command); return actionResult; } return actionResult ?? new BadRequestResult(); }
方案二:预构建处理器字典(兼容旧版Azure Function)
如果你的Azure Function版本不支持DI,或者你需要更简单的过渡方案,可以用字典来维护事件类型与处理器的映射:
// 在Function类中静态初始化字典(只执行一次) private static readonly Dictionary<EventType, Action<ICommand>> _commandHandlerMap = new() { { EventType.CARD_BLOCK, cmd => _cardBlockCommandHandler.Handle(cmd as CardBlockCommand) }, { EventType.CARD_CANCEL, cmd => _cardCancelledCommandHandler.Handle(cmd as CardCancelledCommand) }, { EventType.CARD_UNBLOCK, cmd => _cardUnBlockHandler.Handle(cmd as CardUnBlockCommand) }, // 其他处理器依次添加... }; // 在Function方法中调用 if (_commandHandlerMap.TryGetValue(command.EventType, out var handler)) { handler(command); }
这种方式比switch-case更易维护,但新增处理器时仍需手动更新字典,适合临时过渡。
方案优势对比
| 特性 | DI+动态解析方案 | 字典映射方案 |
|---|---|---|
| 符合开闭原则 | ✅ 完全符合 | ❌ 需要修改字典 |
| 扩展性(新增处理器) | ✅ 只需实现+注册 | ❌ 需修改字典 |
| 类型安全性 | ✅ 反射保证类型匹配 | ⚠️ 需要手动强转 |
| 维护成本 | 极低 | 中等 |
额外优化建议
- 如果你希望
Handle方法支持异步,可以修改ICommandHandler接口为异步版本:
然后在解析器中调用异步方法即可。public interface ICommandHandler<TCommand> where TCommand : ICommand { Task Handle(TCommand command); } - 可以添加全局异常处理,在解析器中捕获处理器的异常并统一日志记录。
内容的提问来源于stack exchange,提问作者Rakesh Kumar
相关产品推荐
相关产品推荐

