重新整理 .net core 实践篇—————领域事件[二十九]
前文
前面整理了仓储层,工作单元模式,同时简单介绍了一下mediator。
那么就mediator在看下领域事件启到了什么作用吧。
正文
这里先注册一下MediatR服务:
// 注册中间者:MediatR
services.AddMediatRServices();
具体注册:
/// <summary>
/// 注册 ???
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddMediatRServices(this IServiceCollection services)
{
// 注册事务流程管理类
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(DomainContextTransactionBehavior<,>));
// package: MediatR.Extensions.Microsoft.Dependency
return services.AddMediatR(typeof(Order).Assembly, typeof(Program).Assembly);
}
前文提及在共享层的领域抽象类库中,有下面几个类:
用来标志领域事件的接口:
/// <summary>
/// 领域事件接口
/// 用来标记我们某一个对象是否是领域事件
/// </summary>
public interface IDomainEvent : INotification
{
}
用来标志领域处理的接口:
/// <summary>
/// 领域事件处理器接口
/// </summary>
public interface IDomainEventHandler<TDomainEvent> : INotificationHandler<TDomainEvent> where TDomainEvent : IDomainEvent
{
//这里我们使用了INotificationHandler的Handle方法来作为处理方法的定义,所以无需重新定义
//Task Handle(TDomainEvent domainEvent, CancellationToken cancellationToken);
}
实际上就是在mediator的INotification和INotificationHandler 封装一层。
那么是否有必要封装呢?其实我们大多数使用别人的库,如果不是静态调用最好封装一层,这样可以让上层看来依赖于下层的IDomainEvent和IDomainEventHandler,而不是和某个框架耦合在一起。
比如说有一个框架是Mediator的升级版,兼容了Mediator的功能,但是暴露出来的接口是INotificationPlusHandler,如果上层去耦合的话,改动的地方就有点多,风险也就越高,这不符合稳定性。
下面是实体抽象类:
/// <summary>
/// 实体抽象类(包含多个主键的实体接口)
/// </summary>
public abstract class Entity : IEntity
{
public abstract object[] GetKeys();
public override string ToString()
{
return $"[Entity:{GetType().Name}] Keys = {string.Join(",", GetKeys())}";
}
#region 领域事件定义处理 DomainEvents
/// <summary>
/// 领域事件集合
/// </summary>
private List<IDomainEvent> _domainEvents;
/// <summary>
/// 获取当前实体对象领域事件集合(只读)
/// </summary>
public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents?.AsReadOnly();
/// <summary>
/// 添加领域事件至当前实体对象领域事件结合中
/// </summary>
/// <param name="eventItem"></param>
public void AddDomainEvent(IDomainEvent eventItem)
{
_domainEvents = _domainEvents ?? new List<IDomainEvent>();
_domainEvents.Add(eventItem);
}
/// <summary>
/// 移除指定领域事件
/// </summary>
/// <param name="eventItem"></param>
public void RemoveDomainEvent(IDomainEvent eventItem)
{
_domainEvents?.Remove(eventItem);
}
/// <summary>
/// 清空所有领域事件
/// </summary>
public void ClearDomainEvents()
{
_domainEvents?.Clear();
}
#endregion
}
/// <summary>
/// 实体抽象类(包含唯一主键Id的实体接口)
/// </summary>
/// <typeparam name="TKey">主键ID类型</typeparam>
public abstract class Entity<TKey> : Entity, IEntity<TKey>
{
int? _requestedHasCode;
public virtual TKey Id { get; protected set; }
public override object[] GetKeys()
{
return new object[] { Id };
}
/// <summary>
/// 对象是否想等
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public override bool Equals(object obj)
{
if (obj == null || !(obj is Entity<TKey>))
{
return false;
}
if (Object.ReferenceEquals(this, obj))
{
return true;
}
Entity<TKey> item = (Entity<TKey>)obj;
if (item.IsTransient() || this.IsTransient())
{
return false;
}
else
{
return item.Id.Equals(this.Id);
}
}
public override int GetHashCode()
{
if (!IsTransient())
{
if (!_requestedHasCode.HasValue)
{
_requestedHasCode = this.Id.GetHashCode() ^ 31; // TODO
}
return _requestedHasCode.Value;
}
else
{
return base.GetHashCode();
}
}
/// <summary>
/// 对象是否为全新创建的,未持久化的
/// </summary>
/// <returns></returns>
public bool IsTransient()
{
return EqualityComparer<TKey>.Default.Equals(Id, default);
}
public override string ToString()
{
return $"[Entity:{GetType().Name}] Id = {Id}";
}
/// <summary>
/// == 操作符重载
/// </summary>
/// <param name="left"></param>
/// <param name="right"></param>
/// <returns></returns>
public static bool operator ==(Entity<TKey> left,Entity<TKey> right)
{
if (Object.Equals(left,null))
{
return (Object.Equals(right, null)) ? true : false;
}
else
{
return left.Equals(right);
}
}
/// <summary>
/// != 操作符重载
/// </summary>
/// <param name="left"></param>
/// <param name="right"></param>
/// <returns></returns>
public static bool operator !=(Entity<TKey> left, Entity<TKey> right)
{
return !(left == right);
}
}
那么我们在领域层中,我们的实体可以这样定义:
/// <summary>
/// 订单实体
/// </summary>
public class Order : Entity<long>, IAggregateRoot
{
// 实体内字段的 set 方法都是 private 的
// 实体类型相关的数据操作,都应该是由我们实体来负责,而不是被外部的对象去操作
// 这样的好处是让我们的领域模型符合封闭开放的原则
public string UserId { get; private set; }
public string UserName { get; private set; }
public Address Address { get; private set; }
public int ItemCount { get; set; }
protected Order()
{
}
public Order(string userId, string userName, int itemCount, Address address)
{
this.UserId = userId;
this.UserName = userName;
this.ItemCount = itemCount;
this.Address = address;
// 构造新的Order对象的时候,添加一个创建Order领域事件
this.AddDomainEvent(new OrderCreatedDomainEvent(this));
}
/// <summary>
/// 修改收货地址
/// </summary>
/// <param name="address"></param>
public void ChangeAddress(Address address)
{
this.Address = address;
// 同样的,在修改地址操作时,也该定义一个类似的修改地址领域事件
//this.AddDomainEvent(new OrderAddressChangedDomainEvent(this));
}
}
在实例化的时候将创建一个OrderCreatedDomainEvent事件,这个后面是用来处理订单创建完成之后的事件。
那么现在看下这个OrderCreatedDomainEvent是什么:
/// <summary>
/// 创建Order领域事件
/// </summary>
public class OrderCreatedDomainEvent : IDomainEvent
{
/// <summary>
/// 写入私有,读取公开
/// </summary>
public Order Order { get; private set; }
public OrderCreatedDomainEvent(Order order)
{
this.Order = order;
}
}
那么在我们的应用层可以定义一个订单创建完成的事件处理类,比如说OrderCreatedDomainEventHandler:
/// <summary>
/// 创建Order领域事件处理
/// </summary>
public class OrderCreatedDomainEventHandler : IDomainEventHandler<OrderCreatedDomainEvent>
{
ICapPublisher _capPublisher;
public OrderCreatedDomainEventHandler(ICapPublisher capPublisher)
{
_capPublisher = capPublisher;
}
/// <summary>
/// 领域事件处理
/// </summary>
/// <param name="notification"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task Handle(OrderCreatedDomainEvent notification, CancellationToken cancellationToken)
{
// 当创建新订单时,向 EventBus 发布一个事件
await _capPublisher.PublishAsync("OrderCreated", new OrderCreatedIntegrationEvent(notification.Order.Id));
//_capPublisher.Publish("OrderCreated", new OrderCreatedIntegrationEvent(notification.Order.Id));
//return Task.CompletedTask;
}
}
上面表示,当我们创建一个订单后完成后的类,如果订单创建完毕,会向 EventBus 发布一个事件。
上面是订单处理完成后的事件,那么我们创建订单是不是也应该写一个请求呢?
为什么我们写请求,而不是写事件呢?一个是因为请求与请求处理是1对1,事件与事件处理是一对多。第二个是跟符合意境,一切的源头写出请求更好,还有就是创建订单的确在生活中像一个请求。
CreateOrderCommand 如下:
/// <summary>
/// 创建订单 Command
/// </summary>
public class CreateOrderCommand : IRequest<long>
{
public long ItemCount { get; private set; }
// public CreateOrderCommand() { }
public CreateOrderCommand(int itemCount)
{
ItemCount = itemCount;
}
}
具体的订单创建事件CreateOrderCommandHandler:
/// <summary>
/// 领域事件:订单创建命令处理程序
/// 注:在创建完我们的领域模型并将其保存之后才会触发该处理程序
/// </summary>
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, long>
{
IOrderRepository _orderRepository;
ICapPublisher _capPublisher;
public CreateOrderCommandHandler(IOrderRepository orderRepository, ICapPublisher capPublisher)
{
_orderRepository = orderRepository;
_capPublisher = capPublisher;
}
/// <summary>
/// 处理订单创建命令
/// </summary>
/// <param name="request"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<long> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
var address = new Address("wen san lu", "hangzhou", "310000");
var order = new Order("xiaohong1999", "小红", (int)request.ItemCount, address);
_orderRepository.Add(order);
await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
return order.Id;
}
}
那么这个就会去处理相应的创建订单。
那么测试一下:
/// <summary>
/// 创建订单
/// </summary>
/// <param name="cmd"></param>
/// <returns></returns>
[HttpPost]
public async Task<long> CreateOrder([FromBody] CreateOrderCommand cmd)
{
// 中间者,发送订单创建命令
return await _mediator.Send(cmd, HttpContext.RequestAborted);
}
第一步调用这里:
第二步调用CreateOrderCommandHandler,去调用相应的订单调用事件:
第三步,调用订单创建完的中间处理,也就是订单创建完的事件分发:
代码如下:
/// <summary>
/// 中间者,领域事件发布扩展类
/// </summary>
public static class MediatorExtension
{
/// <summary>
/// 领域事件发布,执行事件发送
/// </summary>
/// <param name="mediator"></param>
/// <param name="ctx"></param>
/// <returns></returns>
public static async Task DispatchDomainEventsAsync(this IMediator mediator, DbContext ctx)
{
// 1. 从当前要保存的 EntityContext 里面去跟踪实体
// 从跟踪到的实体对象中,获取到我们当前的 Event
var domainEntities = ctx.ChangeTracker
.Entries<Entity>()
.Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any());
// Events 类型转换
var domainEvents = domainEntities.SelectMany(x => x.Entity.DomainEvents).ToList();
// 2. 将实体内的 Events 清除
domainEntities.ToList().ForEach(entity => entity.Entity.ClearDomainEvents());
// 3. 将所有的 Event 通过中间者发送出去
// 发出后,并找到对应的 handle 进行处理
foreach (var domainEvent in domainEvents)
{
await mediator.Publish(domainEvent);
}
}
}
第四步 调用创建订单之后的回调事件,也就是第三步中的分发:
这里可能疑问DispatchDomainEventsAsync是怎么触发的?
这里是在EFContext中的save中:
/// <summary>
/// 保存实体变更
/// </summary>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default)
{
var result = await base.SaveChangesAsync(cancellationToken);
// 执行发送领域事件
await _mediator.DispatchDomainEventsAsync(this);
return true;
}
然后调用这个就会去找到相应事件,然后调用publish,找到事件处理类进行handle方法调用:
/// <summary>
/// 领域事件发布,执行事件发送
/// </summary>
/// <param name="mediator"></param>
/// <param name="ctx"></param>
/// <returns></returns>
public static async Task DispatchDomainEventsAsync(this IMediator mediator, DbContext ctx)
{
// 1. 从当前要保存的 EntityContext 里面去跟踪实体
// 从跟踪到的实体对象中,获取到我们当前的 Event
var domainEntities = ctx.ChangeTracker.Entries<Entity>().Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any());
// Events 类型转换
var domainEvents = domainEntities.SelectMany(x => x.Entity.DomainEvents).ToList();
// 2. 将实体内的 Events 清除
domainEntities.ToList().ForEach(entity => entity.Entity.ClearDomainEvents());
// 3. 将所有的 Event 通过中间者发送出去
// 发出后,并找到对应的 handle 进行处理
foreach (var domainEvent in domainEvents)
{
await mediator.Publish(domainEvent);
}
}
结
下一节Mediator的介绍。
重新整理 .net core 实践篇—————领域事件[二十九]的更多相关文章
- 重新整理 .net core 实践篇—————Mediator实践[二十八]
前言 简单整理一下Mediator. 正文 Mediator 名字是中介者的意思. 那么它和中介者模式有什么关系呢?前面整理设计模式的时候,并没有去介绍具体的中介者模式的代码实现. 如下: https ...
- 重新整理 .net core 实践篇—————异常中间件[二十]
前言 简单介绍一下异常中间件的使用. 正文 if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } 这样写入中间件哈,那么在env环 ...
- 重新整理 .net core 实践篇————重定向攻击[三十九]
前言 简单介绍一下重定向攻击. 正文 攻击思路: 看着上面挺复杂的,其实是一些很简单的步骤. 攻击者通过某些手段,让用户打开了一个好站点,打开的这个地址里面带有重定向信息,重定向信息就是自己伪造的站点 ...
- 重新整理 .net core 实践篇—————静态中间件[二十一]
前言 简单整理一下静态中间件. 正文 我们使用静态文件调用: app.UseStaticFiles(); 那么这个默认会将我们根目录下的wwwroot作为静态目录. 这个就比较值得注意的,可能刚开始学 ...
- 重新整理 .net core 实践篇————缓存相关[四十二]
前言 简单整理一下缓存. 正文 缓存是什么? 缓存是计算结果的"临时"存储和重复使用 缓存本质是用空间换取时间 缓存的场景: 计算结果,如:反射对象缓存 请求结果,如:DNS 缓存 ...
- 重新整理 .net core 实践篇—————配置文件之环境配置[九]
前言 在当今在互联网微服务比较适用的情况下,docker 可以说一个利器.每次我们打包docker的时候都是适用docker 的配置文件,那么配置文件里面会设置环境变量,这个时候需要我们的应用能够识别 ...
- 重新整理 .net core 实践篇————cookie 安全问题[三十八]
前言 简单整理一下cookie的跨站攻击,这个其实现在不常见,因为很多公司都明确声明不再用cookie存储重要信息,不过对于老站点还是有的. 正文 攻击原理: 这种攻击要达到3个条件: 用户访问了我们 ...
- 重新整理 .net core 实践篇——— UseEndpoints中间件[四十八]
前言 前文已经提及到了endponint 是怎么匹配到的,也就是说在UseRouting 之后的中间件都能获取到endpoint了,如果能够匹配到的话,那么UseEndpoints又做了什么呢?它是如 ...
- 重新整理 .net core 实践篇————配置应用[一]
前言 本来想整理到<<重新整理.net core 计1400篇>>里面去,但是后来一想,整理 .net core 实践篇 是偏于实践,故而分开. 因为是重新整理,那么就从配置开 ...
随机推荐
- 安装过程中出现一个错误: No such plugin: cloudbees-folder
上面的错误显示是,安装插件cloudbees-folder失败,是因为下载的Jenkins.war里没有cloudbees-folder插件 需要去 https://updates.jenkins-c ...
- [Java] SpringBoot
背景 简化SSM(H)中大量的配置工作,开发人员只关心提供业务功能 可以看成简化了的.按照约定开发的SSM(H) 概念 JavaBean:满足规范的Java类(属性private+默认构造方法+get ...
- 【转载】8.2.1 CPU性能测试工具
(KVM连载) 8.2.1 CPU性能测试工具 01/08/2013master 1 Comment 8.2.1 CPU性能测试工具 CPU是计算机系统中最核心的部件,CPU的性能直接决定了系统的计算 ...
- Sqoop 安装部署
1. 上传并解压 Sqoop 安装文件 将 sqoop-1.4.7.bin__hadoop-2.6.0.tar.gz 安装包上传到 node-01 的 /root/ 目录下并将其解压 [root@no ...
- shell基础之for循环语句
For语句 格式:for name [ [ in [ word ... ] ] ; ] do list ; done for 变量名 in 取值列表; do 命令 done 或者 for 变量名 in ...
- 2.9高级变量类型操作(列表 * 元组 * 字典 * 字符串)_内置函数_切片_运算符_for循环
高级变量类型 目标 列表 元组 字典 字符串 公共方法 变量高级 知识点回顾 Python 中数据类型可以分为 数字型 和 非数字型 数字型 整型 (int) 浮点型(float) 布尔型(bool) ...
- 10.20 host:域名查询工具
host命令 是用于查询DNS的工具,它可以将指定主机名称转换为IP地址. host命令的参数选项及说明 -a 显示详细的DNS信息-t 指定查询的域名信息类型,可以是"A".&q ...
- android常用的四种对话框java代码
AlterDialog:确认取消警告提示框 public void showAlertDialog(View view){ AlertDialog.Builder dialog = new Alert ...
- ssh远程主机执行命令或脚本
1.执行单一命令 [root@vps ~]# ssh user@192.168.9.243 "pwd; ls; rm -f Cent* ;echo --------; ls"/ho ...
- lua type 获取 类型
lua中的类型作一小记 print(type("Hello world")) --> string print(type(10.4*3)) --> number pri ...