处理事件

在现代系统中,遵循诸如 DDD(领域驱动设计)和 Clean Architecture(整洁架构)等架构最佳实践时,事件是用于建模状态变化以及组件或系统之间异步通信的基本机制。 事件表示在应用程序域中已经发生的事情,并且可能对系统的其他部分或外部服务有兴趣。 补充: This section also covers shadow entities and the Transactional Outbox flow with DDD, MediatR, MassTransit, RabbitMQ, Hangfire, and Unit of Work for reliable messaging.

领域事件

领域事件表示在应用程序上下文中发生的重要事实。 它们在系统内部生成和消费,允许状态变化以解耦的方式传播,也就是说,对象之间无需直接相互了解。

何时使用领域事件?

  • 当某个操作在领域中产生重大变化,并且需要通知或触发系统的其他部分时。
  • 为保持领域模型的内聚性,使不同的流程能够触发而无需在模块或服务之间创建直接依赖。

Lino 中,创建新的领域事件非常简单。您可以执行:

lino event new
lino event new --name <EventName> --service <ServiceName> --module <ModuleName> --entity <EntityName>
lino event edit --service <ServiceName> --module <ModuleName> --entity <EntityName>
lino event list --service <ServiceName> --module <ModuleName> --entity <EntityName>

CLI 向导将要求提供:

  • 服务 – 将要创建事件的服务。
  • 模块 – 将要创建事件的模块(仅适用于模块化服务)。
  • 实体 – 事件将创建/关联的实体。
  • 事件类型 – 领域事件或集成事件。
  • 事件名称 – 在领域中使用的名称,与实体关联。

示例

创建与 User 实体关联的领域事件 UserCreated 时, 系统会自动创建名为 UserCreatedDomainEvent 的事件。 该名称清楚地表明,消费该事件的系统部分,用户创建操作已完成。

Lino 生成的结构:

<ProjectName>/
└── src/
    └── Services/
        └── <ServiceName>/
            └── Domain/
                ├── <ProjectName>.<ServiceName>.Domain.csproj
                └── Aggregates/
                    └── Users/
                        ├── User.cs
                        ├── Errors/
                        ├── Events/
                        │   └── UserCreatedDomainEvent.cs
                        ├── Repositories/
                        └── Resources/

Technical flow in Lino

  1. The aggregate registers an IDomainEvent with RegisterDomainEvent.
  2. The command handler saves through IUnitOfWork, preferably SaveChangesInTransactionAsync when events are involved.
  3. The Unit of Work persists changes and publishes events through MediatR.
  4. A domain handler can register an integration event in IOutbox.

Events are not commands; use past-tense names such as UserCreatedDomainEvent.

领域事件处理器

Domain Event Handler 是一个类,负责响应 领域事件,执行与应用程序内部状态相关的操作,并始终在相同的事务上下文中运行。

这些处理器的主要目的是保持系统的内聚性和松耦合,使得可以在不增加实体或聚合核心逻辑负担的情况下应用额外规则。

例如,在创建 User 后,可能需要更新统计数据、生成内部日志或通知其他聚合。 这些操作在领域内是合理的,可以同步执行,从而保证即时一致性。

但是,依赖外部资源的操作——例如发送电子邮件或调用第三方 API——不应直接从领域事件中执行,因为这会将事务绑定到缓慢或不稳定的任务。 在这种情况下,领域可以生成一个集成事件(记录在 Outbox 中),稍后以异步且具弹性的方式处理。

主要特性:

  • 响应事件,但绝不修改事件。
  • 仅执行与领域一致性相关的进程内操作。
  • 确保所有操作在同一事务中运行。

要创建新的领域事件处理器,只需运行以下命令:

lino event-handler new

CLI 向导将要求提供:

  • 服务 – 创建事件处理器的服务。
  • 模块 – 创建事件处理器的模块(仅在模块化服务中)。
  • 实体 – 创建事件处理器的实体。
  • 事件类型 – 领域事件或集成事件。
  • 事件 – 将被消费的事件。
  • 事件处理器名称 – 将与实体和领域事件关联的名称。

示例

创建与实体 User 和事件 UserCreatedDomainEvent 关联的领域事件处理器 UserCreated,系统将自动创建名为 UserCreatedDomainEventHandler 的事件处理器。

Lino 生成的结构:

