9.2领域事件
基本使用
领域(近似理解为实现某个功能的多个模型)事件可以切断领域模型之间的强依赖关系,事件发布后,由事件的处理者决定如何响应事件,以便于实现事件发布和事件处理的解耦。
MediatR
可实现进程内事件的传递,支持一对一和一对多,使用步骤如下:
- NuGet安装
MediatR.Extensions.Microsoft.DependencyInjection
- 在Program.cs中调用
AddMediatR
方法进行注册,参数为事件处理者所在的程序集
builder.Services.AddMediatR(Assembly.Load("用MediatR实现领域事件"));
- 定义在发布者和处理者之间进行数据传递的类,即消息类型。该类需要实现
INotification
接口
public record TestEvent(string UserName) : INotification;
- 事件处理者要实现
NotificationHandler<TNotification>
接口,泛型参数代表的是要处理的消息类型
publicclassTestEventHandler1 : INotificationHandler<TestEvent>
{
publicTaskHandle(TestEventnotification, CancellationTokencancellationToken)
{
Console.WriteLine($"我收到了{notification.UserName}");
returnTask.CompletedTask;
}
}
publicclassTestEventHandler2 : INotificationHandler<TestEvent>
{
publicasyncTaskHandle(TestEventnotification, CancellationTokencancellationToken)
{
awaitFile.WriteAllTextAsync("d:/1.txt", $"来了{notification.UserName}");
}
}
- 在需要发布事件的类中注入
IMediator
类型的服务,调用Publish
方法来发布一对多事件,Send
用来发布一对一事件。
[Route("api/[controller]/[action]")]
[ApiController]
publicclassTestController : ControllerBase
{
privatereadonlyIMediatormediator;
publicTestController(IMediatormediator)
{
this.mediator=mediator;
}
[HttpPost]
publicasyncTask<IActionResult>Login(LoginRequestreq)
{
//不要写成Send
//如果使用await那么要等所有的Handle方法执行完才能继续执行
awaitmediator.Publish(newTestEvent(req.UserName));
returnOk("ok");
}
}
EF Core中发布领域事件
我们一般在操作EF Core的changeName、构造方法等方法中调用IMediator的publish方法来发布领域事件,但是在这些方法中立即处理发布的事件会有以下问题:
- 可能存在重复发送领域事件的情况。如分别调用changeName,changeAge方法进行修改,由于每个changeXXX都会发布“实体类被修改”的事件,到导致出多次处理事件,其实只需最后执行一次就可以。
- 领域事件发布的太早。为了能够发布“新增实体类”的领域事件,我们一般在实体类的构造方法中发布领域事件,但可能存在数据验证没有通过等等的原因最终没有将新增实体类保存在数据库,那就会出现了事件发布过早的错误问题。
解决方法:把领域事件的发布延迟到上下文修改时,即在实体类中仅仅是注册领域事件,而在上下文中的SaveChanges方法中发布事件。
实现步骤:
- 为了方便实体类关于领域事件的管理,定义接口
publicinterfaceIDomainEvents
{
IEnumerable<INotification>GetDomainEvents();//获得注册的领域事件
voidAddDomainEvent(INotificationeventItem);//注册领域事件
voidAddDomainEventIfAbsent(INotificationeventItem);//如果领域事件不存在,则注册事件
voidClearDomainEvents();//清除领域事件
}
- 为了简化实体类的编写,定义实体类的抽象类,该抽象类要实现自定义的IDomainEvents接口
publicabstractclassBaseEntity : IDomainEvents
{
privateList<INotification>DomainEvents=new();
publicvoidAddDomainEvent(INotificationeventItem)
{
DomainEvents.Add(eventItem);
}
publicvoidAddDomainEventIfAbsent(INotificationeventItem)
{
if (!DomainEvents.Contains(eventItem))
{
DomainEvents.Add(eventItem);
}
}
publicvoidClearDomainEvents()
{
DomainEvents.Clear();
}
publicIEnumerable<INotification>GetDomainEvents()
{
returnDomainEvents;
}
}
- 需要在上下文中保存数据的时候发布注册的领域事件,为了简化上下文代码的编写,声明上下文抽象类
publicabstractclassBaseDbContext : DbContext
{
privateIMediatormediator;//依赖注入
publicBaseDbContext(DbContextOptionsoptions, IMediatormediator) : base(options)
{
this.mediator=mediator;
}
//强制不能使用同步方法
publicoverrideintSaveChanges(boolacceptAllChangesOnSuccess)
{
thrownewNotImplementedException("Don not call SaveChanges, please call SaveChangesAsync instead.");
}
//重写父类的方法
publicasyncoverrideTask<int>SaveChangesAsync(boolacceptAllChangesOnSuccess, CancellationTokencancellationToken=default)
{ //ChangeTracker是上下文用来对实体类变化进行追踪的对象
//Entries<IDomainEvents>获得所有实现了IDomainEvents接口的实体类
//只要包含了领域事件的所有实体
vardomainEntities=this.ChangeTracker.Entries<IDomainEvents>()
.Where(x=>x.Entity.GetDomainEvents().Any());
//获得所有实体的所有领域事件
vardomainEvents=domainEntities
.SelectMany(x=>x.Entity.GetDomainEvents()).ToList();
//清除所有实体类中的所有领域事件,因为执行完了就要在集合中清除
domainEntities.ToList()
.ForEach(entity=>entity.Entity.ClearDomainEvents());
//发布所有的领域事件,要放到代用父类的SaveChangeAsync之前
//因为这样事件的处理代码会在上下文模型修改保存之前执行
foreach (vardomainEventindomainEvents)
{
awaitmediator.Publish(domainEvent);
}
//调用父类方法
returnawaitbase.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
}
- 编写传递领域事件的类
publicrecordUserUpdatedEvent(GuidId):INotification;
publicrecordUserAddedEvent(UserItem):INotification;
- 编写实体类
publicclassUser: BaseEntity
{
publicGuidId { get; init; }
publicstringUserName { get; init; }
publicstringEmail { get; privateset; }
publicstring?NickName { get; privateset; }
publicint?Age { get; privateset; }
publicboolIsDeleted { get; privateset; }
privateUser()
{
//提供无参构造方法。避免EF Core加载数据的时候调用有参的构造方法触发领域事件
}
publicUser(stringuserName,stringemail)
{
this.Id=Guid.NewGuid();
this.UserName=userName;
this.Email=email;
this.IsDeleted=false;
//构造方法中注册增加用户事件,放到集合中
AddDomainEvent(newUserAddedEvent(this));
}
publicvoidChangeEmail(stringvalue)
{
this.Email=value;
//避免重复注册
AddDomainEventIfAbsent(newUserUpdatedEvent(Id));
}
publicvoidChangeNickName(string?value)
{
this.NickName=value;
AddDomainEventIfAbsent(newUserUpdatedEvent(Id));
}
publicvoidChangeAge(intvalue)
{
this.Age=value;
AddDomainEventIfAbsent(newUserUpdatedEvent(Id));
}
}
- 实体上下文类继承自上面的
BaseDbContext
- 编写事件处理类
publicclassNewUserSendEmailHandler : INotificationHandler<UserAddedEvent>
{
privatereadonlyILogger<NewUserSendEmailHandler>logger;
publicNewUserSendEmailHandler(ILogger<NewUserSendEmailHandler>logger)
{
this.logger=logger;
}
publicTaskHandle(UserAddedEventnotification, CancellationTokencancellationToken)
{
varuser=notification.Item;
logger.LogInformation($"向{user.Email}发送欢迎邮件");
returnTask.CompletedTask;
}
}
publicclassModifyUserLogHandler : INotificationHandler<UserUpdatedEvent>
{
privatereadonlyUserDbContextcontext;
privatereadonlyILogger<ModifyUserLogHandler>logger;
publicModifyUserLogHandler(UserDbContextcontext, ILogger<ModifyUserLogHandler>logger)
{
this.context=context;
this.logger=logger;
}
publicasyncTaskHandle(UserUpdatedEventnotification, CancellationTokencancellationToken)
{
//var user = await context.Users.SingleAsync(u=>u.Id== notification.Id);
varuser=awaitcontext.Users.FindAsync(notification.Id);
logger.LogInformation($"通知用户{user.Email}的信息被修改");
}
}
- 在控制器中使用user的增删改查
[HttpPut]
[Route("{id}")]
publicasyncTask<IActionResult>Update(Guidid,UpdateUserRequestreq)
{
User?user=context.Users.Find(id);
if (user==null)
{
returnNotFound($"id={id}的User不存在");
}
user.ChangeAge(req.Age);
user.ChangeEmail(req.Email);
user.ChangeNickName(req.NickName);
awaitcontext.SaveChangesAsync();
returnOk();
}