30:领域事件:提升业务内聚,实现模块解耦-开发实战
.NET Core开发实战前文传送门:
- 第1课:课程介绍
- 第2课:内容综述
- 第3课:.NET Core的现状、未来以及环境搭建
- 第4课:Startup:掌握ASP.NET Core的启动过程
- 第5课:依赖注入:良好架构的起点(上)
- 第5课:依赖注入:良好架构的起点(中)
- 第5课:依赖注入:良好架构的起点(下)
- 第6课:作用域与对象释放行为(上)
- 第6课:作用域与对象释放行为(下)
- 第7课:用Autofac增强容器能力(上)
- 第7课:用Autofac增强容器能力(下)
- 第8课:配置框架:让服务无缝适应各种环境
- 第9课:命令行配置提供程序
- 第10课:环境变量配置提供程序
- 第11课:文件配置提供程序
- 第12课:配置变更监听
- 第13课:配置绑定:使用强类型对象承载配置数据
- 第14课:自定义配置数据源:低成本实现定制化配置方案
- 第15课:选项框架:服务组件集成配置的最佳实践
- 第16课:选项数据热更新:让服务感知配置的变化
- 第17课:为选项数据添加验证:避免错误配置的应用接收用户流量
- 第18课:日志框架:聊聊记日志的最佳姿势(上)
- 第18课:日志框架:聊聊记日志的最佳姿势(下)
- 第19课:日志作用域:解决不同请求之间的日志干扰
- 第20课:结构化日志组件Serilog:记录对查询分析友好的日志
- 第21课:中间件:掌控请求处理过程的关键(上)
- 第21课:中间件:掌控请求处理过程的关键(下)
- 第22课:异常处理中间件:区分真异常与逻辑异常(上)
- 第22课:异常处理中间件:区分真异常与逻辑异常(下)
- 第23课:静态文件中间件:前后端分离开发合并部署骚操作(上)
- 第23课:静态文件中间件:前后端分离开发合并部署骚操作(下)
- 第24课:文件提供程序:让你可以将文件放在任何地方
- 第25课:路由与终结点:如何规划好你的Web API(上)
- 第25课:路由与终结点:如何规划好你的Web API(下)
- 第26课:工程结构概览:定义应用分层及依赖关系
- 第27课:定义Entity:区分领域模型的内在逻辑和外在行为
- 第28课:工作单元模式(UnitOfWork):管理好你的事务
- 第29课:定义仓储:使用EF Core实现仓储层
30 | 领域事件:提升业务内聚,实现模块解耦
我们在领域的抽象层定义了领域事件和领域事件处理的接口
IDomainEvent
namespace GeekTime.Domain { public interface IDomainEvent : INotification { } }
这是一个空接口,它只是标记出来某一个对象是否是领域事件,INotification 也是一个空接口,它是 MediatR 框架的一个接口,是用来实现事件传递用的
namespace MediatR { public interface INotification { } }
接着是 IDomainEventHandler
namespace GeekTime.Domain { public interface IDomainEventHandler<TDomainEvent> : INotificationHandler<TDomainEvent> where TDomainEvent : IDomainEvent { //这里我们使用了INotificationHandler的Handle方法来作为处理方法的定义 //Task Handle(TDomainEvent domainEvent, CancellationToken cancellationToken); } }
同样这个接口也是继承了 IDomainEventHandler 接口,它有一个泛型参数是 TDomainEvent,这个 TDomainEvent 约束必须为 IDomainEvent,也就是说处理程序只处理 IDomainEvent 作为入参
实际上该方法已经在 INotificationHandler 中定义好了,所以这里不需要重新定义,只是告诉大家它的定义是什么样子的
在 Entity 中对领域事件代码的处理
private List<IDomainEvent> _domainEvents; public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents?.AsReadOnly(); public void AddDomainEvent(IDomainEvent eventItem) { _domainEvents = _domainEvents ?? new List<IDomainEvent>(); _domainEvents.Add(eventItem); } public void RemoveDomainEvent(IDomainEvent eventItem) { _domainEvents?.Remove(eventItem); } public void ClearDomainEvents() { _domainEvents?.Clear(); }
将领域事件做一个实体的属性存储进来,它应该是一个列表,因为在一个实体操作过程中间可能会发生多件事情,领域事件应该是可以被实体模型之外的代码读到,所以暴露一个 ReadOnly 的 Collection
这里还提供几个方法:添加领域事件,移除领域事件,清除领域事件
这些方法都是在领域模型内部进行调用的
可以看一下之前定义的 Order
public Order(string userId, string userName, int itemCount, Address address) { this.UserId = userId; this.UserName = userName; this.Address = address; this.ItemCount = itemCount; this.AddDomainEvent(new OrderCreatedDomainEvent(this)); } public void ChangeAddress(Address address) { this.Address = address; //this.AddDomainEvent(new OrderAddressChangedDomainEvent(this)); }
当我们构造一个全新的 Order 的时候,实际上这里可以定义一个事件叫做 OrderCreatedDomainEvent,这个领域事件它的构造函数的入参就是一个 Order,当我们调用 Order 的构造函数时,实际上我们的行为就是在创建一个全新的 Order,所以在这里添加一个事件 AddDomainEvent
同理的比如说 ChangeAddress 被调用了,我们在这里实际上可以定义一个 OrderAddressChangedDomainEvent 类似这样子的领域事件出来
大家可以看到领域事件的构造和添加都应该是在领域模型的方法内完成的,而不应该是被外界的代码去调用创建,因为这些事件都是领域模型内部发生的事件
接着看看 OrderCreatedDomainEvent 的定义
namespace GeekTime.Domain.Events { public class OrderCreatedDomainEvent : IDomainEvent { public Order Order { get; private set; } public OrderCreatedDomainEvent(Order order) { this.Order = order; } } }
那我们如何处理我们的领域事件,接收领域事件的处理应该定义在应用层
namespace GeekTime.API.Application.DomainEventHandlers { public class OrderCreatedDomainEventHandler : IDomainEventHandler<OrderCreatedDomainEvent> { ICapPublisher _capPublisher; public OrderCreatedDomainEventHandler(ICapPublisher capPublisher) { _capPublisher = capPublisher; } public async Task Handle(OrderCreatedDomainEvent notification, CancellationToken cancellationToken) { await _capPublisher.PublishAsync("OrderCreated", new OrderCreatedIntegrationEvent(notification.Order.Id)); } } }
它继承了 IDomainEventHandler,这个接口是上面讲到的领域事件处理器的接口,它的泛型入参就是要处理的事件的类型 OrderCreatedDomainEvent
为了简单演示起见,这里的逻辑是当我们创建一个新的订单时,我们向 EventBus 发布一条事件,叫做 OrderCreated 这个事件
我们在 OrderController 的 CreateOrder 定义了一个 CreateOrderCommand
[HttpPost] public async Task<long> CreateOrder([FromBody]CreateOrderCommand cmd) { return await _mediator.Send(cmd, HttpContext.RequestAborted); }
CreateOrderCommand
namespace GeekTime.API.Application.Commands { public class CreateOrderCommand : IRequest<long> { //ublic CreateOrderCommand() { } public CreateOrderCommand(int itemCount) { ItemCount = itemCount; } public long ItemCount { get; private set; } } }
CreateOrderCommandHandler
public async Task<long> Handle(CreateOrderCommand request, CancellationToken cancellationToken) { var address = new Address("wen san lu", "hangzhou", "310000"); var order = new Order("xiaohong1999", "xiaohong", 25, address); _orderRepository.Add(order); await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken); return order.Id; }
我们在 CreateOrderCommandHandler 里面创建了一个 Order,然后保存进仓储,调用了 UnitOfWork 的 SaveEntitiesAsync
启动程序,直接执行,调用我们的方法,可以看到我们先进入到了创建订单的处理系统(CreateOrderCommandHandler),接着进入到了领域事件发布的 Publish 的代码(MediatorExtension),当仓储存储完毕之后,进入到了 OrderCreatedDomainEventHandler,也就是说我们在创建完我们的领域模型并将其保存之后,我们的领域事件的处理程序才触发
在之前讲解实现 UnitOfWork 的时候(EFContext),我们的 SaveEntitiesAsync 里面只有一行代码是 SaveChangesAsync,这里添加了一行代码,是发送领域事件的代码 DispatchDomainEventsAsync
public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default) { var result = await base.SaveChangesAsync(cancellationToken); //await _mediator.DispatchDomainEventsAsync(this); return true; }
这就是 MediatorExtension 中看到的 DispatchDomainEventsAsync
namespace GeekTime.Infrastructure.Core.Extensions { static class MediatorExtension { public static async Task DispatchDomainEventsAsync(this IMediator mediator, DbContext ctx) { var domainEntities = ctx.ChangeTracker .Entries<Entity>() .Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any()); var domainEvents = domainEntities .SelectMany(x => x.Entity.DomainEvents) .ToList(); domainEntities.ToList() .ForEach(entity => entity.Entity.ClearDomainEvents()); foreach (var domainEvent in domainEvents) await mediator.Publish(domainEvent); } } }
大家可以看到我们发送领域事件实际上是这么一个过程:我们从当前要保存的 EntityContext 里面去跟踪我们的实体,然后从跟踪到的实体的对象中获取到我们当前的 Event,如果 Event 是存在的,就把它取出来,然后将实体内的 Event 进行清除,再然后将这些 Event 逐条地通过中间件发送出去,并且找到对应的 Handler 处理
定义领域事件实际上也非常简单,只需要在领域模型创建一个 Events 的目录,然后将领域事件都定义在这里,领域事件需要继承 IDomainEvent,领域事件的处理器都定义在 DomainEventHandler,在应用层这个目录下面,我们可以为每一个事件都定义我们的处理程序
总结一下
领域模型内创建事件:我们不要在领域模型的外面去构造事件,然后传递给领域模型,因为整个领域事件是由领域的业务逻辑触发的,而不是说外面的对模型的操作触发的
另外就是针对领域事件应该定义专有的领域事件处理类,就像我们刚才演示的,在一个特定的目录,对每一个事件进行定义处理类
还有一个就是在同一个事务里面去处理我们的领域事件,实际上我们也可以选择在不同的事务里面处理,如果需要在不同的事务里面去处理领域事件的时候,我们就需要考虑一致性的问题,考虑中间出错,消息丢失的问题

原文出处:微信公众号【郑子铭 DotNet NB】
原文链接:https://mp.weixin.qq.com/s/3560xWZkJvp3FNqvuYQlCg
本文观点不代表Dotnet9立场,转载请联系原作者。