DDD 事件源为创建的对象引发事件

DDD Event Source raise event for created object

我有类别 class,它有子项 属性。创建类别时,我在构造函数中引发事件 CategoryCreated,它在 BaseCategory 中注册此事件。我在类别中也有应用方法,它将事件应用于状态。

 public class Category :BaseCategory
{
    public Category(string id, TranslatableString name, DateTime timestamp)
    {
        Raise(new CategoryCreated(id, name, timestamp));
    }
}
  public override void Apply(DomainEvent @event)
    {
        switch (@event)
        {
            case CategoryCreated e:
                 this.Id = e.Id;
                 this.Name = e.Name;
                 break;
                ...

现在假设我要创建类别并向其添加子项。

var category = new Category("1","2",DateTime.UtcNow);
category.AddChild("some category", "name", DateTime.UtcNow);
foreach(var e in category.UncomittedEvents)
{  
    category.Apply(e);
}

添加子项时,我将新建类别的私有 属性 ParentId 设置为父项的 Id。

 public void AddChild(string id, string name,DateTime date)
    {
        if (string.IsNullOrWhiteSpace(id))
            throw new ArgumentNullException(nameof(id));
        if (Children.Any(a => a.Id== Id))
            throw new InvalidOperationException("Category already exist ");
        Raise(new CategoryAdded(Guid.NewGuid().ToString(), this.Id/*parent id*/, name, DateTime.UtcNow));
    }


  public class CategoryAdded : DomainEvent
    {
        public CategoryAdded(string id, string parentId, string name, DateTime timestamp) {}
    }

问题是,在应用事件时,父 ID 将为空,因为事件尚未应用并且作为父 ID 传递的父 ID 属性 为空:

new CategoryAdded(Guid.NewGuid().ToString(), this.Id /*parent id*/, name, DateTime.UtcNow)

设计错误在哪里? 应该在何时何地引发 CategoryCreated 事件? 你会如何处理这种情况?

Where is design mistake?

你的 Raise(...) 方法也应该调用 Apply。请记住,您的聚合负责维护一致的状态。在聚合之外应用事件违反了该原则。

protected void Raise(DomainEvent @event)
{
    this.Apply(@event);
    this.UncomittedEvents.Add(@event);
}

Where is design mistake? Where and when should be CategoryCreated event raised? How would you tackle this situation?

好的,这不是你的错。文献糟透了

显示了修复症状的通用机制,但我认为了解发生了什么很重要。

如果我们以纯形式应用 "event sourcing" 模式,我们的 数据模型 看起来就像一个事件流:

class Category {
    private final List[Event] History;
}

对当前状态的更改将通过将事件附加到历史记录来实现。

public Category(string id, TranslatableString name, DateTime timestamp) {
    History.Add(new CategoryCreated(id, name, timestamp));
}

当前状态的查询将是方法,它将搜索事件历史记录以查找数据。

public Id Id() {
    Id current = null;
    History.forEach( e -> {
        if (e instance of CreatedEvent) {
            current = CreatedEvent.Id(e)
        }
    });
    return current
}

好消息是设计原则上比较简单。坏消息是性能很糟糕——阅读通常比写作更常见,但每次我们想阅读一些东西时,我们都必须浏览事件才能找到答案。

并不总是那么糟糕——在实体的整个生命周期中保持不变的属性通常会出现在第一个事件中;要获得 属性 的最新版本,您通常可以 向后 枚举历史记录,并在第一个(最近的)匹配项处停止。

但还是很别扭。因此,为了提高查询性能,我们将有趣的结果缓存在属性中——有效地使用快照来回答查询。但是为了让它起作用,我们需要在向历史记录中添加新事件时更新缓存值(快照)。

所以Raise方法应该做两件事,修改事件历史,修改快照。修改事件历史是通用的,因此工作通常会共享到一个公共基础中 class;但快照特定于我们要缓存的查询结果集合,因此该位通常在 "aggregate root" 本身内实现。

因为当我们从存储在数据库中的事件中恢复聚合时的快照应该与 Live Copy 相匹配,所以这种设计通常包括一个在两种设置中都使用的 Apply 方法。