【DDD】领域事件
领域事件何时发布
领域事件的发布应该放在聚合中,因为无论是应用服务还是领域服务,最终要调用聚合根中的方法来操作聚合,我们这样做可以确保领域事件不会被漏掉。并且应该在SaveChange()
时,发布事件,否则会造成重复发布或发布太早。
微软开源的eShopOnContainers项目中的做法:把领域事件的发布延迟到上下文保存修改时。实体中只是注册要发布的领域事件,然后在上下文 的SaveChanges方法被调用时,我们再发布事件。
实现
IDomainEvents:
供聚合根进行事件注册的接口
public interface IDomainEvents
{
IEnumerable<INotification> GetDomainEvents();
void AddDomainEvent(INotification eventItem);
void AddDomainEventIfAbsent(INotification eventItem);
void ClearDomainEvents();
}
BaseEntity:
public abstract class BaseEntity : 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 void ClearDomainEvents()
{
DomainEvents.Clear();
}
public IEnumerable<INotification> GetDomainEvents()
{
return DomainEvents;
}
}
Events:
public record UserAddedEvent(User Item):INotification;
public record UserUpdatedEvent(Guid Id):INotification;
public record UserSoftDeletedEvent(Guid Id):INotification;
实体:
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()
{
//提供无参构造方法。避免EF Core加载数据的时候调用有参的构造方法触发领域事件
}
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 ChangeEmail(string value)
{
this.Email = value;
AddDomainEventIfAbsent(new UserUpdatedEvent(Id));
}
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 SoftDelete()
{
this.IsDeleted = true;
AddDomainEvent(new UserSoftDeletedEvent(Id));
}
}
BaseDbContext:
public abstract class BaseDbContext : DbContext
{
private IMediator mediator;
public BaseDbContext(DbContextOptions options, IMediator mediator) : base(options)
{
this.mediator = mediator;
}
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
throw new NotImplementedException("Don not call SaveChanges, please call SaveChangesAsync instead.");
}
public async override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
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());
foreach (var domainEvent in domainEvents)
{
await mediator.Publish(domainEvent);
}
return await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
}