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,在应用层这个目录下面,我们可以为每一个事件都定义我们的处理程序

总结一下

领域模型内创建事件:我们不要在领域模型的外面去构造事件,然后传递给领域模型,因为整个领域事件是由领域的业务逻辑触发的,而不是说外面的对模型的操作触发的

另外就是针对领域事件应该定义专有的领域事件处理类,就像我们刚才演示的,在一个特定的目录,对每一个事件进行定义处理类

还有一个就是在同一个事务里面去处理我们的领域事件,实际上我们也可以选择在不同的事务里面处理,如果需要在不同的事务里面去处理领域事件的时候,我们就需要考虑一致性的问题,考虑中间出错,消息丢失的问题

.NET Core开发实战(第30课:领域事件:提升业务内聚,实现模块解耦)--学习笔记...相关推荐

  1. .NET Core开发实战(第5课:依赖注入:良好架构的起点)--学习笔记(上)

    05 | 依赖注入:良好架构的起点 为什么要使用依赖注入框架 借助依赖注入框架,我们可以轻松管理类之间的依赖,帮助我们在构建应用时遵循设计原则,确保代码的可维护性和可扩展性 ASP.NET Core ...

  2. .NET Core开发实战(定义API的最佳实践)Source Generators版

    前言 极客时间上的<.NET Core开发实战>是一门非常好的课程,作者肖伟宇在第31课(https://time.geekbang.org/course/detail/100044601 ...

  3. gram矩阵的性质_第十七课:正交矩阵和GramSchmidt正交化——MIT线性代数课程学习笔记...

    公众号关注  "DL_NLP" 设为 "星标",重磅干货,第一时间送达! ◎ 原创 | 深度学习算法与自然语言处理 ◎ 作者 | 丁坤博 一. 知识概要 这一节 ...

  4. .NET Core开发实战(第35课:MediatR:让领域事件处理更加优雅)--学习笔记

    35 | MediatR:让领域事件处理更加优雅 核心对象 IMediator INotification INotificationHandler 这两个与之前的 Request 的行为是不一样的, ...

  5. .NET Core开发实战(第33课:集成事件:使用RabbitMQ来实现EventBus)--学习笔记(上)...

    33 | 集成事件:使用RabbitMQ来实现EventBus 这一节我们来讲解如何通过 CAP 组件和 RabbitMQ 来实现 EventBus 要实现 EventBus,我们这里借助了 Rabb ...

  6. .NET Core开发实战(第32课:集成事件:解决跨微服务的最终一致性)--学习笔记...

    32 | 集成事件:解决跨微服务的最终一致性 首先看一下集成事件的工作原理 它的目的时为了实现系统的集成,它主要是用于系统里面多个微服务之间相互传递事件 集成事件的实现方式有两种,一种是图上显示的发布 ...

  7. .NET Core开发实战(第26课:工程结构概览:定义应用分层及依赖关系)--学习笔记...

    26 | 工程结构概览:定义应用分层及依赖关系 从这一节开始进入微服务实战部分 这一节主要讲解工程的结构和应用的分层 在应用的分层这里定义了四个层次: 1.领域模型层 2.基础设施层 3.应用层 4. ...

  8. .NET Core开发实战(第33课:集成事件:使用RabbitMQ来实现EventBus)--学习笔记(下)...

    33 | 集成事件:使用RabbitMQ来实现EventBus 为了演示我们的发布和订阅的话,我们在这里的代码做一些稍微的调整 namespace GeekTime.API.Application.D ...

  9. .NET Core开发实战(第27课:定义Entity:区分领域模型的内在逻辑和外在行为)--学习笔记...

    27 | 定义Entity:区分领域模型的内在逻辑和外在行为 上一节讲到领域模型分为两层 一层是抽象层,定义了公共的接口和类 另一层就是领域模型的定义层 先看一下抽象层的定义 1.实体接口 IEnti ...

最新文章

  1. 谷歌、脸书、微软、亚马逊、苹果、百度等AI巨头发展路径探析
  2. 防止接口数据出问题,前端假数据调试
  3. unity json mysql_unity——json总结
  4. fcntl函数参数F_GETPIPE_SZ、F_SETPIPE_SZ报错引出的关于linux-specific头文件的使用方法...
  5. 监控linux时间不对,shell 计算故障时间 配合web监控
  6. 组合数学 —— 组合数取模 —— 逆元与递推打表
  7. 【什么是数据隐私?安全与隐私的区别?】差分隐私代码实现系列(一)
  8. Android-----巧用科大讯飞语句实现中文语音播报
  9. python中文怎么读-python的读法
  10. mysql8 连接不上数据库_MySQL升级8.0后连接不上数据库
  11. iPhone 自动关机 原因 天气太冷
  12. 03.怎样调试学习mico-demos?
  13. K_A16_001 基于STM32等单片机驱动HX711称重模块 串口与OLED0.96双显示
  14. Spring Boot内置Tomcat的静态资源配置(在页面中显示项目外的某个图片)
  15. 很简单的Python小程序:使用random库 实现随机选取元素
  16. iphone,ipad分辨率大全
  17. Mysql最新版8.0.21下载安装配置教程
  18. Udacity优达学城优惠码 5641874B 立减320元
  19. pytdx 调用沪深300 所有股票实时行情
  20. 9微电网两阶段鲁棒优化经济调度方法(MATLAB程序)

热门文章

  1. SqlServer和MySQL中存储过程out返回值处理C#代码
  2. ubuntu 安装 php
  3. 遭遇“烧钱瓶颈” 优酷成本结构堪忧
  4. office自定义安装选项_如何自定义Office 2013中功能区上的现有选项卡
  5. 中输入learn_Scikit-learn新版本发布,一行代码秒升级
  6. Redis 通配符批量删除key
  7. Ubuntu 18.04上Qmmp安装教程
  8. java提示找不到或无法加载主类
  9. Springboot项目搭建(三)整合thymeleaf模板
  10. 图片加载框架Picasso - 源码分析