前言
应用层(Ordering.API)
1.应用层职责
| 职责 | 说明 |
|---|---|
| 1️⃣ 编排业务流程(流程协调者) | 调用多个领域对象(聚合根、领域服务),协调它们完成一个完整的用例。例如:下单时,依次校验库存、计算价格、保存订单、发布领域事件。 |
| 2️⃣ 事务控制(Unit of Work) | 保证一个应用服务方法内的所有操作作为一个事务提交。应用层通常不直接实现事务逻辑,而是通过 IUnitOfWork 控制提交或回滚。 |
| 3️⃣ 调用领域逻辑而非实现逻辑 | 自身不编写业务规则,而是调用聚合根或领域服务执行。例如:order.MarkAsPaid() 或 paymentService.ValidatePayment()。 |
| 4️⃣ 集成外部接口 / 基础设施服务 | 负责调用外部系统(邮件、支付、消息队列等),通常通过接口(Repository、MessageBus、EmailService)间接依赖基础设施。 |
| 5️⃣ 发布领域事件与集成事件 | 负责在事务成功后发布领域事件(DomainEvent)或集成事件(IntegrationEvent),以支持微服务之间的解耦通信。 |
| 6️⃣ 数据传输与 DTO 映射 | 在表现层与领域层之间传递数据时,使用 DTO、Command、Query 等对象进行转换(避免直接暴露领域模型)。 |
| 7️⃣ 输入验证(或调用验证器) | 可调用 FluentValidation 或内置验证器,对输入命令(Command)进行校验(但不涉及业务规则)。 |
| 8️⃣ 安全与授权控制 | 判断调用方是否有权限执行该用例(一般结合认证授权框架)。 |
2.应用层设计原则
| 原则 | 说明 |
|---|---|
| 1️⃣ 职责单一(Single Responsibility) | 每个应用服务只负责一个业务用例(Use Case),例如“创建订单”“取消订单”。不混合多个流程逻辑。 |
| 2️⃣ 协调而非实现业务(Orchestration, Not Implementation) | 应用层负责“调用领域对象完成业务”,而非自己实现业务规则。核心逻辑应放在领域层。 |
| 3️⃣ 明确定义事务边界(Transactional Boundary) | 应用层负责开启与提交事务,确保在一个用例内的一致性;跨聚合或系统需通过事件或最终一致性。 |
| 4️⃣ 依赖倒置(Dependency Inversion) | 依赖领域层定义的抽象(如仓储接口、领域服务接口),不直接依赖基础设施实现。 |
| 5️⃣ 使用 DTO 与 Command 进行隔离(Data Isolation) | 通过输入命令(Command)与输出 DTO(Query/Response)与外部交互,避免外部直接操作领域模型。 |
| 6️⃣ 以用例为中心(Use Case Oriented) | 服务设计围绕业务流程(动词)展开,而非数据操作(CRUD)。体现业务语义,如“确认支付”“发货订单”。 |
| 7️⃣ 保持幂等性(Idempotency) | 应对重复调用(如 API 重试、消息重放),通过业务唯一键或防重复机制保证结果一致。 |
| 8️⃣ 协调领域事件与集成事件(Event Coordination) | 应用层负责在事务完成后发布领域事件、集成事件,驱动跨聚合或跨服务的后续动作。 |
| 9️⃣ 控制安全与权限(Security and Authorization) | 应用层是权限校验的第一关,保证用例执行的安全性与授权合法性。 |
| 🔟 异常与日志集中处理(Exception & Logging) | 应用层负责捕获领域异常、记录日志、返回统一响应,维持系统可观测性与可维护性。 |
3.应用层组成结构
| 组成 | 作用 | 示例 |
|---|---|---|
| 1️⃣ 应用服务(Application Service) | 协调用例流程,管理事务,调用领域对象和领域服务;不包含业务规则。 | OrderingAppService 调用 CreateOrderCommandHandler |
| 2️⃣ 命令(Command) | 封装写操作请求的数据和意图,由命令处理器执行。 | CreateOrderCommand、CancelOrderCommand |
| 3️⃣ 命令处理器(Command Handler) | 执行命令,调用聚合根或领域服务完成操作,并保存变更。 | CreateOrderCommandHandler |
| 4️⃣ 查询(Query / DTO) | 封装只读操作,返回数据传输对象(DTO),不修改领域状态。 | GetOrderByIdQuery、OrderDto |
| 5️⃣ 领域事件处理器(Domain Event Handler) | 响应领域事件,处理跨聚合副作用或发布集成事件。 | OrderStatusChangedToPaidDomainEventHandler |
| 6️⃣ 集成事件服务(Integration Event Service) | 持久化或发布跨微服务的事件,实现系统间解耦。 | OrderingIntegrationEventService |
4.应用服务(ApplicationService )
4.1基本概念
应用服务位于 接口层(Controller/API) 与 领域层(Domain) 之间。它不是业务规则的承载者,而是用例级别的协调者:组织领域对象(聚合、领域服务)、管理事务边界、做权限与输入校验、发布事件、调用基础设施(仓储、消息总线、外部API),最后把结果映射成 DTO 返回给上层。
核心职责
- 1.实现单一用例(每个服务/方法对应一个业务用例)
- 2.组织/协调领域模型(调用聚合方法、领域服务)
- 3.控制事务边界(UnitOfWork:开始/提交/回滚)
- 4.发布领域事件 / 集成事件(事务成功后)
- 5.权限/授权检查(用例级)
- 6.输入 DTO 转换 与 输出 DTO 映射(隔离领域)
- 7.日志、异常统一处理
- 8.实现幂等性/防重放策略(防止攻击者重放历史请求(伪造/重发)来骗服务器执行多次操作)(必要时)
俩中常见实现方式
- 传统 ApplicationService 类(例如
OrderAppService)- 优点:概念直观、便于分组用例、易于注入依赖。
- 场景:不使用 MediatR / CQRS 时常见。
- 命令/查询处理器(Command/Query Handler)(eShop 中常见)
- 每个
CommandHandler对应一个用例(更细粒度)。 - 与 MediatR 配合良好,天然解耦与可测试。
- 每个
二者职责相同,只是表达形式不同。eShop 用 CommandHandler 代替了显式的 ApplicationService。
4.2传统ApplicationService类
在 eShop 中,应用层采用了 CQRS 模式(Command/Query Responsibility Segregation)来实现业务逻辑处理,而不是传统的 ApplicationService 结构。
在这种设计下,命令(Command) 和 查询(Query) 各自由独立的 Handler 处理,系统通过 MediatR 中介者模式实现命令的传递与分发,避免了应用层直接依赖具体服务类,从而实现了更高的解耦性和可测试性。
相比之下,传统的 ApplicationService 通常以类为单位封装单个业务用例(Use Case),职责同样单一,但由表现层(Controller)直接调用,并通过依赖注入管理。
因此,eShop 的实现本质上保留了应用层的单一职责思想,只是通过 MediatR 将命令的调用与执行解耦,实现了更清晰的职责分离与命令流控制。
这里是伪代码,简单了解一下ApplicationService基本实现,我也觉得实际开发过程中CQRS 模式(Command/Query Responsibility Segregation)更为实用
public class OrderAppService
{private readonly IOrderRepository _orderRepository;private readonly IBuyerRepository _buyerRepository;private readonly IOrderingIntegrationEventService _integrationEventService;public OrderAppService(IOrderRepository orderRepo, IBuyerRepository buyerRepo, IOrderingIntegrationEventService integrationEventService){_orderRepository = orderRepo;_buyerRepository = buyerRepo;_integrationEventService = integrationEventService;}public async Task<OrderDto> CreateOrderAsync(CreateOrderCommand cmd){// 1. 输入校验(轻量)if (cmd.Items == null || !cmd.Items.Any()) throw new ArgumentException("订单项不能为空");// 2. 权限/限流/幂等(省略)// 3. 查询领域对象var buyer = await _buyerRepository.FindAsync(cmd.BuyerId);var order = Order.Create(cmd.Items, cmd.ShippingAddress); // 聚合工厂/领域逻辑// 4. 保存_orderRepository.Add(order);await _orderRepository.UnitOfWork.SaveEntitiesAsync(); // 事务 & 领域事件分发// 5. 发布集成事件(事务成功后)var integrationEvent = new OrderStartedIntegrationEvent(order.Id, buyer.Id);await _integrationEventService.PublishThroughEventBusAsync(integrationEvent);// 6. 返回 DTOreturn new OrderDto(order.Id, order.Status.ToString());}
}
4.3命令/查询处理器(Command/Query Handler)
创建订单之前详细的看过了,回忆下命令执行流程(流程比较复杂,这个流程得好好调试几遍,不会的话看之前的记录):
1.MiniApi:最外层 HTTP 接入点(轻量 API);2.创建订单接口(CreateOrderAsync):控制器/端点接收请求并构造 Command,调用 Mediator.Send;3.发送幂等性验证事件(Mediator.Send):触发第一个 MediatR 调用(发送包含幂等 x-requestid 的 IdentifiedCommand);4.初始化日志管道:进入日志管道构造函数;5.初始化幂等性验证器(IdentifiedCommandValidator):构建/解析用于校验命令有效性的 验证规则(注意:validator 通常用于参数校验,不是幂等性);6.初始化参数管道:进入参数验证管道构造函数;7.初始化事务管道:进入事务管道构造函数;8.日志管道1:再次进入日志记录(前面是进入构造函数初始化,这是进入Handle方法开始执行逻辑);9.验证参数管道:执行参数校验(前面是进入构造函数初始化,这是进入Handle方法开始执行逻辑);10.事务管道1:进入事务行为(前面是进入构造函数初始化,这是进入Handle方法开始执行逻辑);11.幂等性验证(CreateOrderIdentifiedCommandHandler):该类并未实现 Handle 方法,实际的执行逻辑由其父类 IdentifiedCommandHandler<T, R> 提供。此类的主要作用是确定泛型类型并绑定具体命令与结果类型,从而实现幂等性处理的通用复用。12.幂等性验证抽象类(IdentifiedCommandHandler<T, R>):通用抽象实现,封装了“查询是否已执行 / 若未执行则转发真实命令”的逻辑;13.发送创建订单事件(Mediator.Send):在幂等性检查通过后,IdentifiedCommandHandler 发起真正的 CreateOrderCommand(第二次调用 MediatR);14.初始化日志管道:发送新的创建订单命令会重新初始化管道(进入日志管道构造函数进行初始化)15.初始化创建订单验证器(IdentifiedCommandValidator):为创建订单命令创建参数验证规则;16.初始化参数管道:进入验证参数构造函数初始化;17.初始化事务管道:进入事务管道构造函数初始化;18.日志管道2:再次日志记录;19.验证参数管道:参数校验执行;20.创建订单处理器:真正执行业务(写入订单、扣库存、发布事件等);21.日志管道2:出栈日志或后置日志;22.事务管道1:提交/回滚事务;23.日志管道1:最终日志收尾。24.创建订单接口(CreateOrderAsync)获取创建订单命令结果25.根据结果返回联合返回类型
创建订单操作由 CreateOrderCommand 表示,并由 创建订单命令处理器(CreateOrderCommandHandler) 负责处理。这体现了 eShop 基于 命令/查询处理器(Command/Query Handler, CQRS)模式 实现的应用服务设计,每个命令对应一个处理器,职责单一且清晰。

5.命令(Command)
大部分命令我们之前都讲过了,这个篇章主要是学习架构思路,重复的我们都一笔带过了
OrdersApi中主要调用了5个命令,4个业务命令,1个幂等性验证命令:
CancelOrderCommand : IRequest<bool>:取消订单命令CreateOrderCommand : IRequest<bool>:创建订单命令CreateOrderDraftCommand : IRequest<OrderDraftDTO>:创建草稿命令ShipOrderCommand : IRequest<bool>:发货订单命令IdentifiedCommand<T, R> : IRequest<R>:幂等性命令
这些命令都继承了 IRequest<bool>接口,说明使用了MediatR的CQRS(Command/Query Handler)模式
都到DDD架构了,这些基本的应该都懂了

6.命令处理器(IRequestHandler<CancelOrderCommand, bool>)
eshop中命令和命令处理器都放在Ordering.API的eShop.Ordering.API.Application.Commands目录下:
- 取消订单命令处理器:
- 处理器:
CancelOrderCommand→CancelOrderCommandHandler - 业务逻辑:
- 1.根据
OrderNumber查询订单 - 2.若订单不存在,返回
false - 3.若订单存在,设置订单状态为已取消
- 4.发布领域事件并保存数据
- 1.根据
- 处理器:
- 创建订单命令处理器:
- 处理器:
CreateOrderCommand→CreateOrderCommandHandler - 业务步骤:
- 1.触发集成事件:订单开始
- 2.构建订单实体
- 3.构建订单明细
- 4.记录操作日志
- 5.调用仓储添加订单
- 6.发布领域事件并保存数据
- 处理器:
- 创建订单草稿命令处理器:
- 处理器:
CreateOrderDraftCommand→CreateOrderDraftCommandHandler - 业务步骤:
- 1.构建订单实体
- 2.构建订单明细
- 3.返回
OrderDraftDTO
- 处理器:
- 发货订单命令处理器:
- 处理器:
ShipOrderCommand→ShipOrderCommandHandler - 业务步骤:
- 1.根据
OrderNumber查询订单 - 2.若订单不存在,返回
false - 3.若订单存在,设置订单状态为已发货
- 4.发布领域事件并保存数据
- 1.根据
- 处理器:
我还是又过了一边业务逻辑。。。

7.查询(Query/DTO)
eShop中单独封装了订单查询接口(IOrderQueries)和订单查询实现(OrderQueries)

在 eShop 中,查询逻辑被单独封装为 OrderQueries 类,这是遵循 CQRS(命令/查询职责分离) 和 DDD 分层设计 的体现。OrderQueries 只负责从数据库读取订单信息、订单摘要或信用卡类型等数据,而不修改系统状态;写操作则由命令处理器(如 CreateOrderCommandHandler、CancelOrderCommandHandler)调用仓储执行。这样的设计实现了 读写分离、职责单一、低耦合高内聚,便于针对查询进行性能优化、单独测试和维护,同时避免聚合根被查询逻辑污染,使系统架构清晰、可扩展性强。

在 eShop 中,DTO(数据传输对象)没有集中管理,而是散布在各个模块中。不过,按照设计原则,DTO 属于 应用层 的职责,因此在开发时可以将它们统一放在一个单独的目录中,便于维护和查找。

8.领域事件处理器(INotificationHandler<T>)
领域事件处理器在领域事件中讲的比较详细,我们这里在过一下:
-
订单等待验证领域事件处理器(
OrderStatusChangedToAwaitingValidationDomainEventHandler: INotificationHandler<OrderCancelledDomainEvent>):- 1.记录日志
- 2.仓储查询订单和买家信息
- 3.构建订单库存列表
- 4.构建订单等待验证集成事件并保存(
INotificationHandler不会触发事务管道,若有命令触发事务管道会查询未发布的集成事件进行发布。正常来说,领域事件都是在命令处理器操作后触发的,命令处理器执行完成后会执行事务管道)
-
订单库存已确认领域事件处理器(
OrderStatusChangedToStockConfirmedDomainEventHandler: INotificationHandler<OrderStatusChangedToStockConfirmedDomainEvent>):- 1.记录日志
- 2.仓储查询订单和买家信息
- 3.构建库存已确认集成事件并保存
-
订单已支付领域事件处理器(
OrderStatusChangedToPaidDomainEventHandler : INotificationHandler<OrderStatusChangedToPaidDomainEvent>):- 1.记录日志
- 2.仓储查询订单和买家信息
- 3.构建订单库存列表
- 4.构建订单已支付集成事件并保存
-
订单已发货领域事件处理器(
OrderShippedDomainEventHandler : INotificationHandler<OrderShippedDomainEvent>):- 1.记录日志
- 2.仓储查询订单和买家信息
- 3.构建订单已发货集成事件并保存
-
订单取消领域事件处理器(
OrderCancelledDomainEventHandler : INotificationHandler<OrderCancelledDomainEvent>):- 1.记录日志
- 2.仓储查询订单和买家信息
- 3.构建订单已取消集成事件并保存
-
订单买家或者支付方式变更领域事件处理器(
UpdateOrderWhenBuyerAndPaymentMethodVerifiedDomainEventHandler : INotificationHandler<BuyerAndPaymentMethodVerifiedDomainEvent>):- 1.仓储查询订单
- 2.更新订单支付方式
- 3.记录日志(这里可能有
bug,最后没有持久化,领域事件没有事务管道保存)
-
订单绑定买家和支付方式领域事件(
ValidateOrAddBuyerAggregateWhenOrderStartedDomainEventHandler : INotificationHandler<OrderStartedDomainEvent>):- 1.确定支付卡类型
- 2.查询买家,若买家不存在,则创建新的买家聚合
- 3.验证或添加支付方式
- 4.保存买家
- 5.构建订单已提交集成事件(验证的时候说明已经提交了订单)
- 6.记录日志
我自己又复习了一遍这几个领域事件,记住领域事件处理器继承INotificationHandler<T>,使用的是MediatR发布/订阅模式,实现在应用层

在 eShop 的架构中,命令处理器触发领域事件,而领域事件可能会进一步发布集成事件。但即使采用 Outbox 模式将业务数据和事件日志在同一事务内保存,领域事件本身是异步的,其处理和集成事件发布的时机仍然不确定。这意味着:
- 1.事务提交后,领域事件可能还未处理完成;
- 2.领域事件生成的集成事件可能延迟发布,甚至在处理失败时未发布;
- 3.数据库状态和下游系统收到的集成事件存在时间差,导致暂时性不一致。
根本原因:
- 事务边界与异步事件不一致:数据库事务保证了业务数据和事件日志的原子性,但异步领域事件处理与事务提交分离;
- 异步执行不可预测:异步领域事件可能在事务提交前、提交后或出现失败重试情况,导致集成事件发布时间不确定;
- 跨服务最终一致性问题:集成事件依赖异步处理,无法纳入本地事务,只能依靠最终一致性策略。
解决方案:
Outbox 模式(主流方案)
在事务内保存业务数据和领域事件日志;
事务提交后,由独立服务或后台任务异步读取事件日志并发布集成事件;
事件发布支持重试和幂等,保证最终一致性。
幂等和重试机制
异步事件处理失败时可重试;
集成事件必须保证幂等,避免重复或丢失。
可靠消息队列
- 使用 Kafka、RabbitMQ 等可靠消息总线,确保至少一次投递;
- 异步发布时可以解耦命令处理器与下游系统,降低事务依赖。
核心结论
在分布式系统中,本地事务无法保证异步领域事件和集成事件与数据库操作严格同步,只能通过 Outbox 模式、幂等与重试机制实现 最终一致性。事务保证了业务数据和事件日志的一致性,而集成事件的发布时机不确定,需要异步处理来确保可靠交付。
这个问题是我在复习领域事件时比较好奇,正常情况可以忽略,微服务架构大部分都是保证事务最终一致性。
9.集成事件服务(IOrderingIntegrationEventService)
集成事件我们在商品服务(Catalog.API)中详细学习了整个完整流程,商品服务(Catalog.API)和订单服务(Ordering.API)架构不一样,商品服务(Catalog.API)简单没有分层,订单服务(Ordering.API)使用了DDD架构。我们再回忆下OrderingIntegrationEventService
IOrderingIntegrationEventService定义了2个接口:
PublishEventsThroughEventBusAsync:发布集成事件AddAndSaveEventAsync:保存集成事件

查看OrderingIntegrationEventService的具体实现PublishEventsThroughEventBusAsync方法:该方法在事务管道中提交事务后调用,因为一个命令可能触发多个集成事件,读取的是同一个事务触发的领域事件(也就是同一个业务的所有集成事件,一个业务命令造成的数据变更会被最后的事务管道提交),然后遍历所有集成事件调用事件总线(RabbitMQEventBus)的PublishAsync进行发布
领域事件触发的集成事件可能无法被及时发布,因为
PublishEventsThroughEventBusAsync仅能发布已经绑定事务并保存到事件日志的事件(即有事务 Id 的事件),领域事件可能没有使用事务。具体解决方案可以参考商品服务(
Catalog.API)中的集成事件服务(CatalogIntegrationEventService),商品集成服务(CatalogIntegrationEventService)并没有事务Id的限制,可以先保存再发布

AddAndSaveEventAsync比较简单,因为由事务管道(TransactionBehavior)统一事务提交,这里只是保存了数据变更

10.MediatR管道
应用层(Application Layer):协调用例,处理命令/查询,调用领域逻辑,管理事务、事件、验证、日志等。不包含业务规则,但管理业务流程。
MediatR 的 Pipeline Behaviors 是跨命令/查询的通用处理逻辑:
- LoggingBehavior:记录请求和响应
- ValidatorBehavior:执行 FluentValidation 验证
- TransactionBehavior:控制事务提交/回滚
这些行为本质上是 用例级别的横切关注点,属于 应用服务职责:
- 它们协调命令的执行流程,而不实现具体业务规则
- 可以在调用领域层之前或之后执行
- 保证命令的处理过程有统一的日志、验证和事务管理

11.验证器
验证器在应用层是因为它属于 用例边界逻辑,负责保证输入合法性,协调命令执行,而不属于领域核心业务规则,放在应用层可以保持领域层纯粹性并方便统一管理。

📌 创作不易,感谢支持!
每一篇内容都凝聚了心血与热情,如果我的内容对您有帮助,欢迎请我喝杯咖啡☕,您的支持是我持续分享的最大动力!
💬 加入交流群(QQ群):576434538

