MediatR:EF Core中发布领域事件

领域事件大部分发生在领域模型的业务逻辑方法上或者领域服务上,我们可以在一个领域事件发生的时候立即调用IMediatorPublish方法来发布领域事件。

我们一般在聚合根的实体类对象的ChangeName、构造方法等方法中发布领域事件,因为无论是应用服务还是领域服务,最终都要调用聚合根中的方法来操作聚合,我们这样做可以确保领域事件不会被漏掉。但是在实体类的业务方法中立即进行领域事件的发布可能会有如下的问题:

  • 可能存在重复发送领域事件的情况。比如,在“修改用户信息”这个应用服务操作中,我们分别调用实体类的ChangeName、ChangeAge、ChangeEmail方法修改用户的姓名、年龄和邮箱。因为每个ChangeXXX方法中都会发布“实体类被修改”的领域事件,所以领域事件的处理者就会被多次调用,这是没有必要的,其实只要发布一次“实体类被修改”的领域事件即可。
  • 领域事件发布太早。为了确保新增加的实体类能够发布“新增实体类”的领域事件,我们需要在实体类的构造方法中发布领域事件,但是有可能因为数据验证没有通过等原因,我们最终没有把这个新增的实体类保存到数据库中,这样在构造方法中过早地发布领域事件就可能导致“误报”的问题。

参考微软开源的eShopOnContainers项目中的做法,把领域事件的发布延迟到上下文保存修改时也就是实体类中只注册要发布的领域事件,然后在上下文的SaveChanges方法被调用时,我们再发布领域事件。

第1步:
领域事件是由聚合根进行管理的,因此我们定义了供聚合根进行事件注册的接口IDomainEvents。

public interface IDomainEvents
{
//获取注册的领域事件
IEnumerable<INotification> GetDomainEvents();
//注册领域事件
void AddDomainEvent(INotification notification);
//如果领域事件不存在,则注册事件
void AddDomainEventIfAbsent(INotification notification);
//清除注册的领域事件
void ClearDomainEvents();
}

为了简化实体类的代码编写,我们编写实现了IDomainEvents接口的抽象实体类BaseEntity。

public abstract class BaseEntity : IDomainEvents
{
public List<INotification> DomainEvents = new();
public void AddDomainEvent(INotification notification)
{
DomainEvents.Add(notification);
}
public void AddDomainEventIfAbsent(INotification notification)
{
if (!DomainEvents.Contains(notification))
{
DomainEvents.Add(notification);
}
}
public void ClearDomainEvents()
{
DomainEvents.Clear();
}
public IEnumerable<INotification> GetDomainEvents()
{
return DomainEvents;
}
}

第2步:
我们需要在上下文中保存数据的时候发布注册的领域事件。在DDD中,每个聚合都对应一个上下文,因此项目中的上下文类非常多。为了简化上下文代码的编写,我们编写BaseDbContext类,将在SaveChanges中发布领域事件的代码封装到这个类中。

public abstract class BaseDbContext : DbContext
{
private IMediator _mediator;
public BaseDbContext(DbContextOptions options, IMediator mediator)
: base(options)
{
_mediator = mediator;
}
public override int SaveChanges(bool acceptAllChangesOnSucess)
{
//在项目中强制要求不能使用同步方法,因此对SaveChanges的调用抛出异常。
throw new NotImplementedException("未调用SaveChanges");
}
public async override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess,
CancellationToken cancellationToken = default)
{
//ChangeTracker是上下文中用来对实体类的变化进行追踪的对象,
//Entries<IDomainEvents>获得的是所有实现了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(entity => entity.Entity.ClearDomainEvents());
//在调用父类的SaveChangesAsync方法保存修改之前,
//我们把所有实体类中注册的领域事件发布出去
foreach (var domainEvent in domainEvents)
{
await _mediator.Publish(domainEvent);
}
return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
}

至此,我们完成了EF Core中简化领域事件发布的几个接口和抽象类的开发。

第3步:
接下来,我们编写用来测试的实体类和上下文。首先我们编写代表用户的实体类User。

public class User : BaseEntity
{
public Guid Id { get; init; }
public string UserName { get; init; }
public string Email { get; private set; }
public string? NickName { get; private set; }
public int? Age { get; private set; }
public bool IsDeleted { get; private set; }
private User() { }
public User(string userName, string email)
{
this.Id = Guid.NewGuid();
this.UserName = userName;
this.Email = email;
this.IsDeleted = false;
AddDomainEvent(new UserAddedEvent(this));
}
public void ChangeNickName(string? value)
{
this.NickName = value;
AddDomainEventIfAbsent(new UserUpdatedEvent(Id));
}
public void ChangeAge(int value)
{
this.Age = value;
AddDomainEventIfAbsent(new UserUpdatedEvent(Id));
}
public void ChangeEmail(string value)
{
this.Email = value;
AddDomainEventIfAbsent(new UserUpdatedEvent(Id));
}
public void SoftDelete()
{
this.IsDeleted = true;
AddDomainEvent(new UserSoftDeletedEvent(Id));
}
}
public record UserAddedEvent(User Item) : INotification;
public record UserUpdatedEvent(Guid Id) : INotification;
public record UserSoftDeletedEvent(Guid Id) : INotification;

