在保留聚合之前发布域事件是否安全? [英] Is it safe to publish Domain Event before persisting the Aggregate?

查看:53
本文介绍了在保留聚合之前发布域事件是否安全?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

In many different projects I have seen 2 different approaches of raising Domain Events.


  1. 直接从聚合中引发域事件。例如,假设您有客户聚合,并且其中有一个方法:

  1. Raise Domain Event directly from aggregate. For example imagine you have Customer aggregate and here is a method inside it:

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

我看到这种方法有两个问题。第一个是无论聚集是否持久,都会引发该事件。想象一下,如果您想在成功注册后向客户发送电子邮件。即使未保存汇总,也会引发事件 CustomerChangedEmail,并且某些IEmailSender将发送电子邮件。当前实现的第二个问题是每个事件都应该是不可变的。因此,问题是如何初始化其 OccuredOn属性?只有内部聚集!它合乎逻辑,对!它迫使我将ISystemClock(系统时间抽象)传递给聚合中的每个方法!哇???您不觉得这个设计脆弱又累赘吗?这是我们要想到的:

I can see 2 problems with this approach. The first one is that the event is raised regardless of whether the aggregate is persisted or not. Imagine if you want to send an email to a customer after successful registration. An event "CustomerChangedEmail" will be raised and some IEmailSender will send the email even if the aggregate wasn't saved. The second problem with the current implementation is that every event should be immutable. So the question is how can I initialize its "OccuredOn" property? Only inside aggregate! Its logical, right! It forces me to pass ISystemClock (system time abstraction) to each and every method on aggregate! Whaaat??? Don't you find this design brittle and cumbersome? Here is what we'll come up with:

public virtual void ChangeEmail(string email, ISystemClock systemClock)
{
    if(this.Email != email)
    {
        this.Email = email;
        DomainEvents.Raise<CustomerChangedEmail>(new CustomerChangedEmail(email, systemClock.DateTimeNow));
    }
}


  • 第二种方法是进行事件建议采用采购模式。在每个聚合上,我们定义一个未提交事件的(列表)列表。请注意UncommitedEvent不是域事件!它甚至没有OccuredOn属性。现在,当在Customer Aggregate上调用ChangeEmail方法时,我们不会进行任何操作。我们只是将事件保存到聚合中存在的uncommitedEvents集合中。像这样:

  • The second approach is to go what Event Sourcing pattern recommends to do. On each and every aggregate, we define a (List) list of uncommited events. Please payAttention that UncommitedEvent is not a domain Event! It doesn't even has OccuredOn property. Now, when ChangeEmail method is called on Customer Aggregate, we don't raise anything. We just save the event to uncommitedEvents collection which exists on our aggregate. Like this:

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


  • 那么,什么时候会引发实际的域事件???该责任委托给持久层。在ICustomerRepository中,我们可以访问ISystemClock,因为我们可以轻松地将其注入到存储库中。在ICustomerRepository的Save()方法内部,我们应该从Aggregate中提取所有uncommitedEvent,并为它们中的每个创建DomainEvent。然后,我们在新创建的域事件上设置OccuredOn属性。然后,在一次交易中,我们保存汇总并发布所有域事件。这样,我们可以确保所有事件都将在跨国界内以持久性持续增长。

    我对这种方法不满意吗?我不想为同一事件创建2种不同的类型,即对于CustomerChangedEmail行为,我应该具有CustomerChangedEmailUncommited类型和CustomerChangedEmailDomainEvent。只有一种类型会很好。请分享您对该主题的经验!

    So, when does the actual domain event is raised??? This responsibility is delegated to persistence layer. In ICustomerRepository we have access to ISystemClock, because we can easily inject it inside repository. Inside Save() method of ICustomerRepository we should extract all uncommitedEvents from Aggregate and for each of them create a DomainEvent. Then we set up OccuredOn property on newly created Domain Event. Then, IN ONE TRANSACTION we save the aggregate and publish ALL domain events. This way we'll be sure that all events will will raised in transnational boundary with aggregate persistence.
    What I don't like about this approach? I don't want to create 2 different types for the same event, i.e for CustomerChangedEmail behavior I should have CustomerChangedEmailUncommited type and CustomerChangedEmailDomainEvent. It would be nice to have just one type. Please share your experience regarding to this topic!

    推荐答案

    我不支持您提出的两种技术:)

    I am not a proponent of either of the two techniques you present :)

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

    Nowadays I favour returning an event or response object from the domain:

    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时相同。

    In this way I don't need to keep track of my uncommitted events and I don't rely on a global infrastructure class such as DomainEvents. The application layer controls transactions and persistence in the same way it would without ES.

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

    As for coordinating the publishing/saving: usually another layer of indirection helps. I must mention that I regard ES events as different from system events. System events being those between bounded contexts. A messaging infrastructure would rely on system events as these would usually convey more information than a domain event.

    通常,在协调诸如发送电子邮件之类的事情时,会使用流程管理者或某个其他实体的状态。您可以在 Customer 上携带一些 DateEMailChangedSent ,如果为空则需要发送。

    Usually when coordinating things such as sending of e-mails one would make use of a process manager or some other entity to carry state. You could carry this on your Customer with some DateEMailChangedSent and if null then sending is required.

    步骤如下:


    • 开始交易

    • 获取事件流

    • 进行呼叫以更改客户的电子邮件,甚至将其添加到事件流中

    • 记录所需的电子邮件发送(DateEMailChangedSent返回为空)

    • 保存事件流(1)

    • 发送 SendEMailChangedCommand 消息(2)

    • 提交事务(3)

    • Begin Transaction
    • Get event stream
    • Make call to change e-mail on customer, adding even to event stream
    • record e-mail sending required (DateEMailChangedSent back to null)
    • Save event stream (1)
    • Send a SendEMailChangedCommand message (2)
    • Commit transaction (3)

    有两种方法可以使消息发送部分可能将其包含在同一笔交易中(没有2PC),但现在暂时忽略它。

    There are a couple of ways to do that message sending part that may include it in the same transaction (no 2PC) but let's ignore that for now.

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

    Assuming that previously we had sent an e-mail our DateEMailChangedSent has a value before we start we may run into the following exceptions:

    (1)如果不能保存事件流,那么自从这没问题

    (2)如果由于某些消息传递失败而无法发送消息,则没有问题,因为回滚会将所有内容恢复为开始之前的状态。
    (3)好吧,我们已经发送了消息,因此提交异常可能看起来像是一个问题,但是请记住,我们无法将 DateEMailChangedSent 设置回 null 表示我们需要发送一封新电子邮件。

    (1) If we cannot save the event stream then here's no problem since the exception will rollback the transaction and the processing would occur again.
    (2) If we cannot send the message due to some messaging failure then there's no problem since the rollback will set everything back to before we started. (3) Well, we've sent our message so an exception on commit may seem like an issue but remember that we could not set our DateEMailChangedSent back to null to indicate that we require a new e-mail to be sent.

    <$ c的消息处理程序$ c> SendEMailChangedCommand 会检查 DateEMailChangedSent ,如果不是 null ,它将简单地返回,确认消息,消息消失。但是,如果为null,则它将通过与某些基础结构服务终结点之间的消息传递来直接与电子邮件网关进行交互的邮件(我更愿意)。

    The message handler for the SendEMailChangedCommand would check the DateEMailChangedSent and if not null it would simply return, acknowledging the message and it disappears. However, if it is null then it would send the mail either interacting with the e-mail gateway directly ot making use of some infrastructure service endpoint through messaging (I'd prefer that).

    好吧,这还是 my :)

    这篇关于在保留聚合之前发布域事件是否安全?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