加载聚合对域事件的反应

Loading aggregates on reacting to domain events

我正在使用领域驱动设计和事件溯源来实现一个应用程序。我将所有域事件存储在 SQL 服务器中的 DomainEvents table 中。

我有以下聚合:

- City
   + Id
   + Enable()
   + Disable()

- Company
   + Id
   + CityId
   + Enable()
   + Disable()

- Employee
   + Id
   + CompnayId
   + Enable()
   + Disable()

每一个都封装了自己的领域逻辑和不变量。我将它们设计为单独的聚合,因为一个城市可能有数千(也许更多)公司,而且公司也可能有非常多的员工。如果这些实体属于同一个聚合,我必须将它们加载在一起,这在大多数情况下是不必要的。

调用 Enable 或 Disable 将产生域事件(例如 CityEnabledCompanyDisabledEmployeeEnabled)。这些事件包含启用或禁用实体的主键。

现在我的问题是一个新要求迫使我 enable/disable 所有相关的公司如果一个城市是 enabled/disabled。如果公司是 enabled/disabled.

,则员工也需要同样的要求

在我的事件处理程序中,如果例如 CityDisabled 发生,则会调用该事件处理程序 我需要为属于该城市的每个公司执行DisableCompanyCommand

但我怎么知道哪些公司会受到该变化的影响?

我的想法:

  1. 查询事件存储是不可能的,因为我不能使用像'where CityId = event.CityId'

    这样的条件
  2. 让父级知道其子级 ID 并将所有子级 ID 放入父级生成的每个事件中。这也是一个坏主意,因为事件创建者不应该关心以后谁会使用这些事件。所以只有属于正在发生的事件的信息应该在事件中。

  3. 为每个公司执行DisableCompanyCommand。只有匹配 CityId 的公司才会更改其状态。即使我会异步执行此操作,它也会产生巨大的开销,让每个公司都对这些事件产生负担。而且对于每家被禁用的公司,都应重复相同的程序以禁用所有用户。

  4. 创建读取模型将ParentIds 映射到ChildIds 并根据事件中的parentId 加载childIds。这听起来是最合适的解决方案,但问题是,我如何在禁用现有公司的同时知道是否创建了新公司?

我对以上任何解决方案都不满意。基本上,问题是确定已发生事件的受影响聚合。

也许您有更好的解决方案?

您所描述的内容可以通过侦听 CityDisabled 事件的 Saga/Process manager 来解决。然后它找到 City 中所有 CompaniesCompanyIds(通过使用现有的 Read model 或通过维护 CityIdsx[=11= 的私有状态]) 并向每个人发送一个 DisableCompany 命令。

同样适用于CompanyDisabled事件,关于禁用Employee

P.S。禁用 City/Company/Employee 对我来说似乎是 CRUD,这些似乎不是来自 普通 普遍存在语言的术语,它不是很 DDD-ish 但我认为你的设计是正确的关于这个问题。

您的要求是否意味着您必须在禁用城市时触发 CompanyDisabled 事件?

如果不是 - 并且您的要求只是禁用城市意味着所有公司都禁用,那么您要做的是在您的城市阅读模型投影中监听 CityDisabled 事件并在阅读中将公司标记为禁用模型。 (如果你的要求是为每个城市触发一个事件,那么康斯坦丁的答案就很好)

您的模型更像是一种子/父关系 - 它打破了传统的 "blue book" 思想,但我建议在您的域中用不止一个 CityId 来表示这种关系。

在我的应用程序中,类似这样的内容将被编码为

public Task Handle(DoSomething command, IHandlerContext ctx) 
{
   var city = ctx.For<City>().Get(command.CityId);
   var company = city.For<Company>().Get(command.CompanyId);

   company.DoSomething();
}

public Company : Entity<City>
{

   public void DoSomething()
   {
        // Parent is the City
        if(Parent.Disabled)
            throw new BusinessException("City is disabled");

        Apply<SomethingDone>(x => {
            x.CityId = Parent.Id;
            x.CompanyId = Id;
            ...
        });
   }

}

(伪代码是 NServiceBus 样式代码并使用我的库 Aggregates.NET)

很可能您不必明确强制执行诸如“enable/disable 所有相关公司如果城市是 enabled/disabled”之类的规则域(写)端。

如果是这样,则在禁用域内的城市时无需禁用所有公司。正如查尔斯在他的回答中提到的,只需引入一个规则,例如 "a Company is disabled if it is disabled itself (directly) or its City is disabled"。公司及其员工也是如此。

这条规则应该在读取端实现。读取模型中的 Company 将具有 2 个属性:第一个是直接从域映射的 Enabled;第二个是 EnabledEffective,可根据公司的 Enabled 值及其城市的 Enabled 值计算得出。当 CityDisabled 事件发生时,读取模型的事件处理程序遍历读取模型中城市的所有 Companies 并将其 EnabledEffective 属性 设置为 false;当 CityEnabled 事件发生时,处理程序将城市的每个公司的 EnabledEffective 属性 设置回其自己的 Enabled 值。您将在 UI.

中使用的是 EnabledEffective 属性

CompanyEnabled/CompanyDisabled 事件处理(关于员工)的逻辑可能有点复杂,因为您必须同时考虑事件信息和主办城市的 enabled/disabled 状态。

如果域端确实需要 Company/Employee 的(有效)enabled/disabled 状态(例如,影响这些聚合处理其命令的方式),请考虑采用 EnabledEffective 读取端的值并将其与命令对象一起传递。