事件溯源:将事件重播到聚合的子实体

本文关键字:事件 实体 溯源 | 更新日期: 2023-09-27 18:35:11

当涉及到为聚合重放事件时,如何将这些事件Apply到子(非根)实体。

到目前为止,我有两个关于如何做到这一点的想法。

  1. 获取聚合根以将事件路由到适当的实体
  2. 有一个聚合加载程序,该加载程序按 id 加载实体并直接应用其事件

你采取了什么方法,什么有效,什么没有?


在我的搜索中,我只找到了两个讨论问题的链接(都采用了第一种方法):

复杂骨料结构 (4.2.3.)
聚合根,在事件溯源系统中协调其实体

事件溯源:将事件重播到聚合的子实体

作为上述讨论的参与者之一,我可以分享一些见解。如果你看一下像NCQRS这样的项目,它以一种相当明确的方式正式化了实体的构建和水合方式,你会注意到这种方法有一定的刚性。对于实体,我发现我将它们的存储从简单的字段、列表或字典演变为聚合中的状态,具体取决于维护它们的分散行为。较低的刚性带来自由,在聚合边界内选择建模。我非常珍惜这一点。

解除冻结时的事件路由发生在聚合内部。这不是应该外化IMO的东西。有多种方法可以做到这一点。在我自己的项目中,这就是我以非常轻量级的方式将其形式化的方式(此处仅显示实体):

/// <summary>
/// Base class for aggregate entities that need some basic infrastructure for tracking state changes on their aggregate root entity.
/// </summary>
public abstract class Entity : IInstanceEventRouter
{
    readonly Action<object> _applier;
    readonly InstanceEventRouter _router;
    /// <summary>
    /// Initializes a new instance of the <see cref="Entity"/> class.
    /// </summary>
    /// <param name="applier">The event player and recorder.</param>
    /// <exception cref="System.ArgumentNullException">Thrown when the <paramref name="applier"/> is null.</exception>
    protected Entity(Action<object> applier)
    {
        if (applier == null) throw new ArgumentNullException("applier");
        _applier = applier;
        _router = new InstanceEventRouter();
    }
    /// <summary>
    /// Registers the state handler to be invoked when the specified event is applied.
    /// </summary>
    /// <typeparam name="TEvent">The type of the event to register the handler for.</typeparam>
    /// <param name="handler">The state handler.</param>
    /// <exception cref="System.ArgumentNullException">Thrown when the <paramref name="handler"/> is null.</exception>
    protected void Register<TEvent>(Action<TEvent> handler)
    {
        if (handler == null) throw new ArgumentNullException("handler");
        _router.ConfigureRoute(handler);
    }
    /// <summary>
    /// Routes the specified <paramref name="event"/> to a configured state handler, if any.
    /// </summary>
    /// <param name="event">The event to route.</param>
    /// <exception cref="ArgumentNullException">Thrown when the <paramref name="event"/> is null.</exception>
    public void Route(object @event)
    {
        if (@event == null) throw new ArgumentNullException("event");
        _router.Route(@event);
    }
    /// <summary>
    /// Applies the specified event to this instance and invokes the associated state handler.
    /// </summary>
    /// <param name="event">The event to apply.</param>
    protected void Apply(object @event)
    {
        if (@event == null) throw new ArgumentNullException("event");
        _applier(@event);
    }
}

行为执行期间的事件路由遵循实体生命周期的轮廓:创建、修改和删除。在创建过程中,Apply 方法创建一个新实体(请记住,这是我们唯一可以更改状态的地方),并将其分配给字段,将其添加到列表、字典或自定义集合中。Apply 方法中的修改通常涉及查找受影响的一个或多个实体,并将事件路由到实体,或者在实体上使用事件中的数据应用更改的专用内部方法。删除时,Apply 方法将执行查找并删除受影响的一个或多个实体。请注意这些 Apply 方法如何与解除冻结阶段混合以达到相同的状态。

现在,重要的是要了解可能还有其他行为会影响实体,但并不"属于"任何特定实体(可以这么说,没有一对一的映射)。它只是发生的事情,并对一个或多个实体产生副作用。正是这些东西让你欣赏实体设计的灵活性。