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

C# 在持久化聚合之前发布域事件是否安全?,c#,dns,domain-driven-design,event-sourcing,C#,Dns,Domain Driven Design,Event Sourcing,在许多不同的项目中,我看到了两种引发域事件的不同方法 直接从聚合引发域事件。例如,假设您有Customer aggregate,其中有一个方法: public virtual void ChangeEmail(string email) { if(this.Email != email) { this.Email = email; DomainEvents.Raise<CustomerChangedEmail>(new Customer

在许多不同的项目中,我看到了两种引发域事件的不同方法

  • 直接从聚合引发域事件。例如,假设您有Customer aggregate,其中有一个方法:

    public virtual void ChangeEmail(string email)
    {
        if(this.Email != email)
        {
            this.Email = email;
            DomainEvents.Raise<CustomerChangedEmail>(new CustomerChangedEmail(email));
        }
    }
    
  • 那么,实际的域事件何时引发???这个职责被委派给持久层。在ICustomerRepository中,我们可以访问ISystemClock,因为我们可以轻松地将它注入到存储库中。在ICCustomerRepository的Save()方法中,我们应该从聚合中提取所有未限制的事件,并为每个事件创建一个DomainEvent。然后,我们在新创建的域事件上设置OccurdOn属性。然后,在一个事务中,我们保存聚合并发布所有域事件。通过这种方式,我们将确保所有事件都将在具有聚合持久性的跨国边界中引发。
    我不喜欢这种方法的什么地方?我不想为同一事件创建两种不同的类型,即对于CustomerChangedMail行为,我应该有CustomerChangedMailUncommitted类型和CustomerChangedMailDomainEvent。只要一种就好了。请分享您在这方面的经验

    我见过两种不同的引发域事件的方法

    历史上,有两种不同的方法。Evans在描述域驱动设计的战术模式时没有包括域事件

    在一种方法中,域事件充当事务中的协调机制。许多描述这种模式的帖子得出结论:

    请注意,上述代码将与常规域工作在同一事务中的同一线程上运行,因此应避免执行任何阻止活动,如使用SMTP或web服务

    ,通常的替代品,实际上是一种非常不同的动物,只要事件被写入记录簿,而不仅仅是用于协调写入模型中的活动

    当前实现的第二个问题是,每个事件都应该是不可变的。所以问题是如何初始化它的“occurrendon”属性?仅限内部聚合!这是合乎逻辑的,对吧!它迫使我将ISystemClock(系统时间抽象)传递给聚合上的每个方法

    当然-看

    如果你不认为时间是一个输入值,想想看,直到你做了——这是一个重要的概念

    在实践中,实际上有两个重要的时间概念要考虑。如果时间是域模型的一部分,那么它就是一个输入

    如果时间只是您试图保存的元数据,那么聚合不一定需要知道它——您可以将元数据附加到其他地方的事件。例如,一个答案是使用工厂实例创建事件,工厂本身负责附加元数据(包括时间)

    如何实现这一目标?一个代码示例对我很有帮助

    最直接的例子是将工厂作为参数传递给方法

    public virtual void ChangeEmail(string email, EventFactory factory)
    {
        if(this.Email != email)
        {
            this.Email = email;
            UncommitedEvents.Add(factory.createCustomerChangedEmail(email));
        }
    }
    
    应用层中的流看起来像

  • 根据请求创建元数据
  • 从元数据创建工厂
  • 将工厂作为参数传递
  • 然后,在一个事务中,我们保存聚合并发布所有域事件。通过这种方式,我们将确保所有事件都将在具有聚合持久性的跨国边界中引发

    通常,大多数人都尽量避免两阶段提交

    因此,发布通常不是事务的一部分,而是单独进行的。
    请看格雷格·杨的演讲。主要流程是订阅者从记录簿中提取事件。在这种设计中,推送模型是一种延迟优化。

    我不是您介绍的两种技术中任何一种的支持者:)

    现在我喜欢从域返回事件或响应对象:

    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;
    }
    
    这样,我就不需要跟踪未提交的事件,也不需要依赖全局基础结构类,例如
    DomainEvents
    。应用层控制事务和持久性的方式与不使用ES时相同

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

    通常,在协调诸如发送电子邮件之类的事情时,会使用流程管理器或其他实体来传递状态。您可以通过一些
    DateEMailChangedSent
    客户
    上执行此操作,如果为空,则需要发送

    这些步骤是:

    • 开始交易
    • 获取事件流
    • 拨打电话更改客户的电子邮件,甚至添加到事件流中
    • 需要记录电子邮件发送(DateEMailChangedSent返回null)
    • 保存事件流(1)
    • 发送
      sendmailchangedCommand
      消息(2)
    • 提交事务(3)
    有两种方法可以完成消息发送部分,它们可能会包含在同一事务中(不是2PC),但现在我们忽略这一点

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

    (1) 如果我们无法保存事件流,那么这里没有问题,因为异常将回滚事务,处理将再次发生。
    (2) 如果由于某些消息传递失败而无法发送消息,那么就没有问题了,因为回滚会将所有内容设置回开始之前的状态。 (3) 好吧,我们已经发送了消息,所以提交时的异常可能看起来像是一个错误
    public virtual void ChangeEmail(string email, EventFactory factory)
    {
        if(this.Email != email)
        {
            this.Email = email;
            UncommitedEvents.Add(factory.createCustomerChangedEmail(email));
        }
    }
    
    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;
    }