【.NET后端工具系列】MediatR与CQRS:构建解耦的领域驱动设计架构

张开发
2026/6/8 12:10:47 15 分钟阅读
【.NET后端工具系列】MediatR与CQRS:构建解耦的领域驱动设计架构
1. 为什么需要MediatR和CQRS在传统的.NET应用开发中我们经常会遇到业务逻辑高度耦合的问题。想象一下一个电商系统的订单处理模块可能同时包含了订单创建、库存更新、支付处理、物流通知等十几种操作。这些代码如果全部堆积在Controller或Service层很快就会变成难以维护的上帝类。我去年接手过一个遗留项目OrderService.cs文件足足有5000多行代码每次修改都要小心翼翼生怕牵一发而动全身。这就是典型的高耦合架构带来的维护噩梦。而MediatR配合CQRS模式就像是为这种场景量身定制的解药。CQRSCommand Query Responsibility Segregation的核心思想很简单把读写操作彻底分离。读操作Query只负责获取数据写操作Command只负责修改状态。这种分离带来的直接好处是读写模型可以独立优化比如查询可以用DTO直接返回命令可以用领域模型严格校验系统复杂度被天然分割更容易实现性能优化比如读写分离数据库而MediatR作为中介者模式的.NET实现完美适配CQRS架构。它就像个智能路由器把命令/查询请求自动分发给对应的处理器让各个组件不需要知道彼此的存在。这种解耦程度在我实践DDD领域驱动设计时简直是如虎添翼。2. MediatR的核心机制解析2.1 中介者模式的实战价值中介者模式在MediatR中的实现堪称教科书级别。记得我第一次用MediatR改造旧系统时原本错综复杂的服务引用关系变成了清晰的星型结构。举个例子原本用户注册需要调用用户服务创建账号邮件服务发送验证码积分服务初始化积分推荐服务处理邀请关系改造后只需要发送一个RegisterUserCommand各个处理器会自动响应。这种转变带来的维护便利性在后续需求变更时体现得淋漓尽致。当需要新增注册审计功能时我只需要添加一个新的Handler完全不用修改原有代码。MediatR内部通过类型系统实现智能路由。当你调用_mediator.Send(command)时它会根据command类型扫描已注册的IRequestHandler通过依赖注入获取handler实例执行handler的Handle方法返回处理结果如果有整个过程对开发者完全透明这种设计让代码组织变得异常清晰。我在团队内部推行时新成员最快半小时就能上手因为每个业务功能的入口和出口都变得高度可预测。2.2 两种消息模式的适用场景MediatR的Request/Response和Notification模式我在实际项目中是这样分工的Request/Response模式最适合需要明确返回值的操作如获取用户详情需要事务性保证的写操作如创建订单需要明确错误处理的场景典型代码示例public class GetUserQuery : IRequestUserDto { public int UserId { get; set; } } public class GetUserHandler : IRequestHandlerGetUserQuery, UserDto { public async TaskUserDto Handle(GetUserQuery request, CancellationToken ct) { // 数据库查询等操作 return await _db.Users.FindAsync(request.UserId); } }Notification模式则擅长处理需要触发多个后续动作的场景如订单创建后发邮件、更新库存不需要即时返回结果的操作事件溯源Event Sourcing场景实际项目中的典型应用public class OrderCreatedEvent : INotification { public int OrderId { get; } public OrderCreatedEvent(int orderId) OrderId orderId; } // 库存处理Handler public class InventoryHandler : INotificationHandlerOrderCreatedEvent { public async Task Handle(OrderCreatedEvent notification, CancellationToken ct) { await _inventoryService.UpdateStock(notification.OrderId); } } // 物流处理Handler public class ShippingHandler : INotificationHandlerOrderCreatedEvent { public async Task Handle(OrderCreatedEvent notification, CancellationToken ct) { await _shippingService.ScheduleDelivery(notification.OrderId); } }3. 领域驱动设计中的最佳实践3.1 聚合根与领域事件在DDD实践中MediatR最亮眼的表现是处理领域事件。以电商系统为例当订单聚合根状态变更时传统的做法是在Service层显式调用各种处理逻辑。而使用MediatR后聚合根本身只需要发布事件public class Order : AggregateRoot { public void ConfirmPayment() { // 修改自身状态 Status OrderStatus.Paid; // 发布领域事件 AddDomainEvent(new OrderPaidEvent(Id)); } } // 在EF Core中可以通过重写SaveChanges自动发布事件 public override async Taskint SaveChangesAsync(CancellationToken ct default) { var aggregates ChangeTracker.EntriesAggregateRoot() .Select(x x.Entity) .Where(x x.DomainEvents.Any()); var events aggregates.SelectMany(x x.DomainEvents).ToList(); await _mediator.DispatchDomainEvents(events); aggregates.ToList().ForEach(x x.ClearDomainEvents()); return await base.SaveChangesAsync(ct); }这种模式完美遵循了单一职责原则聚合根只关心自己的状态变更不关心后续处理流程。我在最近的项目中采用这种架构后领域层的单元测试覆盖率直接从40%提升到了85%因为每个组件的职责变得极其明确。3.2 CQRS的进阶实现基础版的CQRS使用MediatR已经能解决大部分问题但对于高性能场景可以考虑以下优化方案查询端优化使用Dapper代替EF Core提升查询性能实现专门的ReadModel数据库添加缓存层Redis或MemoryCachepublic class GetProductListQuery : IRequestPagedListProductDto { public int Page { get; set; } 1; public int PageSize { get; set; } 10; } public class GetProductListHandler : IRequestHandlerGetProductListQuery, PagedListProductDto { private readonly IDbConnection _db; public async TaskPagedListProductDto Handle(GetProductListQuery request, CancellationToken ct) { const string sql SELECT * FROM ProductViews ORDER BY Id OFFSET Offset ROWS FETCH NEXT PageSize ROWS ONLY; var products await _db.QueryAsyncProductDto(sql, new { Offset (request.Page - 1) * request.PageSize, request.PageSize }); return new PagedListProductDto(products, ...); } }命令端强化实现UnitOfWork模式保证事务一致性添加领域验证管道实现重试机制// 管道行为示例 public class ValidationBehaviorTRequest, TResponse : IPipelineBehaviorTRequest, TResponse { public async TaskTResponse Handle(TRequest request, CancellationToken ct, RequestHandlerDelegateTResponse next) { var validator _serviceProvider.GetServiceIValidatorTRequest(); if (validator ! null) { await validator.ValidateAndThrowAsync(request, ct); } return await next(); } } // 注册管道 services.AddScoped(typeof(IPipelineBehavior,), typeof(ValidationBehavior,));4. 实战构建电商订单系统4.1 架构分层设计基于MediatR和CQRS的典型分层如下Ordering.API (表现层) ├── Controllers └── Models Ordering.Application (应用层) ├── Commands ├── Queries ├── Handlers └── Events Ordering.Domain (领域层) ├── Entities ├── ValueObjects └── Events Ordering.Infrastructure (基础设施层) ├── Persistence └── ExternalServices这种架构下依赖关系始终保持单向流动API → Application → Domain ← Infrastructure。我在实际项目中验证过这种结构在6个月后的维护阶段依然能保持极高的可维护性。4.2 典型业务流程实现以取消订单为例完整实现如下// 领域事件 public class OrderCancelledEvent : INotification { public int OrderId { get; } public string Reason { get; } public OrderCancelledEvent(int orderId, string reason) (OrderId, Reason) (orderId, reason); } // 命令定义 public class CancelOrderCommand : IRequestbool { public int OrderId { get; } public string Reason { get; } public CancelOrderCommand(int orderId, string reason) (OrderId, Reason) (orderId, reason); } // 命令处理器 public class CancelOrderHandler : IRequestHandlerCancelOrderCommand, bool { private readonly IOrderRepository _repository; private readonly IMediator _mediator; public async Taskbool Handle(CancelOrderCommand request, CancellationToken ct) { var order await _repository.GetByIdAsync(request.OrderId); if (order null) return false; order.Cancel(request.Reason); await _repository.UpdateAsync(order); await _mediator.Publish(new OrderCancelledEvent(order.Id, request.Reason)); return true; } } // 事件处理器示例库存回滚 public class InventoryRollbackHandler : INotificationHandlerOrderCancelledEvent { public async Task Handle(OrderCancelledEvent notification, CancellationToken ct) { await _inventoryService.RollbackOrderItems(notification.OrderId); } }这种实现方式带来了几个显著优势业务逻辑集中在领域层不会泄漏到其他层每个Handler职责单一易于测试和维护新增取消订单的后续处理如发送通知只需添加新的Handler5. 性能优化与疑难解答5.1 性能调优经验在大型项目中MediatR可能遇到性能瓶颈。通过性能分析我发现主要开销在Handler的实例化特别是依赖较多的Handler管道行为的执行反射调用优化方案包括1. 使用Scoped生命周期// 默认是Transient对于复杂Handler可改为Scoped services.AddScopedIRequestHandlerCreateOrderCommand, int, CreateOrderHandler();2. 精简管道行为避免在管道中执行耗时操作如// 不好的实践 public class LoggingBehaviorTRequest, TResponse : IPipelineBehaviorTRequest, TResponse { public async TaskTResponse Handle(TRequest request, CancellationToken ct, RequestHandlerDelegateTResponse next) { var stopwatch Stopwatch.StartNew(); // 高开销操作 try { return await next(); } finally { _logger.LogInformation($耗时: {stopwatch.ElapsedMilliseconds}ms); } } }3. 预编译委托使用MediatR.Extensions.Compression等第三方包可以提升20%以上的性能。5.2 常见问题解决Q如何处理循环依赖A典型场景是HandlerA依赖HandlerB而HandlerB又依赖HandlerA。解决方案提取公共逻辑到新服务使用Lazy延迟初始化考虑是否违反单一职责原则Q事务管理的最佳实践A推荐两种方案使用EF Core的SaveChanges天然事务public async Task Handle(CreateOrderCommand request, CancellationToken ct) { using var transaction await _db.Database.BeginTransactionAsync(ct); try { // 业务操作 await _db.SaveChangesAsync(ct); await transaction.CommitAsync(ct); } catch { await transaction.RollbackAsync(ct); throw; } }使用Outbox模式处理分布式事务Q如何调试复杂的消息流A我的经验是实现诊断管道行为public class DiagnosticBehaviorTRequest, TResponse : IPipelineBehaviorTRequest, TResponse { public async TaskTResponse Handle(TRequest request, CancellationToken ct, RequestHandlerDelegateTResponse next) { Debug.WriteLine($处理 {typeof(TRequest).Name}); return await next(); } }使用Visual Studio的调试数据可视化工具为重要Handler添加详细日志6. 测试策略与团队协作6.1 单元测试模式MediatR架构的测试非常直观。对于Handler的测试我通常采用以下结构public class CreateOrderHandlerTests { [Fact] public async Task Handle_ValidCommand_ShouldCreateOrder() { // 准备 var mockRepo new MockIOrderRepository(); var mediator new MockIMediator(); var handler new CreateOrderHandler(mockRepo.Object, mediator.Object); var command new CreateOrderCommand(/* 测试数据 */); // 执行 var result await handler.Handle(command, CancellationToken.None); // 断言 mockRepo.Verify(x x.AddAsync(It.IsAnyOrder()), Times.Once); Assert.True(result 0); } }对于管道行为的测试public class ValidationBehaviorTests { [Fact] public async Task Handle_InvalidCommand_ShouldThrow() { var validator new CreateOrderCommandValidator(); var behavior new ValidationBehaviorCreateOrderCommand, int(validator); var invalidCommand new CreateOrderCommand(/* 无效数据 */); await Assert.ThrowsAsyncValidationException(() behavior.Handle(invalidCommand, CancellationToken.None, () Task.FromResult(1))); } }6.2 团队协作规范在大团队中使用MediatR需要建立一些规范命名约定命令动词名词CommandCreateOrderCommand查询名词QueryGetOrderByIdQuery事件名词过去式EventOrderCreatedEvent文件夹结构Features ├── Ordering │ ├── Commands │ │ └── CreateOrder │ │ ├── CreateOrderCommand.cs │ │ └── CreateOrderHandler.cs │ └── Queries │ └── GetOrderDetails └── Product └── ...代码审查要点检查Handler是否过于复杂超过200行应考虑拆分验证管道行为的执行顺序确保没有绕过MediatR的直接服务调用7. 架构演进与扩展方案7.1 与微服务集成当系统演进到微服务架构时MediatR仍然可以发挥重要作用方案一保持进程内通信每个微服务内部使用独立的MediatR实例处理服务内的领域事件和命令。方案二跨服务事件总线结合MassTransit或NServiceBus将领域事件转发到消息队列public class OrderCreatedIntegrationEvent : INotification { public int OrderId { get; } public DateTime OccurredOn { get; } DateTime.UtcNow; } public class IntegrationEventPublisher : INotificationHandlerOrderCreatedIntegrationEvent { private readonly IBus _bus; public async Task Handle(OrderCreatedIntegrationEvent notification, CancellationToken ct) { await _bus.Publish(notification, ct); } }7.2 高级扩展技巧动态Handler选择 通过自定义IMediator实现可以根据运行时条件选择不同Handlerpublic class DynamicMediator : IMediator { public async TaskTResponse SendTResponse(IRequestTResponse request, CancellationToken ct) { if (request is ISpecialRequest special special.IsPremium) { return await _premiumHandler.Handle(request, ct); } return await _defaultHandler.Handle(request, ct); } }AOP集成 结合Castle DynamicProxy实现更强大的切面编程public class MediatrProxy : IMediator { private readonly IMediator _inner; public async TaskTResponse SendTResponse(IRequestTResponse request, CancellationToken ct) { // 前置处理 LogRequest(request); try { var result await _inner.Send(request, ct); // 后置处理 LogResponse(result); return result; } catch (Exception ex) { // 异常处理 HandleException(ex); throw; } } }在最近的一个金融项目中我们通过这种扩展实现了自动化的请求审计和敏感操作二次验证安全团队对此赞不绝口。

更多文章