领域事件:设计和实现
Use domain events to explicitly implement side effects of changes within your domain. In other words, and using DDD terminology, use domain events to explicitly implement side effects across multiple aggregates. Optionally, for better scalability and less impact in database locks, use eventual consistency between aggregates within the same domain.
使用领域事件来显式地实现领域内变更所带来的次生影响(衍生行为)。换句话说,用 DDD 的术语来说,就是使用领域事件来显式地实现跨越多个聚合的次生影响(衍生行为)。另外,为了获得更好的可扩展性并减少对数据库锁的影响,你还可以选择在同一个领域的多个聚合之间使用最终一致性。
What is a domain event? 什么是领域事件?
An event is something that has happened in the past. A domain event is, something that happened in the domain that you want other parts of the same domain (in-process) to be aware of. The notified parts usually react somehow to the events.
事件就是过去发生的某件事。领域事件,则是指在你所在的领域中发生、并且你希望同一领域(进程内)的其他部分能够感知到的事情。被通知的部分通常会对这些事件做出某种反应。
An important benefit of domain events is that side effects can be expressed explicitly.
领域事件的一个重要好处是,它可以显式地表达次生影响(衍生行为)。
For example, if you're just using Entity Framework and there has to be a reaction to some event, you would probably code whatever you need close to what triggers the event. So the rule gets coupled, implicitly, to the code, and you have to look into the code to, hopefully, realize the rule is implemented there.
举个例子,如果你只是使用 Entity Framework,并且必须对某个事件做出反应,你很可能会把需要的代码写在触发该事件的地方附近。这样一来,这条业务规则就隐式地与代码耦合在了一起,你必须深入代码中仔细查看,才能(希望)意识到这条规则是在那里被实现的。
On the other hand, using domain events makes the concept explicit, because there's a DomainEvent and at least one DomainEventHandler involved.
而另一方面,使用领域事件可以让这个概念变得非常明确,因为这里会有一个 DomainEvent(领域事件)以及至少一个参与其中的 DomainEventHandler(领域事件处理程序)。
For example, in the eShop application, when an order is created, the user becomes a buyer, so an OrderStartedDomainEvent is raised and handled in the ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler, so the underlying concept is evident.
例如,在 eShop 应用程序中,当一个订单被创建时,该用户就变成了一名买家。因此,系统会触发一个 OrderStartedDomainEvent,并由 ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler 进行处理,这样一来,底层的业务概念就变得清晰可见了。
In short, domain events help you to express, explicitly, the domain rules, based in the ubiquitous language provided by the domain experts. Domain events also enable a better separation of concerns among classes within the same domain.
简而言之,领域事件帮助你基于领域专家提供的通用语言,显式地表达领域规则。领域事件还能让同一个领域内的各个类之间实现更好的关注点分离。
It's important to ensure that, just like a database transaction, either all the operations related to a domain event finish successfully or none of them do.
需要特别注意的是,就像数据库事务一样,你必须确保与某个领域事件相关的所有操作要么全部成功完成,要么一个都不执行。
Domain events are similar to messaging-style events, with one important difference. With real messaging, message queuing, message brokers, or a service bus using AMQP, a message is always sent asynchronously and communicated across processes and machines. This is useful for integrating multiple Bounded Contexts, microservices, or even different applications. However, with domain events, you want to raise an event from the domain operation you're currently running, but you want any side effects to occur within the same domain.
领域事件与消息传递风格的事件类似,但有一个重要的区别。在真正的消息传递中(例如使用 AMQP 协议的消息队列、消息代理或服务总线),消息总是被异步发送,并且是在不同的进程和机器之间进行通信的。这对于集成多个限界上下文、微服务,甚至是不同的应用程序非常有用。但是,在使用领域事件时,你是想从当前正在运行的领域操作中触发一个事件,但你希望所有的次生影响(衍生行为)都发生在同一个领域内部。
The domain events and their side effects (the actions triggered afterwards that are managed by event handlers) should occur almost immediately, usually in-process, and within the same domain. Thus, domain events could be synchronous or asynchronous. Integration events, however, should always be asynchronous.
领域事件及其次生影响(衍生行为)(即由事件处理程序触发的后续操作)应该几乎立即发生,通常是在进程内,并且处于同一个领域之中。因此,领域事件可以是同步的,也可以是异步的。不过,集成事件(Integration events)则必须始终是异步的。
Domain events versus integration events 领域事件与集成事件
Semantically, domain and integration events are the same thing: notifications about something that just happened. However, their implementation must be different. Domain events are just messages pushed to a domain event dispatcher, which could be implemented as an in-memory mediator based on an IoC container or any other method.
从语义上来说,领域事件和集成事件是同一回事:它们都是关于刚刚发生的某件事的通知。不过,它们的实现方式必须有所不同。领域事件只是被推送到领域事件调度器(Domain Event Dispatcher)的消息,这种调度器可以基于 IoC 容器实现为内存中的中介者(Mediator),也可以使用任何其他方法。
On the other hand, the purpose of integration events is to propagate committed transactions and updates to additional subsystems, whether they are other microservices, Bounded Contexts or even external applications. Hence, they should occur only if the entity is successfully persisted, otherwise it's as if the entire operation never happened.
另一方面,集成事件的目的是将已提交的事务和更新传播到其他的子系统,无论这些子系统是其他的微服务、限界上下文(Bounded Contexts),甚至是外部应用程序。因此,集成事件只有在实体被成功持久化之后才应该发生,否则就好像整个操作从未发生过一样。
As mentioned before, integration events must be based on asynchronous communication between multiple microservices (other Bounded Contexts) or even external systems/applications.
正如前面提到的,集成事件必须基于多个微服务(或其他限界上下文)甚至外部系统/应用程序之间的异步通信。
Thus, the event bus interface needs some infrastructure that allows inter-process and distributed communication between potentially remote services. It can be based on a commercial service bus, queues, a shared database used as a mailbox, or any other distributed and ideally push based messaging system.
因此,事件总线(Event Bus)接口需要某种基础设施,来支持潜在远程服务之间的进程间通信和分布式通信。它可以基于商业服务总线、消息队列、用作邮箱的共享数据库,或者任何其他分布式的、且理想情况下是基于推送(push based)的消息系统。
Domain events as a preferred way to trigger side effects across multiple aggregates within the same domain 领域事件是在同一领域内跨多个聚合触发次生影响(衍生行为)的首选方式
If executing a command related to one aggregate instance requires additional domain rules to be run on one or more additional aggregates, you should design and implement those side effects to be triggered by domain events. As shown in Figure 7-14, and as one of the most important use cases, a domain event should be used to propagate state changes across multiple aggregates within the same domain model.
如果执行与某个聚合实例相关的命令时,还需要在一个或多个额外的聚合上运行其他的领域规则,你应该设计并实现由领域事件来触发这些次生影响(衍生行为)。如图 7-14 所示,作为最重要的用例之一,领域事件应该被用来在同一领域模型内的多个聚合之间传播状态变更。

