本节内容,部分为补充内容,部分涉及到9.3.8-9.3.9(P327-334)。主要NuGet包:
- MediatR.Extensions.Microsoft.DependencyInjection
领域事件可以切断领域模型之间的强依赖关系,但如果使用传递的事件机制,要求将事件的处理者显式的注册到事件发布者对象中,耦合性强。所以在.NET中实现同一个进程内的领域事件,推荐使用MediatR库,它可以实现事件发布和事件处理对象之间的解耦。
一、MediatR的基本使用
1、基本步骤:
- 第一步:安装NuGet包,MediatR.Extensions.Microsoft.DependencyInjection
- 第二步:注册MediatR服务:builder.Services.AddMediatR(Assembly.Load(nameof(WebApplication15))); 通过AddMediatR方法的参数为 事件处理者 所在的若干程序集。
- 第三步:创建事件类,如下例中的TestEvent类
- 第四步:创建事件处理类,定义事件处理程序(Handle方法),如下例中的TestEventHandler1类和TestEventHandler2类。事件发布和事件处理,可以一对一,也可以一对多。
- 第五步:创建事件发布类,如下例中的TestPublishController控制器,注入IMediator服务,调用Publish或Send方法,发布事件。
2、基本使用案例
(1)事件类
//实现INotification即可 //由于事件一般从发布者到处理者,是单向行为,不可变,所以建议使用record类型 public record TestEvent(string Name):INotification;
(2)事件处理类
//事件处理类1 //INotificationHandler<TestEvent>,订阅事件TestEvent //Handle方法,为事件处理程序,事件发布后执行 public class TestEventHandler1 : INotificationHandler<TestEvent> { public Task Handle(TestEvent notification, CancellationToken cancellationToken) { Console.WriteLine($"TestEventHandler1收到了{notification.Name}"); return Task.CompletedTask; } } //事件处理类2 //INotificationHandler<TestEvent>,订阅事件TestEvent //Handle方法,为事件处理程序,事件发布后执行 public class TestEventHandler2 : INotificationHandler<TestEvent> { public async Task Handle(TestEvent notification, CancellationToken cancellationToken) { await File.WriteAllTextAsync("d:/1.txt",$"TestEventHandler2收到了{notification.Name}"); } }
(3)事件发布类,此例中使用控制器来发布
//控制器TestPublishController中,注入IMediator,调用IMediator的Publish方法发布一对多事件 //IMediator还有另外一个方法,Send方法,用于发布一对一事件 [Route("api/[controller]/[action]")] [ApiController] public class TestPublishController : ControllerBase { private readonly IMediator mediator; public TestPublishController(IMediator mediator) { this.mediator = mediator; } [HttpPost] public async Task<ActionResult> PublishName(string name) { //发布事件,此处使用await,需要等待全部事件处理程序执行完后,才会向下执行 //如果不使用await,则不会等待 //特殊情况:即使使用了await,如果事件处理程序本身是异步的,也不会等待这个异步Handler //await能够保证事务的一致性,但需要耗时等待,如果不等待,则需要自行处理事务一致性问题 await mediator.Publish(new TestEvent(name)); return Ok("已发布事件"); } }
二、Mediat的进阶使用
无论是应用服务,还是领域服务,最终都要调用聚合根中的方法来操作聚合。如果领域事件都在聚合根中发布,就可以确保领域事件不会被漏掉。但是这种模式存在两个问题:(1)我们可能定义了多个修改实体信息的方法,如ChangeName,ChangeEmail,如果每次调用这些方法都发布领域事件,则存在重复发布事件的问题;(2)领域事件可能发布太早,如在构造方法中发布了新增实体的事件,但可能因为数据校验没有通过,新增实体失败,但事件已经发布。为了解决以上问题,书中提供了一个解决方案,在聚合根中定义一个事件集合,领域事件只添加到集合中(称之为注册),暂不发布。将发布事件的工作,延迟到上下文保存修改时才进行。
1、准备工作
1.1 定义注册领域事件相关方法的接口IDomainEvents
public interface IDomainEvents { IEnumerable<INotification> GetDomainEvents(); //获取领域事件集合 void AddDomainEvent(INotification eventItem); //将领域事件添加到集合中 void AddDomainEventIfAbsent(INotification eventItem); //集合中不存中相同领域事件时,才添加领域事件 void ClearDomainEvents(); //清除领域事件 }
1.2 创建实现IDomainEvents接口的抽象实体基类BaseEntity,聚合根实体类都继承自BaseEntity
public class BasicEntity : IDomainEvents { //初始化一个领域事件的集合 private List<INotification> DomainEvents = new(); //将领域事件添加到集合中 public void AddDomainEvent(INotification eventItem) { DomainEvents.Add(eventItem); } //如果集合中不存中相同的领域事件,则添加到集合 public void AddDomainEventIfAbsent(INotification eventItem) { if (!DomainEvents.Contains(eventItem)) { DomainEvents.Add(eventItem); } } //获取领域事件集合 public IEnumerable<INotification> GetDomainEvents() { return DomainEvents; } //清除领域事件集合 public void ClearDomainEvents() { DomainEvents.Clear(); } }
1.3 封装一个DbContext上下文基类BaseDbContext,重写SaveChanges和SaveChangesAsync方法,在SaveChanges时发布领域事件
public abstract class BaseDbContext:DbContext { //注入IMediat服务 private readonly IMediator mediator; public BaseDbContext(DbContextOptions options,IMediator mediator):base(options) { this.mediator = mediator; } //重写SaveChanges方法,禁用它 public override int SaveChanges(bool acceptAllChangesOnSuccess) { throw new NotImplementedException("不要使用同步的SaveChanges方法"); } //重写SaveChangesAsync方法 public override async Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) { //ChangeTracker方法,获取所有实现了IDomainEvents接口的实体类,且实体类中的领域事件集合不为空 //GetDomainEvents()方法,是我们在IDomainEvents接口中定义的方法,返回聚合根的领域事件集合 var domainEntities = this.ChangeTracker.Entries<IDomainEvents>().Where(x=>x.Entity.GetDomainEvents().Any()); //获取所有领域事件 var domainEvents = domainEntities.SelectMany(x=>x.Entity.GetDomainEvents()).ToList(); //因为已经获取了所有领域事件,所以可以清除聚合根中的领域事件 domainEntities.ToList().ForEach(x => x.Entity.ClearDomainEvents()); //发布领域事件 foreach (var domainEvent in domainEvents) { await mediator.Publish(domainEvent); } //调用基类的SaveChangesAsync方法,即在调用基类的保存数据修改前,发布了领域事件 return await base.SaveChangesAsync(acceptAllChangesOnSuccess,cancellationToken); } }
2、定义、订阅和注册事件,调用DbContext上下文的SaveChanges方法,自动发布事件
2.1 定义事件:以下案例定义了三个事件类
public record UserAddEvent(User User):INotification; //添加用户 public record UserUpdateEvent(Guid Id):INotification; //修改用户 public record UserSoftDeleteEvent(Guid Id): INotification; //软删除用户
2.2 创建事件处理类(订阅事件):以下案例创建了两个事件处理类
//①添加用户事件处理类 public class UserAddEventHandler : INotificationHandler<UserAddEvent> { //注入ILogger日志服务 private readonly ILogger<UserAddEventHandler> logger; public UserAddEventHandler(ILogger<UserAddEventHandler> logger) { this.logger = logger; } //伪发送邮件 public Task Handle(UserAddEvent notification, CancellationToken cancellationToken) { var user = notification.User; logger.LogInformation($"向{user.Email}发送邮件"); return Task.CompletedTask; } } //②修改用户事件处理类 public class UserModifyEventHandler : INotificationHandler<UserUpdateEvent> { //注入ILogger和DbContext private readonly MyDbContext myDbContext; private readonly ILogger<UserModifyEventHandler> logger; public UserModifyEventHandler(MyDbContext myDbContext,ILogger<UserModifyEventHandler> logger) { this.myDbContext = myDbContext; this.logger = logger; } //伪发邮件给用户 public async Task Handle(UserUpdateEvent notification, CancellationToken cancellationToken) { var user = await myDbContext.Users.FindAsync(notification.Id); logger.LogInformation($"通知用户{user.Email}信息已经更改"); } }
2.3 在聚合实体类中注册事件
public class User:BaseEntity { public Guid Id { get; init; } //Guid类型的ID,只允许在创建对象时初始化 public string UserName { get; init; } //用户名,只允许在创建对象时初始化 public string Email { get; init; } //Email,只允许在创建对象时初始化 public string? NickName { get; private set; } //呢称,只允许在类内部修改 public int? Age { get; private set; } //年龄,只允许在类内部修改 public bool IsDeleted { get; private set; } //软删除,只允许在类内部修改 private User() { } //私有无参构造函数,EFCore默认使用 //调用BaseEntity的AddDomainEvent方法注册事件UserAddEvent public User(Guid id, string userName, string email) { this.Id = id; this.UserName = userName; this.Email = email; AddDomainEvent(new UserAddEvent(this)); } //调用BaseEntity的AddDomainEventIfAbsent方法注册事件UserUpdateEvent public void ChangeNickName(string? newNickName) { this.NickName = newNickName; AddDomainEventIfAbsent(new UserUpdateEvent(Id)); } //调用BaseEntity的AddDomainEventIfAbsent方法注册事件UserUpdateEvent public void ChangeAge(int newAge) { this.Age = newAge; AddDomainEventIfAbsent(new UserUpdateEvent(Id)); } //调用BaseEntity的AddDomainEvent方法注册事件UserSoftDeleteEvent public void SoftDelete() { this.IsDeleted = true; AddDomainEvent(new UserSoftDeleteEvent(Id)); } }
2.4 调用上下文的SaveChanges方法,自动发布事件。在控制器中,不再需要注入IMediat服务
[Route("api/[controller]/[action]")] [ApiController] public class TestPublishController : ControllerBase { private readonly MyDbContext ctx; public TestPublishController(MyDbContext ctx) { this.ctx = ctx; } //新增用户Action [HttpPost] public async Task<ActionResult> AddUser(User user) { var u1 = new User(Guid.NewGuid(), user.UserName, user.Email); ctx.Users.Add(u1); await ctx.SaveChangesAsync(); return Ok($"添加成功,新用户的ID为{u1.Id}"); } //修改用户Action [HttpPut] public async Task<ActionResult> UpdateUser(Guid id,string nickName,int age) { var u1 = ctx.Users.Find(id); u1.ChangeNickName(nickName); u1.ChangeAge(age); await ctx.SaveChangesAsync(); return Ok("修改成功"); } }
特别说明:
1、本系列内容主要基于杨中科老师的书籍《ASP.NET Core技术内幕与项目实战》及配套的B站视频视频教程,同时会增加极少部分的小知识点
2、本系列教程主要目的是提炼知识点,追求快准狠,以求快速复习,如果说书籍学习的效率是视频的2倍,那么“简读系列”应该做到再快3-5倍
标签:Core,ASP,5.4,事件处理,await,private,领域,事件,public From: https://www.cnblogs.com/functionMC/p/16927301.html