在持久化聚合之前发布域事件是否安全?

Dmi*_*diu 8 c# dns domain-driven-design

在许多不同的项目中,我看到了两种不同的方法来提升域事件.

  1. 直接从聚合中提升域事件.例如,假设您有Customer聚合,这里面是一个方法:

    public virtual void ChangeEmail(string email)
    {
        if(this.Email != email)
        {
            this.Email = email;
            DomainEvents.Raise<CustomerChangedEmail>(new CustomerChangedEmail(email));
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

    我可以看到这种方法存在两个问题.第一个是无论聚合是否持久化,都会引发事件.想象一下,如果您想在注册成功后向客户发送电子邮件.将引发事件"CustomerChangedEmail",即使未保存聚合,某些IEmailSender也会发送电子邮件.当前实现的第二个问题是每个事件都应该是不可变的.所以问题是如何初始化其"OccuredOn"属性?只在里面汇总!合乎逻辑,对吧!它迫使我将ISystemClock(系统时间抽象)传递给聚合中的每个方法!Whaaat ??? 难道你不觉得这个设计很脆弱吗?以下是我们将要提出的建议:

    public virtual void ChangeEmail(string email, ISystemClock systemClock)
    {
        if(this.Email != email)
        {
            this.Email = email;
            DomainEvents.Raise<CustomerChangedEmail>(new CustomerChangedEmail(email, systemClock.DateTimeNow));
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  2. 第二种方法是采用Event Sourcing模式建议的方法.在每个聚合上,我们定义一个未列出事件的(List)列表.请注意UncommitedEvent不是域事件!它甚至没有OccuredOn属性.现在,当在Customer Aggregate上调用ChangeEmail方法时,我们不会提出任何内容.我们只是将事件保存到我们的聚合中存在的uncommitedEvents集合.像这样:

    public virtual void ChangeEmail(string email)
    {
        if(this.Email != email)
        {
            this.Email = email;
            UncommitedEvents.Add(new CustomerChangedEmail(email));
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

那么,什么时候实际的域事件被提出?此职责委托给持久层.在ICustomerRepository中,我们可以访问ISystemClock,因为我们可以轻松地将其注入到存储库中.在ICustomerRepository的Save()方法中,我们应该从Aggregate中提取所有uncommitedEvents,并为每个事件创建一个DomainEvent.然后我们在新创建的Domain Event上设置OccuredOn属性.然后,在IN ONE TRANSACTION中,我们保存聚合并发布所有域事件.通过这种方式,我们将确保所有事件都将在具有聚合持久性的跨国界限中提升.
我对这种方法不喜欢什么?我不想为同一事件创建2种不同的类型,即对于CustomerChangedEmail行为,我应该有CustomerChangedEmailUncommited类型和CustomerChangedEmailDomainEvent.只有一种类型会很好.请分享您对此主题的体验!

Ebe*_*oux 4

我不支持您提出的两种技术中的任何一种:)

现在我赞成从域返回事件或响应对象:

public CustomerChangedEmail ChangeEmail(string email)
{
    if(this.Email.Equals(email))
    {
        throw new DomainException("Cannot change e-mail since it is the same.");
    }

    return On(new CustomerChangedEmail { EMail = email});
}

public CustomerChangedEmail On(CustomerChangedEmail customerChangedEmail)
{
    // guard against a null instance
    this.EMail = customerChangedEmail.EMail;

    return customerChangedEmail;
}
Run Code Online (Sandbox Code Playgroud)

通过这种方式,我不需要跟踪我的未提交事件,也不需要依赖全局基础设施类,例如DomainEvents. 应用层以与没有 ES 相同的方式控制事务和持久性。

至于协调发布/保存:通常另一层间接会有所帮助。我必须提到,我认为 ES 事件与系统事件不同。系统事件是有界上下文之间的事件。消息传递基础设施将依赖于系统事件,因为系统事件通常比领域事件传达更多信息。

通常,在协调诸如发送电子邮件之类的事情时,人们会利用流程管理器或其他一些实体来携带状态。您可以将其Customer随身携带DateEMailChangedSent,如果为空,则需要发送。

步骤是:

  • 开始交易
  • 获取事件流
  • 拨打电话更改客户的电子邮件,甚至添加到事件流
  • 记录需要发送的电子邮件(DateEMailChangedSent 返回 null)
  • 保存事件流 (1)
  • 发送SendEMailChangedCommand消息 (2)
  • 提交交易 (3)

有几种方法可以完成消息发送部分,可以将其包含在同一事务中(无 2PC),但现在让我们忽略它。

假设之前我们在开始之前发送了一封电子邮件并DateEMailChangedSent有一个值,我们可能会遇到以下异常:

(1) 如果我们无法保存事件流,那么这没有问题,因为异常将回滚事务并且处理将再次发生。
(2) 如果由于某些消息传递失败而无法发送消息,那么没有问题,因为回滚会将所有内容设置回我们开始之前的状态。(3) 好吧,我们已经发送了我们的消息,因此提交时的异常可能看起来像是一个问题,但请记住,我们无法设置返回DateEMailChangedSentnull表明我们需要发送新电子邮件。

的消息处理程序SendEMailChangedCommand将检查DateEMailChangedSent,如果没有,null它将简单地返回,确认消息,然后消息就会消失。但是,如果它空,那么它将直接与电子邮件网关交互或通过消息传递使用某些基础设施服务端点来发送邮件(我更喜欢这样)。

好吧,无论如何,这就是我的看法:)

  • 此解决方案的唯一问题是,每次在域对象上调用任何域方法时,您还需要记住发布适当的事件。这是类似 DomainEvents 的解决方案背后的主要原因。该问题有点类似于持久性问题(无论是在域方法上还是在外部使用存储库来持久化实体)。然而,对于存储库来说,情况很简单——我们都同意应该在实体之外使用存储库。就我个人而言,我更倾向于从应用程序层发出事件,因为我相信它不是域层的一部分。 (3认同)