我们在有参构造方法中,AddDomainEvent发布了UserAddedEvent领域事件,这样当我们创建新的实体类并且保存修改的时候,这个领域事件就会被发布。但是如果EF Core从数据库中加载已有数据的时候,也执行有参构造方法,就会导致在加载数据的时候也发布UserAddedEvent领域事件,这就发生逻辑错误了,因此我们提供了一个无参构造方法供EF Core从数据库中加载数据时使用。

因为我们可能连续调用ChangeNickName、ChangeAge等方法,所以我们通过AddDomainEventIfAbsent注册领域事件,从而避免消息的重复发布。

第4步:
接下来,我们编写事件处理类来对这些领域事件进行处理。首先我们编写响应UserAddedEvent领域事件,然后向用户发送注册邮件的NewUserSendEmailHandler类。

public class NewUserSendEmailHandler : INotificationHandler<UserAddedEvent>
{
private readonly ILogger<NewUserSendEmailHandler> logger;
public NewUserSendEmailHandler(ILogger<NewUserSendEmailHandler> logger)
{
this.logger = logger;
}
public Task Handle(UserAddedEvent notification, CancellationToken cancellationToken)
{
var user = notification.User;
logger.LogInformation($"向{user.Email}发送欢迎邮件");
return Task.CompletedTask;
}
}

还有一个“当用户的个人信息被修改后,发邮件通知用户的事件处理者”的功能。

public class ModifyUserLogHandler : INotificationHandler<UserUpdatedEvent>
{
private readonly UserDbContext context;
private readonly ILogger<ModifyUserLogHandler> logger;
public ModifyUserLogHandler(UserDbContext context,
ILogger<ModifyUserLogHandler> logger)
{
this.context = context;
this.logger = logger;
}
public async Task Handle(UserUpdatedEvent notification,
CancellationToken cancellationToken)
{
var user = await context.Users.FindAsync(notification.Id);
logger.LogInformation($"通知用户{user.Email}的信息被修改");
}
}

因为UserUpdatedEvent中只包含被修改用户的标识符,所以我们通过FindAsync获取被修改用户的详细信息。因为FindAsync会首先从上下文的缓存中获取对象,而修改操作之前被修改的对象已经存在于缓存中了,所以用FindAsync不仅能够获取还没有提交到数据库的对象,而且由于FindAsync操作不会再到数据库中查询,因此程序的性能更高。

最后:
我们编写一个控制器类UsersController来执行用户新增、用户修改等操作。

[Route("api/[controller]/[action]")]
[ApiController]
public class UsersController : ControllerBase
{
private UserDbContext context;
public UsersController(UserDbContext context)
{
this.context = context;
}
[HttpPost]
public async Task<IActionResult> Add(AddUserRequest req)
{
var user = new User(req.UserName, req.Email);
context.Users.Add(user);
await context.SaveChangesAsync();
return Ok();
}
[HttpPut]
[Route("{id}")]
public async Task<IActionResult> Update(Guid id, UpdateUserRequest req)
{
User? user = context.Users.Find(id);
if (user == null)
{
return NotFound($"id={id}的User不存在");
}
user.ChangeAge(req.Age);
user.ChangeEmail(req.Email);
user.ChangeNickName(req.NickName);
await context.SaveChangesAsync();
return Ok();
}
[HttpDelete]
[Route("id")]
public async Task<IActionResult> Delete(Guid id)
{
User? user = context.Users.Find(id);
if (user == null)
{
return NotFound($"id={id}的User不存在");
}
user.SoftDelete();
await context.SaveChangesAsync();
return Ok();
}
}

运行后,可以看到,UserAddedEvent和UserUpdatedEvent两个领域事件的事件处理者的代码都执行了。在UsersController中,我们调用了多个ChangeXXX方法,这些方法都通过AddDomainEventIfAbsent方法向聚合根中注册领域事件,只有一个领域事件注册成功了,因此在修改用户的时候,UserUpdatedEvent事件只被发布了一次。

控制器中的方法Update是一个典型的应用服务。我们在ChangeAge等领域方法中只修改数据,并不会立即把修改保存到数据库中,因为只有应用服务才是最终面对用户请求的地方,只有应用服务才知道什么时候把对数据的修改保存到数据库中。我们调用context.SaveChangesAsync标志工作单元的结束,由于我们在此方法中把发布领域事件的代码放到了调用父类上下文的SaveChangesAsync方法之前,而领域事件的处理者的代码也是同步运行的,因此领域事件的处理者的代码也会在把上下文中模型的修改保存到数据库之前执行,这样所有的代码都在同一个数据库事务中执行,就构成了一个强一致性的事务。

本文学习参考自:ASP.NET Core技术内幕与项目实战

posted @   一纸年华  阅读(320)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示