<ProjectName>/
└── src/
    └── Services/
        └── <ServiceName>/
            └── Application/
                ├── <ProjectName>.<ServiceName>.Application.csproj
                └── UseCases/
                    └── Users/
                        ├── Commands/
                        ├── EventHandlers/
                        │   └── Domain/
                        │       └── UserCreatedDomainEventHandler.cs
                        ├── Logging/
                        ├── Queries/
                        └── Resources/

Technical responsibilities

  • Implements IDomainEventHandler and reacts inside the application boundary.
  • Runs internal consistency, logging, tracing, or integration registration work.
  • Uses IOutbox.RegisterIntegrationEvent for reliable messages.
  • Keeps the business change and OutboxMessage in the same transaction.

集成事件

集成事件是指示发生了重要事件的消息,需要与外部系统或其他微服务共享。

与领域事件不同,这里的目标是系统间通信和状态同步。

何时创建集成事件:

  • 当系统中的更改需要反映到其他系统时。
  • 当您的微服务需要发布更改,以便其他微服务做出响应时。

领域事件与集成事件的主要区别:

方面 领域事件 集成事件
目标受众 内部 外部
耦合度 低(内部) 必要(系统间)
处理时间 即时 可以异步,并提供交付保证
持久化需求 非必须 是(为了可靠性和弹性)

Lino 中,创建新的集成事件很简单。 您可以执行:

lino event new
lino event new --name <EventName> --service <ServiceName> --module <ModuleName> --entity <EntityName>
lino event list --service <ServiceName> --module <ModuleName> --entity <EntityName>

CLI 助手将提示:

  • 服务 – 创建事件的服务。
  • 模块 – 创建事件的模块(仅适用于模块化服务)。
  • 实体 – 创建/关联事件的实体。
  • 事件类型 – 领域事件或集成事件。
  • 事件名称 – 集成事件的名称,与实体关联。

示例

创建与实体 User 关联的集成事件 UserCreated 时,系统会自动生成名为 UserCreatedIntegrationEvent 的事件。 此名称清楚地告诉系统中消费该事件的任何部分,用户创建操作已完成。

Lino 生成的结构:

<ProjectName>/
└── src/
    └── Services/
        └── <ServiceName>/
            └── Integration.Events/
                ├── <ProjectName>.<ServiceName>.Integration.Events.csproj
                └── Users/
                    └── UserCreatedIntegrationEvent.cs

Reliable publication

An integration event implements IIntegrationEvent and crosses boundaries between modules, services, or systems. The application registers the event in IOutbox, the Unit of Work persists an OutboxMessage, and a Hangfire worker later publishes it through MassTransit/RabbitMQ.

集成事件处理器

Integration Event Handler 是一个类,负责消费通常由其他服务或上下文发布的 Integration Event, 并在自身的领域内执行特定操作。

这些处理器通过消息机制(如 RabbitMQ、Kafka、Azure Service Bus 等)接收事件, 通常与 Outbox 模式结合使用,以确保可靠的投递和异步处理。

例如,当 Identity 服务发布 UserCreated 事件时,另一个上下文中的对应处理器可以通过发送欢迎邮件或调用外部 API 来响应。 这些操作可能较慢或容易失败,但由于它们在主事务之外处理,因此不会影响应用的内部一致性。

主要特性:

  • 响应对其他上下文具有业务相关性的事件。
  • 执行可能较慢或外部的操作(例如:发送邮件、调用 API)。
  • 以异步和弹性的方式处理,通常伴随重试和监控。
  • 确保外部失败不会影响原始领域事务。
  • 便于有界上下文和分布式系统之间的集成。

要创建新的集成事件处理器,只需运行以下命令:

lino event-handler new
lino event-handler new --name <EventHandlerName> --service <ServiceName> --module <ModuleName> --entity <EntityName>
lino event-handler list --service <ServiceName> --module <ModuleName> --entity <EntityName>

CLI 向导将请求以下信息:

  • 服务 – 创建事件处理器的服务。
  • 模块 – 创建事件处理器的模块(仅限模块化服务)。
  • 实体 – 创建事件处理器的实体。
  • 事件类型 – 域事件或集成事件。
  • 服务 – 要消费的事件所在的服务。
  • 模块 – 要消费的事件所在的模块(仅限模块化服务)。
  • 实体 – 要消费的事件所在的实体。
  • 事件 – 将被消费的事件。
  • 事件处理器名称 – 将关联到实体和集成事件的名称。

示例

创建与 User 实体及 UserCreatedIntegrationEvent 事件关联的集成事件处理器 SendEmailOnUserCreated 时,系统会自动创建一个名为 SendEmailOnUserCreatedIntegrationEventHandler 的处理器。