Figure 7-14. Domain events to enforce consistency between multiple aggregates within the same domain
图 7-14. 使用领域事件来强制维护同一领域内多个聚合之间的一致性
Figure 7-14 shows how consistency between aggregates is achieved by domain events. When the user initiates an order, the Order Aggregate sends an OrderStarted domain event. The OrderStarted domain event is handled by the Buyer Aggregate to create a Buyer object in the ordering microservice, based on the original user info from the identity microservice (with information provided in the CreateOrder command).
图 7-14 展示了如何通过领域事件来实现聚合之间的一致性。当用户发起一个订单时,订单聚合(Order Aggregate)会发送一个 OrderStarted 领域事件。该 OrderStarted 领域事件会被买家聚合(Buyer Aggregate)的事件处理程序捕获,进而根据身份微服务提供的原始用户信息(包含在 CreateOrder 命令中),在订单微服务里创建一个买家对象。
Alternately, you can have the aggregate root subscribed for events raised by members of its aggregates (child entities). For instance, each OrderItem child entity can raise an event when the item price is higher than a specific amount, or when the product item amount is too high. The aggregate root can then receive those events and perform a global calculation or aggregation.
另外,你也可以让聚合根订阅其聚合内部成员(子实体)所触发的事件。例如,当商品单价高于特定金额,或者商品数量过多时,每一个订单项(OrderItem)子实体都可以触发一个事件。随后,聚合根可以接收这些事件,并执行全局的计算或汇总。
It's important to understand that this event-based communication is not implemented directly within the aggregates; you need to implement domain event handlers.
需要重点理解的是,这种基于事件的通信并不是在聚合内部直接实现的;你需要专门去实现领域事件处理程序(Domain Event Handlers)。
Handling the domain events is an application concern. The domain model layer should only focus on the domain logic—things that a domain expert would understand, not application infrastructure like handlers and side-effect persistence actions using repositories. Therefore, the application layer level is where you should have domain event handlers triggering actions when a domain event is raised.
处理领域事件属于应用层的关注点。领域模型层应该只专注于领域逻辑——也就是领域专家能够理解的那些业务概念,而不是像处理程序(handlers)或使用仓储(repositories)进行次生影响(衍生行为)持久化这类应用层的基础设施。因此,你应该在应用层编写领域事件处理程序,以便在领域事件被触发时去执行相应的动作。
Domain events can also be used to trigger any number of application actions, and what is more important, must be open to increase that number in the future in a decoupled way. For instance, when the order is started, you might want to publish a domain event to propagate that info to other aggregates or even to raise application actions like notifications.
领域事件还可以被用来触发任意数量的应用程序动作,而且更重要的是,它必须允许在未来以一种解耦的方式轻松增加这些动作的数量。例如,当订单开始时,你可能想要发布一个领域事件,将这一信息传播给其他的聚合,甚至触发像发送通知这样的应用层动作。
The key point is the open number of actions to be executed when a domain event occurs. Eventually, the actions and rules in the domain and application will grow. The complexity or number of side-effect actions when something happens will grow, but if your code were coupled with "glue" (that is, creating specific objects with new), then every time you needed to add a new action you would also need to change working and tested code.
这里的关键点在于,当一个领域事件发生时,需要执行的动作数量是开放的。最终,领域和应用中的动作与规则会越来越多。当某件事发生时,其次生影响(衍生行为)动作的复杂性或数量也会随之增长。但如果你的代码是通过“胶水代码”(也就是使用 new 关键字去创建具体的对象)紧密耦合的,那么每当你需要增加一个新动作时,就不得不去修改那些原本已经能正常工作且经过测试的代码。
This change could result in new bugs and this approach also goes against the Open/Closed principle from SOLID. Not only that, the original class that was orchestrating the operations would grow and grow, which goes against the Single Responsibility Principle (SRP).
这种修改不仅可能带来新的 Bug,而且这种做法也违背了 SOLID 原则中的开闭原则(Open/Closed Principle)。不仅如此,原本负责协调各项操作的那个类也会变得越来越臃肿,这也违背了单一职责原则(SRP)。
On the other hand, if you use domain events, you can create a fine-grained and decoupled implementation by segregating responsibilities using this approach:
另一方面,如果你使用领域事件,就可以通过这种方式分离职责,从而实现细粒度且解耦的代码:
-
Send a command (for example, CreateOrder).
发送一个命令(例如 CreateOrder)。
-
Receive the command in a command handler.
在命令处理程序(Command Handler)中接收该命令。
-
Execute a single aggregate's transaction.
执行单个聚合的事务。
-
(Optional) Raise domain events for side effects (for example, OrderStartedDomainEvent).
(可选)为次生影响(衍生行为)触发领域事件(例如 OrderStartedDomainEvent)。
-
-
Handle domain events (within the current process) that will execute an open number of side effects in multiple aggregates or application actions. For example:
(在当前进程内)处理领域事件,这将会在多个聚合或应用层动作中执行开放数量的次生影响(衍生行为)。例如:
-
Verify or create buyer and payment method.
验证或创建买家及支付方式。
-
Create and send a related integration event to the event bus to propagate states across microservices or trigger external actions like sending an email to the buyer.
创建并向事件总线(Event Bus)发送一个相关的集成事件,以便在微服务之间传播状态,或触发像给买家发送邮件这样的外部动作。
-
Handle other side effects.
处理其他的次生影响(衍生行为)。
-
As shown in Figure 7-15, starting from the same domain event, you can handle multiple actions related to other aggregates in the domain or additional application actions you need to perform across microservices connecting with integration events and the event bus.
如图 7-15 所示,从同一个领域事件出发,你可以处理与领域内其他聚合相关的多个动作,或者通过集成事件和事件总线连接微服务,去执行跨微服务的额外应用层动作。

