.NET Core开发实战(第30课:领域事件:提升业务内聚,实现模块解耦)--学习笔记
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,在应用层这个目录下面,我们可以为每一个事件都定义我们的处理程序
总结一下
领域模型内创建事件:我们不要在领域模型的外面去构造事件,然后传递给领域模型,因为整个领域事件是由领域的业务逻辑触发的,而不是说外面的对模型的操作触发的
另外就是针对领域事件应该定义专有的领域事件处理类,就像我们刚才演示的,在一个特定的目录,对每一个事件进行定义处理类
还有一个就是在同一个事务里面去处理我们的领域事件,实际上我们也可以选择在不同的事务里面处理,如果需要在不同的事务里面去处理领域事件的时候,我们就需要考虑一致性的问题,考虑中间出错,消息丢失的问题
GitHub源码链接:
https://github.com/witskeeper/geektime
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。
如有任何疑问,请与我联系 (MingsonZheng@outlook.com) 。
.NET Core开发实战(第30课:领域事件:提升业务内聚,实现模块解耦)--学习笔记的更多相关文章
- 2月送书福利:ASP.NET Core开发实战
大家都知道我有一个公众号“恰童鞋骚年”,在公众号2020年第一天发布的推文<2020年,请让我重新介绍我自己>中,我曾说到我会在2020年中每个月为所有关注“恰童鞋骚年”公众号的童鞋们送一 ...
- [ASP.NET Core开发实战]开篇词
前言 本系列课程文章主要是学习官方文档,再输出自己学习心得,希望对你有所帮助. 课程大纲 本系列课程主要分为三个部分:基础篇.实战篇和部署篇. 希望通过本系列课程,能让大家初步掌握使用ASP.NET ...
- .NET Core开发实战(第11课:文件配置提供程序)--学习笔记
11 | 文件配置提供程序:自由选择配置的格式 文件配置提供程序 Microsoft.Extensions.Configuration.Ini Microsoft.Extensions.Configu ...
- [ASP.NET Core开发实战]基础篇01 Startup
Startup,顾名思义,就是启动类,用于配置ASP.NET Core应用的服务和请求管道. Startup有两个主要作用: 通过ConfigureServices方法配置应用的服务.服务是一个提供应 ...
- 2、SpringBoot接口Http协议开发实战8节课(1-6)
1.SpringBoot2.xHTTP请求配置讲解 简介:SpringBoot2.xHTTP请求注解讲解和简化注解配置技巧 1.@RestController and @RequestMapping是 ...
- [ASP.NET Core开发实战]基础篇03 中间件
什么是中间件 中间件是一种装配到应用管道,以处理请求和响应的组件.每个中间件: 选择是否将请求传递到管道中的下一个中间件. 可在管道中的下一个中间件前后执行. ASP.NET Core请求管道包含一系 ...
- [ASP.NET Core开发实战]基础篇02 依赖注入
ASP.NET Core的底层机制之一是依赖注入(DI)设计模式,因此要好好掌握依赖注入的用法. 什么是依赖注入 我们看一下下面的例子: public class MyDependency { pub ...
- 2、SpringBoot接口Http协议开发实战8节课(7-8)
7.SpringBoot2.x文件上传实战 简介:讲解HTML页面文件上传和后端处理实战 1.讲解springboot文件上传 MultipartFile file,源自SpringMVC 1)静态页 ...
- [ASP.NET Core开发实战]基础篇06 配置
配置,是应用程序很重要的组成部分,常常用于提供信息,像第三方应用登录钥匙.上传格式与大小限制等等. ASP.NET Core提供一系列配置提供程序读取配置文件或配置项信息. ASP.NET Core项 ...
- [ASP.NET Core开发实战]基础篇05 服务器
什么是服务器 服务器指ASP.NET Core应用运行在操作系统上的载体,也叫Web服务器. Web服务器实现侦听HTTP请求,并以构建HttpContext的对象发送给ASP.NET Core应用. ...
随机推荐
- new关键字执行过程
在javascript中,现阶段我们可以采用三种方式创建对象(object) 利用字面量创建对象 利用new Object创建对象 利用构造函数创建对象 new关键字执行过程 // new关键字执行过 ...
- P2196-DP【黄】
清醒了一点后我又写了一道黄色DP题,做出来了,还行,开心不少了... 中途暴露出一些问题 1.深搜过程中既然用了二维数组,那么深搜时就应该用二维循环取最优解,而不是只从最后一行中进行一维循环取最优解. ...
- Kubernetes 网络:Pod 和 container 的那点猫腻
1. Kubernetes 网络模型 在 Kubernetes 的网络模型中,最小的网络单位是 Pod.Pod 的网络设计原则是 IP-per-Pod,即 Pod 中 container 共享同一套网 ...
- SV 字符串类型
概述 常见使用方式 string b; string b=""; // 拼接字符串 string a = {"hi",b}; // 将字符串a赋值给[15:0] ...
- 04 Tcl字符串
Tcl字符串 4.1 Tcl将说有的变量值视作字符串,并将他们作为字符串进行保存. 命令 描述 append 将值追加到字符串尾 binary 二进制化字符串 format 字符串格式化 regexp ...
- Laravel - 使用ajax
一,前台模板文件 index.blade.php <!DOCTYPE html> <meta name="_token" content="{{ cs ...
- [转帖]nginx优化配置及方法论
https://www.jianshu.com/p/87f8c03e91bd 1.优化方法论 从软件层面提升硬件使用效率 增大CPU的利用率 增大内存的利用率 增大磁盘IO的利用率 增大网络带宽的利用 ...
- [转帖]Linux 页表、大页与透明大页
一. 内存映射与页表 1. 内存映射 我们通常所说的内存容量,指的是物理内存,只有内核才可以直接访问物理内存,进程并不可以. Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间 ...
- [转帖]一文读懂 K8s 持久化存储流程
https://zhuanlan.zhihu.com/p/128552232 作者 | 孙志恒(惠志) 阿里巴巴开发工程师 导读:众所周知,K8s 的持久化存储(Persistent Storage) ...
- [转帖]看看 Jmeter 是如何玩转 redis 数据库的
柠檬小欧 2021-08-31 20:06420 Jmeter 作为当前非常受欢迎的接口测试和性能测试的工具,在企业中得到非常广泛的使用,而 Redis 作为缓存数据库,也在企业中得到普遍使用,那如何 ...