Lino 生成的结构:

<ProjectName>/
└── src/
    └── Services/
        └── <ServiceName>/
            └── Application/
                ├── <ProjectName>.<ServiceName>.Application.csproj
                └── UseCases/
                    └── Users/
                        ├── Commands/
                        ├── EventHandlers/
                        │   └── Integration/
                        │       └── SendEmailOnUserCreatedIntegrationEventHandler.cs
                        ├── Logging/
                        ├── Queries/
                        └── Resources/

Operational guidance

  • Implements IIntegrationEventHandler and consumes messages with MassTransit.
  • Does not read the producer database directly.
  • Updates local state, projections, or shadow entities.
  • Should be idempotent because retries and duplicate messages can happen.

Shadow Entities

shadow entity 是一份本地、最小且受控的数据副本,其所有者位于另一个模块或服务中。当 consumer 需要查询、校验或展示参考数据,但又不应直接访问 producer 数据库时,它用于降低耦合。

当 eventual consistency 可以接受时,Shadow Entities 很有用。producer 发布 integration event,消息被持久化到 Outbox,worker 将其发布到 bus,consumer 再通过 integration event handler 更新本地副本。

lino shadow-entity new
lino event new
lino event-handler new

该命令也可以通过 alias lino shadow new 访问。在交互流程中,Lino 会询问目标服务或模块、源 entity,以及应复制哪些 properties。这可以避免手动重新创建 identifier type、必填性、string 长度和基础 references,从而减少 source 与 consumer 之间的不一致。

shadow entity 不是完整副本。以 Catalog 模块消费 TenancySecurity 数据为例,catalog 可以只保留 Tenant.IdTenant.SlugUser.IdUser.EmailUser.TenantId。原始 user entity 仍然包含 hash、confirmation、tokens、dates 以及其他不属于 catalog context 的内部字段。

何时使用

  • 当 consumer 只需要另一个 context 所拥有 entity 的少数字段时。
  • 当业务规则可以接受 producer 变更与 consumer 更新之间的小延迟时。
  • 当直接读取 producer 数据库会在模块或服务之间造成不恰当耦合时。
  • 当 grids、filters、本地 validations 或 projections 需要外部参考数据时。

何时避免

  • 当规则要求数据立即更新且不接受 eventual consistency 时。
  • 当本地副本会变成原始 aggregate 的完整副本时。
  • 当 consumer 开始依赖属于 producer 的 invariants 时。

推荐流程

  1. 仅使用 consumer 所需字段对 shadow entity 建模。
  2. 创建或选择 producer 发布的 integration event。
  3. 在 consumer 中创建 integration event handler。
  4. 使用 producer key 或 natural identifier 实现 idempotent upsert。
  5. 记录足够的 logs,以诊断 retries、重复消息和永久失败。

事件驱动同步

完整流程结合 domain、application、Unit of Work、Outbox、worker、bus 和 consumer。目标是避免模块或服务之间脆弱的直接调用,并在发布或消费失败时支持 retry、audit 和 reprocessing。

  1. aggregate 发生变化并注册 domain event。
  2. Unit of Work 在 transaction 中保存该变更。
  3. domain handler 对该事实作出反应,并在 IOutbox 中注册 integration event。
  4. Unit of Work 在同一 transaction 中持久化 business data 和 OutboxMessage
  5. Hangfire worker 读取 Outbox,并通过 MassTransit/RabbitMQ 发布到 bus。
  6. consumer 通过 IIntegrationEventHandler 接收消息。
  7. consumer 更新其 local model、shadow entity、projection、email、external integration 或其他 asynchronous flow。
Domain change -> Domain Event -> Domain Handler -> Integration Event -> Outbox -> Worker -> Event Bus -> Consumer -> Local model

设计规则

  • 将 invariants 放在 domain 中;使用 events 表达 reactions,而不是隐藏强制规则。
  • 在与 business change 相同的 transaction 中注册 integration events。
  • 保持 integration contracts 小型、versionable 且稳定。
  • 实现 idempotent consumers,因为 retries 和重复消息可能发生。
  • 监控 worker failures 以及停留在 Outbox 中的 messages。

实际结果:producer 不需要知道 consumers,consumers 不访问 producer 数据库,并且即使 broker 或 consumer 暂时不可用,context 之间的通信仍保持 resilient。

发生了未处理的错误。 重新加载 🗙