Figure 7-15. Handling multiple actions per domain
图 7-15. 每个领域事件处理多个动作
There can be several handlers for the same domain event in the Application Layer, one handler can solve consistency between aggregates and another handler can publish an integration event, so other microservices can do something with it. The event handlers are typically in the application layer, because you'll use infrastructure objects like repositories or an application API for the microservice's behavior. In that sense, event handlers are similar to command handlers, so both are part of the application layer. The important difference is that a command should be processed only once. A domain event could be processed zero or n times, because it can be received by multiple receivers or event handlers with a different purpose for each handler.
在应用层,同一个领域事件可以拥有多个处理程序(Handlers)。其中一个处理程序可以用来解决聚合之间的一致性问题,而另一个处理程序则可以发布一个集成事件,以便其他微服务能够对此进行相应的处理。事件处理程序通常位于应用层,因为你会用到仓储(repositories)或微服务行为所需的应用程序 API 等基础设施对象。从这个角度来看,事件处理程序和命令处理程序(Command Handlers)非常相似,它们都属于应用层的一部分。但两者有一个重要的区别:一个命令(Command)应该只被处理一次,而一个领域事件可以被处理零次或 n 次,因为它可以被多个接收者或事件处理程序接收,且每个处理程序都有着不同的目的。
Having an open number of handlers per domain event allows you to add as many domain rules as needed, without affecting current code. For instance, implementing the following business rule might be as easy as adding a few event handlers (or even just one):
为每个领域事件配置开放数量的处理程序,可以让你在不影响现有代码的情况下,按需添加任意数量的领域规则。例如,要实现下面这条业务规则,可能只需要简单地添加几个事件处理程序(甚至只需要一个):
When the total amount purchased by a customer in the store, across any number of orders, exceeds $6,000, apply a 10% off discount to every new order and notify the customer with an email about that discount for future orders.
当某个客户在店内的累计消费总额(无论下了多少单)超过 6000 美元时,为其之后的每一笔新订单自动应用 10% 的折扣,并通过电子邮件通知客户该折扣信息,以便其未来下单使用。
Implement domain events 实现领域事件
In C#, a domain event is simply a data-holding structure or class, like a DTO, with all the information related to what just happened in the domain, as shown in the following example:
在 C# 中,领域事件仅仅是一个用于存放数据的结构体或类,就像一个数据传输对象(DTO)一样,它包含了与领域内刚刚发生的事件相关的所有信息,如下面的示例所示:
/// <summary>
/// 订单已启动的领域事件(Domain Event)。
/// 当一个新的订单被创建并启动时触发此事件,通常用于解耦核心订单逻辑与后续的业务衍生行为。
/// </summary>
public class OrderStartedDomainEvent : INotification // 实现 MediatR 的 INotification 接口,使其能够作为通知被分发和处理
{
/// <summary>
/// 下单用户的唯一标识符。
/// </summary>
public string UserId { get; } // 只读属性,确保事件数据在发布后不可变
/// <summary>
/// 下单用户的名称。
/// </summary>
public string UserName { get; }
/// <summary>
/// 支付卡类型的 ID(如 Visa, MasterCard 等)。
/// </summary>
public int CardTypeId { get; }
/// <summary>
/// 支付卡号。
/// </summary>
public string CardNumber { get; }
/// <summary>
/// 支付卡的安全码(CVV)。
/// </summary>
public string CardSecurityNumber { get; }
/// <summary>
/// 持卡人姓名。
/// </summary>
public string CardHolderName { get; }
/// <summary>
/// 支付卡的过期时间。
/// </summary>
public DateTime CardExpiration { get; }
/// <summary>
/// 触发该事件的原始订单对象引用。
/// </summary>
public Order Order { get; }
/// <summary>
/// 初始化一个新的 <see cref="OrderStartedDomainEvent"/> 实例。
/// </summary>
/// <param name="order">触发事件的原始订单实体。</param>
/// <param name="userId">用户唯一标识符。</param>
/// <param name="userName">用户名。</param>
/// <param name="cardTypeId">支付卡类型 ID。</param>
/// <param name="cardNumber">支付卡号。</param>
/// <param name="cardSecurityNumber">支付卡安全码。</param>
/// <param name="cardHolderName">持卡人姓名。</param>
/// <param name="cardExpiration">卡片过期时间。</param>
public OrderStartedDomainEvent(Order order, string userId, string userName,
int cardTypeId, string cardNumber,
string cardSecurityNumber, string cardHolderName,
DateTime cardExpiration)
{
Order = order; // 关联引发该事件的订单聚合根
UserId = userId; // 提取并保存用户标识,方便下游处理程序直接使用,无需再次查询
UserName = userName; // 保存用户名信息
CardTypeId = cardTypeId; // 记录所选的支付方式类型
CardNumber = cardNumber; // 记录卡号信息
CardSecurityNumber = cardSecurityNumber; // 记录安全码信息
CardHolderName = cardHolderName; // 记录持卡人姓名
CardExpiration = cardExpiration; // 记录卡片过期日期
}
}
This is essentially a class that holds all the data related to the OrderStarted event.
本质上,这就是一个包含了所有与 OrderStarted(订单已创建)事件相关数据的类。
In terms of the ubiquitous language of the domain, since an event is something that happened in the past, the class name of the event should be represented as a past-tense verb, like OrderStartedDomainEvent or OrderShippedDomainEvent. That's how the domain event is implemented in the ordering microservice in eShop.
从领域的通用语言(Ubiquitous Language)角度来看,既然事件是过去发生的某件事,那么这个事件类的名称就应该用动词的过去式来表示,比如 OrderStartedDomainEvent 或者 OrderShippedDomainEvent。这也是 eShop 项目的订单微服务中实现领域事件的方式。
As noted earlier, an important characteristic of events is that since an event is something that happened in the past, it shouldn't change. Therefore, it must be an immutable class. You can see in the previous code that the properties are read-only. There's no way to update the object, you can only set values when you create it.
正如前面提到的,事件的一个重要特性是:既然事件是过去发生的,它就不应该被改变。因此,它必须是一个不可变(immutable)的类。你可以从上面的代码中看到,所有的属性都是只读的。没有任何方法可以更新这个对象,你只能在创建它的时候设置值。
It's important to highlight here that if domain events were to be handled asynchronously, using a queue that required serializing and deserializing the event objects, the properties would have to be "private set" instead of read-only, so the deserializer would be able to assign the values upon dequeuing. This is not an issue in the Ordering microservice, as the domain event pub/sub is implemented synchronously using MediatR.
这里需要特别强调一点:如果领域事件需要使用消息队列进行异步处理,而消息队列又要求对事件对象进行序列化和反序列化,那么这些属性就必须是 private set(私有设置),而不能是完全只读,这样反序列化器才能在出队时给属性赋值。不过这在订单微服务中不是问题,因为它的领域事件发布/订阅是使用 MediatR 同步实现的。
Raise domain events 触发领域事件
The next question is how to raise a domain event so it reaches its related event handlers. You can use multiple approaches.
接下来的问题就是:如何触发领域事件,以便让它能够送达相关的事件处理程序?你可以采用多种方法。
Udi Dahan originally proposed (for example, in several related posts, such as Domain Events – Take 2) using a static class for managing and raising the events. This might include a static class named DomainEvents that would raise domain events immediately when it's called, using syntax like DomainEvents.Raise(Event myEvent). Jimmy Bogard wrote a blog post (Strengthening your domain: Domain Events) that recommends a similar approach.
Udi Dahan 最初提议(例如在多篇相关文章中,如《领域事件 - 第二版》)使用一个静态类来管理和触发事件。这可能包括一个名为 DomainEvents 的静态类,当被调用时,它会使用类似 DomainEvents.Raise(Event myEvent) 的语法立即触发领域事件。Jimmy Bogard 也写过一篇博客文章(《强化你的领域:领域事件》),推荐了类似的方法。
However, when the domain events class is static, it also dispatches to handlers immediately. This makes testing and debugging more difficult, because the event handlers with side-effects logic are executed immediately after the event is raised. When you're testing and debugging, you just want to focus on what is happening in the current aggregate classes; you don't want to suddenly be redirected to other event handlers for side effects related to other aggregates or application logic. This is why other approaches have evolved, as explained in the next section.
不过,当这个领域事件类是静态的时候,它也会立即将事件分发给处理程序。这让测试和调试变得更加困难,因为带有次生影响(衍生行为)逻辑的事件处理程序会在事件被触发后立刻执行。当你进行测试和调试时,你只想关注当前聚合类中正在发生的事情;你不希望突然被跳转到其他事件处理程序,去处理与其他聚合或应用逻辑相关的次生影响(衍生行为)。正因如此,才演变出了其他的方法,我们将在下一节进行讲解。
The deferred approach to raise and dispatch events 延迟触发和分发事件的方法
Instead of dispatching to a domain event handler immediately, a better approach is to add the domain events to a collection and then to dispatch those domain events right before or right after committing the transaction (as with SaveChanges in EF). (This approach was described by Jimmy Bogard in this post A better domain events pattern.)
与其立即将事件分发给领域事件处理程序,更好的做法是先把领域事件添加到一个集合中,然后在提交事务之前或之后(就像在 EF 中使用 SaveChanges 那样)再去分发这些领域事件。(Jimmy Bogard 在这篇名为《一个更好的领域事件模式》的文章中描述了这种方法。)
Deciding if you send the domain events right before or right after committing the transaction is important, since it determines whether you will include the side effects as part of the same transaction or in different transactions. In the latter case, you need to deal with eventual consistency across multiple aggregates. This topic is discussed in the next section.
决定是在提交事务之前还是之后发送领域事件非常重要,因为它决定了你是将次生影响(衍生行为)包含在同一个事务中,还是放在不同的事务里。在后一种情况下,你需要处理跨多个聚合的最终一致性。这个话题将在下一节讨论。
The deferred approach is what eShop uses. First, you add the events happening in your entities into a collection or list of events per entity. That list should be part of the entity object, or even better, part of your base entity class, as shown in the following example of the Entity base class:
延迟方法正是 eShop 所采用的方式。首先,你把实体中发生的事件添加到一个集合或每个实体的事件列表中。这个列表应该是实体对象的一部分,或者更好的是,作为你的基础实体类的一部分,如下面 Entity 基类的示例所示:
/// <summary>
/// 实体抽象基类,为所有领域实体提供统一的标识、相等性比较以及领域事件管理能力。
/// </summary>
public abstract class Entity
{
// ...其他基础属性或方法...
/// <summary>
/// 领域事件的内部集合,用于暂存在当前聚合根生命周期内触发的业务事件。
/// 采用延迟初始化策略,避免在不需要事件时分配内存。
/// </summary>
private List<INotification> _domainEvents;
/// <summary>
/// 获取当前实体已收集的领域事件列表。
/// 通常由基础设施层在保存数据前读取并分发这些事件。
/// </summary>
public List<INotification> DomainEvents => _domainEvents;
/// <summary>
/// 向当前实体添加一个新的领域事件。
/// 当实体状态发生改变且需要通知其他模块时调用此方法。
/// </summary>
/// <param name="eventItem">要添加的领域事件对象。</param>
public void AddDomainEvent(INotification eventItem)
{
// 如果事件集合尚未初始化,则创建一个新列表(延迟初始化)
_domainEvents = _domainEvents ?? new List<INotification>();
// 将新的领域事件加入集合中
_domainEvents.Add(eventItem);
}
/// <summary>
/// 从当前实体中移除一个指定的领域事件。
/// 通常在领域事件已被成功处理或需要撤销操作时使用。
/// </summary>
/// <param name="eventItem">要移除的领域事件对象。</param>
public void RemoveDomainEvent(INotification eventItem)
{
// 使用空条件运算符安全地移除事件,防止集合为空时抛出异常
_domainEvents?.Remove(eventItem);
}
// ...其他基础代码...
}
When you want to raise an event, you just add it to the event collection from code at any method of the aggregate-root entity.
当你想要触发一个事件时,只需要在聚合根实体的任意方法中,通过代码把它添加到这个事件集合里就可以了。
The following code, part of the Order aggregate-root at eShop, shows an example:
下面这段代码(选自 eShop 项目中的 Order 聚合根)展示了一个具体的例子:
// 实例化“订单已开始”领域事件,将当前聚合根(this)及相关的支付信息作为事件数据传入
var orderStartedDomainEvent = new OrderStartedDomainEvent(
this, // 传递当前 Order 聚合根实例,以便事件处理器能追溯事件来源
cardTypeId, // 支付卡类型 ID
cardNumber, // 银行卡号
cardSecurityNumber, // 安全码(CVV)
cardHolderName, // 持卡人姓名
cardExpiration // 卡片过期时间
);
// 将该领域事件注册到当前聚合根的待发布事件集合中
// 注意:此时事件尚未真正分发,通常会在当前工作单元(UOW)提交/保存数据库时由基础设施层统一触发
this.AddDomainEvent(orderStartedDomainEvent);
Notice that the only thing that the AddDomainEvent method is doing is adding an event to the list. No event is dispatched yet, and no event handler is invoked yet.
请注意,AddDomainEvent 方法唯一做的事情就是把一个事件添加到列表中。此时,还没有任何事件被分发出去,也没有任何事件处理程序被调用。
You actually want to dispatch the events later on, when you commit the transaction to the database. If you are using Entity Framework Core, that means in the SaveChanges method of your EF DbContext, as in the following code:
你实际上是想在稍后向数据库提交事务时,再去分发这些事件。如果你使用的是 Entity Framework Core,那就意味着要在你的 EF DbContext 的 SaveChanges 方法中进行分发,就像下面的代码一样:
/// <summary>
/// 订单上下文(Ordering Context),作为 EF Core 的数据库上下文及工作单元(IUnitOfWork)的实现。
/// </summary>
public class OrderingContext : DbContext, IUnitOfWork
{
// ...其他实体配置与 DbSets...
/// <summary>
/// 保存实体更改并处理领域事件的核心方法。
/// 该方法会在持久化数据之前先触发所有待处理的领域事件,确保业务逻辑的一致性。
/// </summary>
/// <param name="cancellationToken">用于取消异步操作的令牌。</param>
/// <returns>如果成功将更改保存到数据库则返回 true;否则返回 false。</returns>
public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default(CancellationToken))
{
// 分发领域事件集合。
// 关于执行时机的选择说明:
// A) 在将数据提交到数据库 (EF SaveChanges) 【之前】立即执行。
// 这种方式会将领域事件处理程序产生的次生影响包含在同一个事务中(前提是它们使用具有 Scope 生命周期的同一个 DbContext)。
// B) 在将数据提交到数据库 (EF SaveChanges) 【之后】立即执行。
// 这种方式会产生多个独立的事务。开发者需要自行处理最终一致性,并在发生失败时实现补偿操作。
await _mediator.DispatchDomainEventsAsync(this);
// 在此行代码执行后,所有通过 DbContext 进行的更改(包括来自命令处理程序和领域事件处理程序的修改)将被统一提交到数据库
var result = await base.SaveChangesAsync();
return result > 0; // 补充返回值以确保方法签名完整
}
}
With this code, you dispatch the entity events to their respective event handlers.
通过这段代码,你就可以将实体事件分发给它们各自的事件处理程序了。
The overall result is that you've decoupled the raising of a domain event (a simple add into a list in memory) from dispatching it to an event handler. In addition, depending on what kind of dispatcher you are using, you could dispatch the events synchronously or asynchronously.
整体来看,你已经将领域事件的触发(仅仅是向内存中的列表添加一项)与将其分发给事件处理程序这两个动作解耦了。此外,根据你使用的分发器类型,你可以选择同步或异步地分发这些事件。
Be aware that transactional boundaries come into significant play here. If your unit of work and transaction can span more than one aggregate (as when using EF Core and a relational database), this can work well. But if the transaction cannot span aggregates, you have to implement additional steps to achieve consistency. This is another reason why persistence ignorance is not universal; it depends on the storage system you use.
需要注意的是,事务边界在这里起着至关重要的作用。如果你的工作单元和事务能够跨越多个聚合(就像使用 EF Core 和关系型数据库时那样),那么这种做法就能很好地工作。但如果事务无法跨越聚合,你就必须采取额外的步骤来实现一致性。这也是为什么“持久化无知(persistence ignorance)”并不是放之四海而皆准的;它取决于你使用的存储系统。
Single transaction across aggregates versus eventual consistency across aggregates 跨聚合的单一事务与跨聚合的最终一致性
The question of whether to perform a single transaction across aggregates versus relying on eventual consistency across those aggregates is a controversial one. Many DDD authors like Eric Evans and Vaughn Vernon advocate the rule that one transaction = one aggregate and therefore argue for eventual consistency across aggregates. For example, in his book Domain-Driven Design, Eric Evans says this:
究竟应该对多个聚合执行单一事务,还是依赖聚合之间的最终一致性?这是一个颇具争议的话题。许多 DDD 领域的作者,如 Eric Evans 和 Vaughn Vernon,都提倡“一个事务 = 一个聚合”的规则,因此主张在聚合之间采用最终一致性。例如,Eric Evans 在他的《领域驱动设计》一书中这样说道:
Any rule that spans Aggregates will not be expected to be up-to-date at all times. Through event processing, batch processing, or other update mechanisms, other dependencies can be resolved within some specific time. (page 128)
“任何跨越聚合的规则,都不必期望在任何时刻都是最新的。通过事件处理、批处理或其他更新机制,其他的依赖关系可以在某个特定的时间内得到解决。”(第 128 页)
Vaughn Vernon says the following in Effective Aggregate Design. Part II: Making Aggregates Work Together:
Vaughn Vernon 在他的《有效聚合设计:第二部分——让聚合协同工作》中则这样说道:
Thus, if executing a command on one aggregate instance requires that additional business rules execute on one or more aggregates, use eventual consistency [...] There is a practical way to support eventual consistency in a DDD model. An aggregate method publishes a domain event that is in time delivered to one or more asynchronous subscribers.
“因此,如果在一个聚合实例上执行命令时,需要在另外的一或多个聚合上执行额外的业务规则,那就使用最终一致性……在 DDD 模型中,有一种实际可行的方法来支持最终一致性。一个聚合方法会发布一个领域事件,该事件会在稍后被传递给一或多个异步订阅者。”
This rationale is based on embracing fine-grained transactions instead of transactions spanning many aggregates or entities. The idea is that in the second case, the number of database locks will be substantial in large-scale applications with high scalability needs. Embracing the fact that highly scalable applications need not have instant transactional consistency between multiple aggregates helps with accepting the concept of eventual consistency. Atomic changes are often not needed by the business, and it is in any case the responsibility of the domain experts to say whether particular operations need atomic transactions or not. If an operation always needs an atomic transaction between multiple aggregates, you might ask whether your aggregate should be larger or wasn't correctly designed.
这种理论依据是基于采用细粒度的事务,而不是跨越多个聚合或实体的大事务。其核心思想是,在后一种情况下,对于那些对高可扩展性有需求的大型应用来说,数据库锁的数量将会非常庞大。接受“高可扩展性应用不必在多个聚合之间拥有即时的跨事务一致性”这一事实,有助于我们接纳最终一致性的概念。原子性变更往往并不是业务所必需的,而且无论如何,特定操作是否需要原子性事务,应该由领域专家来判定。如果一个操作在多个聚合之间始终需要原子性事务,那你可能就要反思一下,你的聚合是不是应该设计得更大一些,或者是不是当初的设计就不正确。
However, other developers and architects like Jimmy Bogard are okay with spanning a single transaction across several aggregates—but only when those additional aggregates are related to side effects for the same original command. For instance, in A better domain events pattern, Bogard says this:
不过,也有其他开发者和架构师(比如 Jimmy Bogard)认为可以在多个聚合之间跨越单一事务——但前提是这些额外的聚合与同一个原始命令的次生影响(衍生行为)相关。例如,在《一个更好的领域事件模式》一文中,Bogard 这样说道:
Typically, I want the side effects of a domain event to occur within the same logical transaction, but not necessarily in the same scope of raising the domain event [...] Just before we commit our transaction, we dispatch our events to their respective handlers.
“通常情况下,我希望领域事件的次生影响(衍生行为)发生在同一个逻辑事务中,但不一定要和触发领域事件的作用域完全相同……就在我们提交事务之前,我们将事件分发给各自的处理程序。”
If you dispatch the domain events right before committing the original transaction, it is because you want the side effects of those events to be included in the same transaction. For example, if the EF DbContext SaveChanges method fails, the transaction will roll back all changes, including the result of any side effect operations implemented by the related domain event handlers. This is because the DbContext life scope is by default defined as "scoped." Therefore, the DbContext object is shared across multiple repository objects being instantiated within the same scope or object graph. This coincides with the HttpRequest scope when developing Web API or MVC apps.
如果你选择在提交原始事务之前分发领域事件,那是因为你希望这些事件的次生影响(衍生行为)被包含在同一个事务里。例如,如果 EF 的 DbContext.SaveChanges 方法失败了,事务将会回滚所有的变更,包括由相关领域事件处理程序实现的任何次生影响(衍生行为)操作的结果。这是因为 DbContext 的生命周期默认被定义为“作用域内(scoped)”。因此,在同一个作用域或对象图中实例化的多个仓储对象,会共享同一个 DbContext 对象。这在开发 Web API 或 MVC 应用时,恰好与 HttpRequest 的作用域相吻合。
Actually, both approaches (single atomic transaction and eventual consistency) can be right. It really depends on your domain or business requirements and what the domain experts tell you. It also depends on how scalable you need the service to be (more granular transactions have less impact with regard to database locks). And it depends on how much investment you're willing to make in your code, since eventual consistency requires more complex code in order to detect possible inconsistencies across aggregates and the need to implement compensatory actions. Consider that if you commit changes to the original aggregate and afterwards, when the events are being dispatched, if there's an issue and the event handlers cannot commit their side effects, you'll have inconsistencies between aggregates.
实际上,这两种方法(单一原子事务和最终一致性)都可以是正确的。这真的取决于你的领域或业务需求,以及领域专家给出的意见。这也取决于你的服务需要多高的可扩展性(更细粒度的事务对数据库锁的影响更小)。同时,这也取决于你愿意在代码上投入多少精力,因为最终一致性需要更复杂的代码,以便检测聚合之间可能出现的不一致,并需要实现补偿性操作。试想一下,如果你先提交了原始聚合的变更,然后在分发事件时,事件处理程序因为某些问题无法提交它们的次生影响(衍生行为)操作,那么你的聚合之间就会出现不一致。
A way to allow compensatory actions would be to store the domain events in additional database tables so they can be part of the original transaction. Afterwards, you could have a batch process that detects inconsistencies and runs compensatory actions by comparing the list of events with the current state of the aggregates. The compensatory actions are part of a complex topic that will require deep analysis from your side, which includes discussing it with the business user and domain experts.
一种允许执行补偿性操作的方法是,将领域事件存储在额外的数据库表中,使它们成为原始事务的一部分。之后,你可以运行一个批处理过程,通过比对事件列表与聚合的当前状态来检测不一致,并执行补偿性操作。补偿性操作是一个复杂的话题,需要你进行深入的分析,这包括与业务用户和领域专家进行讨论。
In any case, you can choose the approach you need. But the initial deferred approach—raising the events before committing, so you use a single transaction—is the simplest approach when using EF Core and a relational database. It's easier to implement and valid in many business cases. It's also the approach used in the ordering microservice in eShop.
无论如何,你可以选择适合你需求的方法。但最初的延迟方法——在提交之前触发事件,从而使用单一事务——是当你使用 EF Core 和关系型数据库时最简单的做法。它更容易实现,并且在许多业务场景中都是有效的。这也是 eShop 项目中订单微服务所采用的方法。
But how do you actually dispatch those events to their respective event handlers? What's the _mediator object you see in the previous example? It has to do with the techniques and artifacts you use to map between events and their event handlers.
那么,你实际上是如何将这些事件分发给它们各自的事件处理程序的呢?你在前面的示例中看到的那个 _mediator 对象到底是什么?这就涉及到你用来在事件和事件处理程序之间进行映射的技术和工件了。
The domain event dispatcher: mapping from events to event handlers 领域事件分发器:从事件到事件处理程序的映射
Once you're able to dispatch or publish the events, you need some kind of artifact that will publish the event, so that every related handler can get it and process side effects based on that event.
一旦你能够分发或发布事件,你就需要某种工件来发布这个事件,以便每一个相关的处理程序都能获取到它,并基于该事件处理次生影响(衍生行为)。
One approach is a real messaging system or even an event bus, possibly based on a service bus as opposed to in-memory events. However, for the first case, real messaging would be overkill for processing domain events, since you just need to process those events within the same process (that is, within the same domain and application layer).
一种做法是使用真正的消息系统甚至事件总线,可能是基于服务总线而不是基于内存事件的。不过,对于第一种情况,如果只是为了处理领域事件,使用真正的消息系统就有点杀鸡用牛刀了,因为你只需要在同一个进程内(也就是在同一个领域和应用层内)处理这些事件。
How to subscribe to domain events 如何订阅领域事件
When you use MediatR, each event handler must use an event type that is provided on the generic parameter of the INotificationHandler interface, as you can see in the following code:
当你使用 MediatR 时,每个事件处理程序都必须使用一个事件类型,该类型由 INotificationHandler 接口的泛型参数提供,就像你在下面的代码中看到的那样:
/// <summary>
/// 当订单启动时,用于验证或添加买家聚合根的域事件处理程序。
/// </summary>
public class ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler
: INotificationHandler<OrderStartedDomainEvent> // 实现 MediatR 的通知处理接口,订阅 OrderStartedDomainEvent 域事件
{
}
Based on the relationship between event and event handler, which can be considered the subscription, the MediatR artifact can discover all the event handlers for each event and trigger each one of those event handlers.
基于事件与事件处理程序之间的这种关系(可以把它看作是一种“订阅”关系),MediatR 这个组件能够自动发现每个事件对应的所有处理程序,并逐一触发它们。
How to handle domain events 如何处理领域事件
Finally, the event handler usually implements application layer code that uses infrastructure repositories to obtain the required additional aggregates and to execute side-effect domain logic. The following domain event handler code at eShop, shows an implementation example.
最后,事件处理程序通常会实现一些应用层的代码。这些代码会利用基础设施层的仓储(repositories)来获取所需的其他聚合,并执行带有次生影响(衍生行为)的领域逻辑。下面这段来自 eShop 项目的领域事件处理程序代码,就展示了一个具体的实现示例。
/// <summary>
/// “订单已开始”域事件的处理程序。
/// 负责在订单启动时验证买家聚合根,若买家不存在则自动创建并添加新的买家实体。
/// </summary>
public class ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler
: INotificationHandler<OrderStartedDomainEvent> // 实现 MediatR 的通知处理接口,订阅 OrderStartedDomainEvent 域事件
{
private readonly ILogger _logger; // 日志记录器实例
private readonly IBuyerRepository _buyerRepository; // 买家聚合根的仓储接口
private readonly IOrderingIntegrationEventService _orderingIntegrationEventService; // 用于发布集成事件的服务接口
/// <summary>
/// 初始化一个新的 <see cref="ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler"/> 实例。
/// </summary>
/// <param name="logger">用于记录日志的 ILogger 实例。</param>
/// <param name="buyerRepository">用于操作买家聚合根的仓储接口。</param>
/// <param name="orderingIntegrationEventService">用于发布集成事件的服务接口。</param>
public ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler(
ILogger<ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler> logger,
IBuyerRepository buyerRepository,
IOrderingIntegrationEventService orderingIntegrationEventService)
{
_buyerRepository = buyerRepository ?? throw new ArgumentNullException(nameof(buyerRepository)); // 注入买家仓储并进行空值校验
_orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService)); // 注入集成事件服务并进行空值校验
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); // 注入日志记录器并进行空值校验
}
/// <summary>
/// 处理“订单已开始”域事件的核心逻辑。
/// 根据事件中的用户信息验证或创建买家,更新其支付方式,并发布订单状态变更的集成事件。
/// </summary>
/// <param name="domainEvent">包含订单启动信息的域事件对象。</param>
/// <param name="cancellationToken">用于取消异步操作的令牌。</param>
/// <returns>表示异步处理任务的 Task。</returns>
public async Task Handle(
OrderStartedDomainEvent domainEvent, CancellationToken cancellationToken)
{
// 确定支付卡类型 ID,如果未指定则默认为 1
var cardTypeId = domainEvent.CardTypeId != 0 ? domainEvent.CardTypeId : 1;
// 根据用户 ID 从仓储中查找买家是否已存在
var buyer = await _buyerRepository.FindAsync(domainEvent.UserId);
var buyerExisted = buyer is not null; // 标记买家是否已经存在于系统中
// 如果买家不存在,则基于域事件中的用户信息创建一个新的买家实体
if (!buyerExisted)
{
buyer = new Buyer(domainEvent.UserId, domainEvent.UserName);
}
// 验证或为该买家添加当前的支付方式(包含卡号、安全码、持卡人及过期时间等)
buyer.VerifyOrAddPaymentMethod(
cardTypeId,
$"Payment Method on {DateTime.UtcNow}",
domainEvent.CardNumber,
domainEvent.CardSecurityNumber,
domainEvent.CardHolderName,
domainEvent.CardExpiration,
domainEvent.Order.Id);
// 根据买家是否已存在,决定执行仓储的更新操作还是新增操作
var buyerUpdated = buyerExisted ?
_buyerRepository.Update(buyer) :
_buyerRepository.Add(buyer);
// 通过工作单元保存实体更改,确保数据持久化
await _buyerRepository.UnitOfWork
.SaveEntitiesAsync(cancellationToken);
// 创建订单状态变更为“已提交”的集成事件,准备向微服务外部广播
var integrationEvent = new OrderStatusChangedToSubmittedIntegrationEvent(
domainEvent.Order.Id, domainEvent.Order.OrderStatus.Name, buyer.Name);
// 将集成事件添加到队列并持久化保存
await _orderingIntegrationEventService.AddAndSaveEventAsync(integrationEvent);
// 记录追踪日志,标明买家的支付方式已成功验证或更新
OrderingApiTrace.LogOrderBuyerAndPaymentValidatedOrUpdated(
_logger, buyerUpdated.Id, domainEvent.Order.Id);
}
}
The previous domain event handler code is considered application layer code because it uses infrastructure repositories, as explained in the next section on the infrastructure-persistence layer. Event handlers could also use other infrastructure components.
前面提到的领域事件处理程序代码被视为应用层代码,因为它使用了基础设施层的仓储(repositories),这一点我们将在下一节关于“基础设施-持久化层”的内容中进行讲解。此外,事件处理程序也可以使用其他的基础设施组件。
Domain events can generate integration events to be published outside of the microservice boundaries 领域事件可以生成集成事件,以便发布到微服务边界之外
Finally, it's important to mention that you might sometimes want to propagate events across multiple microservices. That propagation is an integration event, and it could be published through an event bus from any specific domain event handler.
最后,需要特别指出的是,有时候你可能希望在多个微服务之间传播事件。这种传播就是一个集成事件(Integration Event),它可以通过事件总线(Event Bus),从任何一个特定的领域事件处理程序中发布出去。
Conclusions on domain events 关于领域事件的总结
As stated, use domain events to explicitly implement side effects of changes within your domain. To use DDD terminology, use domain events to explicitly implement side effects across one or multiple aggregates. Additionally, and for better scalability and less impact on database locks, use eventual consistency between aggregates within the same domain.
如前所述,请使用领域事件来显式地实现领域内变更所带来的次生影响(衍生行为)。用 DDD 的术语来说,就是使用领域事件来显式地实现跨越一个或多个聚合的次生影响(衍生行为)。另外,为了获得更好的可扩展性并减少对数据库锁的影响,你可以在同一领域内的多个聚合之间采用最终一致性(eventual consistency)。
The reference app uses MediatR to propagate domain events synchronously across aggregates, within a single transaction. However, you could also use some AMQP implementation like RabbitMQ or Azure Service Bus to propagate domain events asynchronously, using eventual consistency but, as mentioned above, you have to consider the need for compensatory actions in case of failures.
这个参考应用使用了 MediatR 来在单个事务内,跨聚合同步地传播领域事件。不过,你也可以使用像 RabbitMQ 或 Azure Service Bus 这样的 AMQP 实现,来异步地传播领域事件,并采用最终一致性。但正如前面提到的,你必须考虑到在发生失败时,需要采取补偿性操作(compensatory actions)。

浙公网安备 33010602011771号