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

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

问题描述

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

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

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

  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));
    }
}

我发现这种方法存在 2 个问题.第一个是无论聚合是否持久都会引发事件.想象一下,如果您想在成功注册后向客户发送电子邮件.将引发事件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 中提取所有未提交的事件,并为每个事件创建一个域事件.然后我们在新创建的领域事件上设置 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.

    通常在协调诸如发送电子邮件之类的事情时,人们会使用进程管理器或其他一些实体来承载状态.您可以使用一些 DateEMailChangedSent 在您的 Customer 上进行此操作,如果为 null,则需要发送.

    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 回null)
    • 保存事件流 (1)
    • 发送 SendEMailChangedCommand 消息 (2)
    • 提交事务 (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.

    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).

    好吧,无论如何,这都是我的 :)

    Well, that's my take on it anyway :)

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

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