使用 Web API 实现微服务应用层

Use Dependency Injection to inject infrastructure objects into your application layer    使用依赖注入将基础设施对象注入到你的应用层

As mentioned previously, the application layer can be implemented as part of the artifact (assembly) you are building, such as within a Web API project or an MVC web app project. In the case of a microservice built with ASP.NET Core, the application layer will usually be your Web API library. If you want to separate what is coming from ASP.NET Core (its infrastructure plus your controllers) from your custom application layer code, you could also place your application layer in a separate class library, but that is optional.

正如前文所述,应用层可以作为你正在构建的构件(程序集)的一部分来实现,比如在 Web API 项目或 MVC Web 应用项目中。对于使用 ASP.NET Core 构建的微服务而言,应用层通常就是你的 Web API 类库。如果你想将来自 ASP.NET Core 的部分(其基础设施以及你的控制器)与你自定义的应用层代码分离开来,你也可以将应用层放在一个独立的类库中,但这不是强制要求。

For instance, the application layer code of the ordering microservice is directly implemented as part of the Ordering.API project (an ASP.NET Core Web API project), as shown in Figure 7-23.

例如, Ordering(下单)微服务的应用层代码就是直接作为 Ordering.API 项目(一个 ASP.NET Core Web API 项目)的一部分来实现的,如图 7-23 所示。

Screenshot of the Ordering.API microservice in the Solution Explorer.

The Solution Explorer view of the Ordering.API microservice, showing the subfolders under the Application folder: Behaviors, Commands, DomainEventHandlers, IntegrationEvents, Models, Queries, and Validations.

解决方案资源管理器中 Ordering.API 微服务的视图,展示了 Application 文件夹下的子文件夹:Behaviors(行为)、Commands(命令)、DomainEventHandlers(领域事件处理器)、IntegrationEvents(集成事件)、Models(模型)、Queries(查询)和 Validations(验证)。

Figure 7-23. The application layer in the Ordering.API ASP.NET Core Web API project

图 7-23. Ordering.API ASP.NET Core Web API 项目中的应用层

ASP.NET Core includes a simple built-in IoC container (represented by the IServiceProvider interface) that supports constructor injection by default, and ASP.NET makes certain services available through DI. ASP.NET Core uses the term service for any of the types you register that will be injected through DI. You configure the built-in container's services in your application's Program.cs file. Your dependencies are implemented in the services that a type needs and that you register in the IoC container.

ASP.NET Core 包含一个简单的内置 IoC 容器(由 IServiceProvider 接口表示),它默认支持构造函数注入(Constructor Injection),并且 ASP.NET 通过 DI 提供了某些可用的服务。ASP.NET Core 使用“服务(Service)”这个术语来指代你注册的、将通过 DI 注入的任何类型。你在应用程序的 Program.cs 文件中配置内置容器的服务。你的依赖项是在某个类型所需且已在 IoC 容器中注册的服务中实现的。

Typically, you want to inject dependencies that implement infrastructure objects. A typical dependency to inject is a repository. But you could inject any other infrastructure dependency that you may have. For simpler implementations, you could directly inject your Unit of Work pattern object (the EF DbContext object), because the DBContext is also the implementation of your infrastructure persistence objects.

通常,你希望注入那些实现基础设施对象的依赖项。一个典型的注入依赖是仓储(Repository)。当然,你也可以注入任何其他可能拥有的基础设施依赖。对于较简单的实现,你可以直接注入你的工作单元(Unit of Work)模式对象(即 EF 的 DbContext 对象),因为 DbContext 本身也是你的基础设施持久化对象的实现。

In the following example, you can see how .NET is injecting the required repository objects through the constructor. The class is a command handler, which will get covered in the next section.

在下面的例子中,你可以看到 .NET 是如何通过构造函数注入所需的仓储对象的。该类是一个命令处理器(Command Handler),我们将在下一节详细讨论它。

/// <summary>
/// 创建订单命令处理器。
/// 实现了 MediatR 的 IRequestHandler 接口,用于处理 CreateOrderCommand 请求并返回布尔值结果。
/// </summary>
public class CreateOrderCommandHandler
    : IRequestHandler<CreateOrderCommand, bool>
{
    // 订单仓储接口,用于数据库的持久化操作
    private readonly IOrderRepository _orderRepository;

    // 身份服务接口,通常用于获取当前登录用户的信息
    private readonly IIdentityService _identityService;

    // MediatR 中介者接口,用于在应用程序内部发送事件或通知
    private readonly IMediator _mediator;

    // 订单集成事件服务接口,用于处理跨微服务的领域事件发布与保存
    private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;

    // 日志记录器接口,用于记录当前类的运行日志
    private readonly ILogger<CreateOrderCommandHandler> _logger;

    /// <summary>
    /// 构造函数,通过依赖注入(DI)自动注入基础设施层的持久化仓储及相关服务。
    /// </summary>
    /// <param name="mediator">MediatR 中介者</param>
    /// <param name="orderingIntegrationEventService">订单集成事件服务</param>
    /// <param name="orderRepository">订单仓储</param>
    /// <param name="identityService">身份服务</param>
    /// <param name="logger">日志记录器</param>
    public CreateOrderCommandHandler(IMediator mediator,
        IOrderingIntegrationEventService orderingIntegrationEventService,
        IOrderRepository orderRepository,
        IIdentityService identityService,
        ILogger<CreateOrderCommandHandler> logger)
    {
        // 对注入的依赖进行空值检查,防止运行时出现 NullReferenceException
        _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
        _identityService = identityService ?? throw new ArgumentNullException(nameof(identityService));
        _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
        _orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    /// <summary>
    /// 异步处理创建订单的命令逻辑。
    /// </summary>
    /// <param name="message">传入的创建订单命令对象,包含用户和商品等信息</param>
    /// <param name="cancellationToken">取消令牌,用于支持操作的取消</param>
    /// <returns>如果订单成功保存到数据库则返回 true,否则返回 false</returns>
    public async Task<bool> Handle(CreateOrderCommand message, CancellationToken cancellationToken)
    {
        // 1. 添加一个集成事件,用于通知其他微服务清空用户的购物车(Basket)
        var orderStartedIntegrationEvent = new OrderStartedIntegrationEvent(message.UserId);
        // 将事件添加到队列中并持久化保存,确保事务一致性
        await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStartedIntegrationEvent);

        // 2. 添加/更新买家聚合根(Buyer AggregateRoot)
        // 【DDD 模式注释】:必须通过 Order 聚合根的方法和构造函数来添加子实体和值对象。
        // 这样可以确保验证规则、不变量(invariants)和业务逻辑得到执行,
        // 从而保证整个聚合(Aggregate)内部的数据一致性。
        
        // 根据传入的参数实例化地址值对象(Value Object)
        var address = new Address(message.Street, message.City, message.State, message.Country, message.ZipCode);
        // 实例化订单聚合根,传入用户信息、地址以及支付卡片等详细信息
        var order = new Order(message.UserId, message.UserName, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration);

        // 遍历命令中包含的所有订单项,并通过聚合根方法将其安全地添加到订单中
        foreach (var item in message.OrderItems)
        {
            order.AddOrderItem(item.ProductId, item.ProductName, item.UnitPrice, item.Discount, item.PictureUrl, item.Units);
        }

        // 记录信息级别的日志,输出正在创建的订单详情(使用结构化日志格式 {@Order})
        _logger.LogInformation("----- Creating Order - Order: {@Order}", order);

        // 3. 将构建好且经过业务验证的订单对象添加到仓储中
        _orderRepository.Add(order);

        // 4. 调用工作单元(Unit of Work)的 SaveEntitiesAsync 方法,
        // 统一提交所有变更到数据库,并触发之前注册的领域事件和集成事件
        return await _orderRepository.UnitOfWork
            .SaveEntitiesAsync(cancellationToken);
    }
}

The class uses the injected repositories to execute the transaction and persist the state changes. It does not matter whether that class is a command handler, an ASP.NET Core Web API controller method, or a DDD Application Service. It is ultimately a simple class that uses repositories, domain entities, and other application coordination in a fashion similar to a command handler. Dependency Injection works the same way for all the mentioned classes, as in the example using DI based on the constructor.

该类使用注入的仓储来执行事务,并持久化状态的更改。无论这个类是一个命令处理器(Command Handler)、一个 ASP.NET Core Web API 控制器方法,还是一个 DDD 应用服务(Application Service),都无关紧要。归根结底,它只是一个以类似于命令处理器的方式,使用仓储、领域实体和其他应用协调逻辑的简单类。对于所有上述提到的类,依赖注入的工作方式都是相同的,就像之前展示的基于构造函数的 DI 示例一样。

Register the dependency implementation types and interfaces or abstractions    注册依赖的实现类型、接口或抽象

Before you use the objects injected through constructors, you need to know where to register the interfaces and classes that produce the objects injected into your application classes through DI. (Like DI based on the constructor, as shown previously.)

在你使用通过构造函数注入的对象之前,你需要知道在哪里注册那些将被注入到你应用程序类中的对象的接口和类。(就像之前展示的基于构造函数的 DI 一样。)

Use the built-in IoC container provided by ASP.NET Core    使用 ASP.NET Core 提供的内置 IoC 容器

When you use the built-in IoC container provided by ASP.NET Core, you register the types you want to inject in the Program.cs file, as in the following code:

当你使用 ASP.NET Core 提供的内置 IoC 容器时,你需要在 Program.cs 文件中注册你想要注入的类型,就像下面的代码一样:

// 注册框架内置的服务(如 MVC、数据库上下文等开箱即用的服务)
builder.Services.AddDbContext<CatalogContext>(c =>
    // 配置 CatalogContext 使用 SQL Server,连接字符串从配置文件 (Configuration) 中读取
    c.UseSqlServer(Configuration["ConnectionString"]),
    // 将 DbContext 的生命周期设置为 Scoped(作用域),确保在同一个 HTTP 请求内共享同一个实例
    ServiceLifetime.Scoped);

// 添加并注册 ASP.NET Core MVC 框架服务,以支持模型绑定、路由和控制器等功能
builder.Services.AddMvc();

// 注册自定义的应用程序依赖项
// 将 IMyCustomRepository 接口与其具体的 SQL 实现类进行绑定
// 生命周期同样为 Scoped,保证每次请求获取的是独立的仓储实例
builder.Services.AddScoped<IMyCustomRepository, MyCustomSQLRepository>();

The most common pattern when registering types in an IoC container is to register a pair of types—an interface and its related implementation class. Then when you request an object from the IoC container through any constructor, you request an object of a certain type of interface. For instance, in the previous example, the last line states that when any of your constructors have a dependency on IMyCustomRepository (interface or abstraction), the IoC container will inject an instance of the MyCustomSQLServerRepository implementation class.

在 IoC 容器中注册类型时,最常用的模式是注册一对类型——即一个接口及其对应的实现类。当你通过构造函数从 IoC 容器请求一个对象时,你实际上是请求某个特定接口类型的对象。例如,在上一个例子中,最后一行代码的意思是:当你的任何一个构造函数依赖于 IMyCustomRepository(接口或抽象)时,IoC 容器就会自动注入一个 MyCustomSQLServerRepository 实现类的实例。

Use the Scrutor library for automatic types registration    使用 Scrutor 库进行类型的自动注册

When using DI in .NET, you might want to be able to scan an assembly and automatically register its types by convention. This feature is not currently available in ASP.NET Core. However, you can use the Scrutor library for that. This approach is convenient when you have dozens of types that need to be registered in your IoC container.

在 .NET 中使用依赖注入(DI)时,你可能希望扫描某个程序集(Assembly),并能根据约定自动注册其中的类型。目前 ASP.NET Core 框架本身并未提供此功能,但你可以使用 Scrutor 库来实现这一点。当你拥有数十个需要在 IoC 容器中注册的类型时,这种方法非常便捷。

Use Autofac as an IoC container    使用 Autofac 作为 IoC 容器

You can also use additional IoC containers and plug them into the ASP.NET Core pipeline, as in the ordering microservice in eShopOnContainers, which uses Autofac. When using Autofac you typically register the types via modules, which allow you to split the registration types between multiple files depending on where your types are, just as you could have the application types distributed across multiple class libraries.

你还可以使用其他的 IoC 容器,并将它们接入 ASP.NET Core 的管道中。例如,在 eShopOnContainers 示例中的 Ordering(下单)微服务就使用了 Autofac。当使用 Autofac 时,你通常通过模块(Modules)来注册类型。这种方式允许你根据类型的所在位置,将注册类型拆分到多个不同的文件中,就像你可以将应用程序类型分布在多个类库中一样。

For example, the following is the Autofac application module for the Ordering.API Web API project with the types you will want to inject.

例如,以下是 Ordering.API Web API 项目的 Autofac 应用模块,其中包含了你想要注入的类型。

/// <summary>
/// 应用程序的 Autofac 依赖注入模块。
/// 负责配置和注册应用层所需的各类服务(如查询接口、仓储接口等)到 IoC 容器中。
/// </summary>
public class ApplicationModule : Autofac.Module
{
    /// <summary>
    /// 获取用于执行数据查询操作的数据库连接字符串。
    /// </summary>
    public string QueriesConnectionString { get; }

    /// <summary>
    /// 初始化 <see cref="ApplicationModule"/> 类的新实例。
    /// </summary>
    /// <param name="qconstr">用于查询操作的数据库连接字符串</param>
    public ApplicationModule(string qconstr)
    {
        // 将传入的连接字符串赋值给只读属性,供后续注册查询服务时使用
        QueriesConnectionString = qconstr;
    }

    /// <summary>
    /// 重写基类的 Load 方法,在此处向 Autofac 容器构建器中注册当前模块所需的各项服务和依赖。
    /// </summary>
    /// <param name="builder">Autofac 的容器构建器对象</param>
    protected override void Load(ContainerBuilder builder)
    {
        // 注册订单查询服务:使用工厂委托方式创建 OrderQueries 实例,并传入专用的查询连接字符串
        // 将其映射为 IOrderQueries 接口,生命周期为“每个生命周期作用域一个实例”(相当于 Scoped 级别)
        builder.Register(c => new OrderQueries(QueriesConnectionString))
            .As<IOrderQueries>()
            .InstancePerLifetimeScope();

        // 注册买家仓储服务:将 BuyerRepository 类映射为 IBuyerRepository 接口
        // 生命周期为“每个生命周期作用域一个实例”,确保在同一请求上下文中共享同一个仓储实例
        builder.RegisterType<BuyerRepository>()
            .As<IBuyerRepository>()
            .InstancePerLifetimeScope();

        // 注册订单仓储服务:将 OrderRepository 类映射为 IOrderRepository 接口
        // 同样采用 Scoped 生命周期,以配合 EF Core DbContext 的生命周期管理
        builder.RegisterType<OrderRepository>()
            .As<IOrderRepository>()
            .InstancePerLifetimeScope();

        // 注册请求管理器服务:将 RequestManager 类映射为 IRequestManager 接口
        // 用于处理幂等性请求或防重复提交逻辑,生命周期同样限定在当前作用域内
        builder.RegisterType<RequestManager>()
            .As<IRequestManager>()
            .InstancePerLifetimeScope();
   }
}

Autofac also has a feature to scan assemblies and register types by name conventions.

Autofac 也具备扫描程序集并根据名称约定注册类型的功能。

The registration process and concepts are very similar to the way you can register types with the built-in ASP.NET Core IoC container, but the syntax when using Autofac is a bit different.

注册流程和概念与使用 ASP.NET Core 内置 IoC 容器注册类型的方式非常相似,但使用 Autofasc 时的语法会略有不同。

In the example code, the abstraction IOrderRepository is registered along with the implementation class OrderRepository. This means that whenever a constructor is declaring a dependency through the IOrderRepository abstraction or interface, the IoC container will inject an instance of the OrderRepository class.

在示例代码中,抽象接口 IOrderRepository 与其对应的实现类 OrderRepository 被进行了注册。这意味着,无论何时,只要有一个构造函数通过 IOrderRepository 抽象或接口声明了依赖项,IoC 容器就会注入一个 OrderRepository 类的实例。

The instance scope type determines how an instance is shared between requests for the same service or dependency. When a request is made for a dependency, the IoC container can return the following:

实例作用域类型决定了实例如何在针对同一服务或依赖项的请求之间被共享。当针对某个依赖项发出请求时,IoC 容器可以返回以下对象:

  • A single instance per lifetime scope (referred to in the ASP.NET Core IoC container as scoped).

    每个生命周期作用域一个单例实例(在 ASP.NET Core IoC 容器中被称为 scoped / 作用域内单例)。

  • A new instance per dependency (referred to in the ASP.NET Core IoC container as transient).

    每个依赖项一个新实例(在 ASP.NET Core IoC 容器中被称为 transient / 瞬态)。

  • A single instance shared across all objects using the IoC container (referred to in the ASP.NET Core IoC container as singleton).

    一个在所有使用该 IoC 容器的对象之间共享的单一实例(在 ASP.NET Core IoC 容器中被称为 singleton / 全局单例)。

Implement the Command and Command Handler patterns    实现命令(Command)与命令处理器(Command Handler)模式

In the DI-through-constructor example shown in the previous section, the IoC container was injecting repositories through a constructor in a class. But exactly where were they injected? In a simple Web API (for example, the catalog microservice in eShopOnContainers), you inject them at the MVC controllers' level, in a controller constructor, as part of the request pipeline of ASP.NET Core. However, in the initial code of this section (the CreateOrderCommandHandler class from the Ordering.API service in eShopOnContainers), the injection of dependencies is done through the constructor of a particular command handler. Let us explain what a command handler is and why you would want to use it.

在上一节展示的“基于构造函数的依赖注入(DI)”示例中,IoC 容器是通过某个类的构造函数来注入仓储(Repositories)的。但问题是,这些依赖项具体是被注入到哪里去了呢?在一个简单的 Web API 中(例如 eShopOnContainers 中的 Catalog 微服务),你会将它们注入到 MVC 控制器(Controller)的构造函数中,作为 ASP.NET Core 请求管道的一部分。然而,在本节的初始代码中(即 eShopOnContainers 的 Ordering.API 服务中的 CreateOrderCommandHandler 类),依赖项的注入是通过特定命令处理器的构造函数来完成的。让我们来解释一下什么是命令处理器,以及为什么要使用它。

The Command pattern is intrinsically related to the CQRS pattern that was introduced earlier in this guide. CQRS has two sides. The first area is queries, using simplified queries with the Dapper micro ORM, which was explained previously. The second area is commands, which are the starting point for transactions, and the input channel from outside the service.

命令模式(Command Pattern) 与本指南前面介绍的 CQRS 模式 密切相关。CQRS 有两面性:
  1. 查询侧(Queries):使用简化的查询(如前文所述使用 Dapper 微 ORM)。
  2. 命令侧(Commands):这是事务的起点,也是服务外部的输入通道。

As shown in Figure 7-24, the pattern is based on accepting commands from the client-side, processing them based on the domain model rules, and finally persisting the states with transactions.

如图 7-24 所示,该模式基于从客户端接受命令,根据领域模型(Domain Model)的规则对其进行处理,并最终通过事务持久化状态。

Diagram showing the high-level data flow from the client to database.

Figure 7-24. High-level view of the commands or "transactional side" in a CQRS pattern

图 7-24. CQRS 模式中命令侧或“事务侧”的高层视图

Figure 7-24 shows that the UI app sends a command through the API that gets to a CommandHandler, that depends on the Domain model and the Infrastructure, to update the database.

图 7-24 展示了 UI 应用程序通过 API 发送一个命令,该命令到达 CommandHandler,后者依赖于领域模型和基础设施,以更新数据库。

The command class    命令类

A command is a request for the system to perform an action that changes the state of the system. Commands are imperative, and should be processed just once.

命令(Command)是向系统发出请求,要求其执行一个能够改变系统状态的动作。命令是“祈使性”的(imperative),并且应该仅被处理一次。

Since commands are imperatives, they are typically named with a verb in the imperative mood (for example, "create" or "update"), and they might include the aggregate type, such as CreateOrderCommand. Unlike an event, a command is not a fact from the past; it is only a request, and thus may be refused.

由于命令是祈使性的,它们通常以祈使语气的动词来命名(例如“create”或“update”),并且可能会包含聚合(Aggregate)的类型,比如 CreateOrderCommand。与事件(Event)不同,命令不是对过去事实的陈述;它仅仅是一个请求,因此可能会被拒绝。

Commands can originate from the UI as a result of a user initiating a request, or from a process manager when the process manager is directing an aggregate to perform an action.

命令可以源自 UI(当用户发起请求时),也可以源自流程管理器(Process Manager,当流程管理器指导某个聚合执行动作时)。

An important characteristic of a command is that it should be processed just once by a single receiver. This is because a command is a single action or transaction you want to perform in the application. For example, the same order creation command should not be processed more than once. This is an important difference between commands and events. Events may be processed multiple times, because many systems or microservices might be interested in the event.

命令的一个重要特征是,它应该被单个接收者仅处理一次。这是因为命令是你希望在应用程序中执行的单一动作或事务。例如,同一个“创建订单”的命令不应该被处理超过一次。这是命令与事件之间的一个重要区别。事件可能会被处理多次,因为可能有多个系统或微服务对该事件感兴趣。

In addition, it is important that a command be processed only once in case the command is not idempotent. A command is idempotent if it can be executed multiple times without changing the result, either because of the nature of the command, or because of the way the system handles the command.

此外,如果命令不是“幂等(idempotent)”的,那么确保它仅被处理一次就显得尤为重要。如果一个命令可以被执行多次而不会改变结果(无论是由于命令本身的性质,还是由于系统处理命令的方式),那么它就是幂等的。

It is a good practice to make your commands and updates idempotent when it makes sense under your domain's business rules and invariants. For instance, to use the same example, if for any reason (retry logic, hacking, etc.) the same CreateOrder command reaches your system multiple times, you should be able to identify it and ensure that you do not create multiple orders. To do so, you need to attach some kind of identity in the operations and identify whether the command or update was already processed.

在你的领域业务规则和不变量(invariants)允许的情况下,让你的命令和更新操作具备幂等性是一个很好的实践。例如,沿用上面的例子,如果出于任何原因(重试逻辑、黑客攻击等),同一个 CreateOrder 命令多次到达你的系统,你应该能够识别出它,并确保不会创建多个重复的订单。为此,你需要在操作中附加某种身份标识,并判断该命令或更新是否已经被处理过。

You send a command to a single receiver; you do not publish a command. Publishing is for events that state a fact—that something has happened and might be interesting for event receivers. In the case of events, the publisher has no concerns about which receivers get the event or what they do it. But domain or integration events are a different story already introduced in previous sections.

你将命令发送给单个接收者;你不会“发布(publish)”命令。发布是针对事件的,用于陈述一个事实——即某事已经发生,并且接收者可能对此感兴趣。在事件的情况下,发布者不关心哪些接收者会收到事件,也不关心它们会如何处理。不过,领域事件或集成事件是另一回事,我们在前面的章节中已经介绍过了。

A command is implemented with a class that contains data fields or collections with all the information that is needed in order to execute that command. A command is a special kind of Data Transfer Object (DTO), one that is specifically used to request changes or transactions. The command itself is based on exactly the information that is needed for processing the command, and nothing more.

命令是通过一个类来实现的,该类包含数据字段或集合,其中包含了执行该命令所需的所有信息。命令是一种特殊的数据传输对象(DTO),专门用于请求更改或事务。命令本身仅基于处理该命令所需的确切信息,不多也不少。

The following example shows the simplified CreateOrderCommand class. This is an immutable command that is used in the ordering microservice in eShopOnContainers.

下面的示例展示了简化版的 CreateOrderCommand 类。这是一个不可变(immutable)的命令,用于 eShopOnContainers 中的 Ordering(下单)微服务。

// DDD 和 CQRS 模式注释:注意,建议将命令(Commands)实现为不可变对象。
// 在这种情况下,其不可变性是通过将所有 setter 设为私有来实现的,
// 并且只能在通过构造函数创建对象时更新一次数据。
// 关于不可变命令的参考链接:
// http://cqrs.nu/Faq
// https://docs.spine3.org/motivation/immutability.html
// http://blog.gauffin.org/2012/06/griffin-container-introducing-command-support/
// https://learn.microsoft.com/dotnet/csharp/programming-guide/classes-and-structs/how-to-implement-a-lightweight-class-with-auto-implemented-properties

/// <summary>
/// 创建订单命令类,封装了生成新订单所需的所有输入参数。
/// </summary>
[DataContract]
public class CreateOrderCommand
    : IRequest<bool> // 继承 MediatR 的请求接口,定义返回类型为 bool
{
    /// <summary>
    /// 订单项集合的只读内部字段。
    /// </summary>
    [DataMember]
    private readonly List<OrderItemDTO> _orderItems;

    /// <summary>
    /// 用户 ID。
    /// </summary>
    [DataMember]
    public string UserId { get; private set; }

    /// <summary>
    /// 用户名。
    /// </summary>
    [DataMember]
    public string UserName { get; private set; }

    /// <summary>
    /// 城市。
    /// </summary>
    [DataMember]
    public string City { get; private set; }

    /// <summary>
    /// 街道地址。
    /// </summary>
    [DataMember]
    public string Street { get; private set; }

    /// <summary>
    /// 州/省。
    /// </summary>
    [DataMember]
    public string State { get; private set; }

    /// <summary>
    /// 国家。
    /// </summary>
    [DataMember]
    public string Country { get; private set; }

    /// <summary>
    /// 邮政编码。
    /// </summary>
    [DataMember]
    public string ZipCode { get; private set; }

    /// <summary>
    /// 信用卡卡号。
    /// </summary>
    [DataMember]
    public string CardNumber { get; private set; }

    /// <summary>
    /// 持卡人姓名。
    /// </summary>
    [DataMember]
    public string CardHolderName { get; private set; }

    /// <summary>
    /// 信用卡过期时间。
    /// </summary>
    [DataMember]
    public DateTime CardExpiration { get; private set; }

    /// <summary>
    /// 信用卡安全码 (CVV)。
    /// </summary>
    [DataMember]
    public string CardSecurityNumber { get; private set; }

    /// <summary>
    /// 信用卡类型 ID。
    /// </summary>
    [DataMember]
    public int CardTypeId { get; private set; }

    /// <summary>
    /// 获取订单项目的只读集合,防止外部直接修改内部列表。
    /// </summary>
    [DataMember]
    public IEnumerable<OrderItemDTO> OrderItems => _orderItems;

    /// <summary>
    /// 默认无参构造函数,初始化空的订单项列表。
    /// </summary>
    public CreateOrderCommand()
    {
        _orderItems = new List<OrderItemDTO>(); // 初始化内部订单项列表
    }

    /// <summary>
    /// 带参数的构造函数,用于根据购物车项及支付信息构建完整的创建订单命令。
    /// </summary>
    /// <param name="basketItems">购物车中的商品项列表。</param>
    /// <param name="userId">当前操作用户的 ID。</param>
    /// <param name="userName">当前操作用户的名称。</param>
    /// <param name="city">收货地址所在城市。</param>
    /// <param name="street">收货地址所在街道。</param>
    /// <param name="state">收货地址所在州/省。</param>
    /// <param name="country">收货地址所在国家。</param>
    /// <param name="zipcode">收货地邮政编码。</param>
    /// <param name="cardNumber">支付使用的信用卡号。</param>
    /// <param name="cardHolderName">持卡人姓名。</param>
    /// <param name="cardExpiration">信用卡有效期。</param>
    /// <param name="cardSecurityNumber">信用卡安全码。</param>
    /// <param name="cardTypeId">信用卡类型的标识 ID。</param>
    public CreateOrderCommand(List<BasketItem> basketItems, string userId, string userName, string city, string street, string state, string country, string zipcode,
        string cardNumber, string cardHolderName, DateTime cardExpiration,
        string cardSecurityNumber, int cardTypeId) : this() // 调用默认构造函数以初始化列表
    {
        _orderItems = basketItems.ToOrderItemsDTO().ToList(); // 将购物车项转换为订单项 DTO 并赋值给内部列表
        UserId = userId; // 设置用户 ID
        UserName = userName; // 设置用户名
        City = city; // 设置城市
        Street = street; // 设置街道
        State = state; // 设置州/省
        Country = country; // 设置国家
        ZipCode = zipcode; // 设置邮编
        CardNumber = cardNumber; // 设置信用卡号
        CardHolderName = cardHolderName; // 设置持卡人姓名
        CardExpiration = cardExpiration; // 设置信用卡过期时间
        CardSecurityNumber = cardSecurityNumber; // 设置信用卡安全码
        CardTypeId = cardTypeId; // 设置信用卡类型 ID
        CardExpiration = cardExpiration; // 再次设置信用卡过期时间(注:此处代码存在冗余赋值)
    }


    /// <summary>
    /// 订单项数据传输对象 (DTO),用于在层间传递单个商品的购买信息。
    /// </summary>
    public class OrderItemDTO
    {
        /// <summary>
        /// 产品的唯一标识符。
        /// </summary>
        public int ProductId { get; set; }

        /// <summary>
        /// 产品名称。
        /// </summary>
        public string ProductName { get; set; }

        /// <summary>
        /// 产品单价。
        /// </summary>
        public decimal UnitPrice { get; set; }

        /// <summary>
        /// 折扣金额。
        /// </summary>
        public decimal Discount { get; set; }

        /// <summary>
        /// 购买数量。
        /// </summary>
        public int Units { get; set; }

        /// <summary>
        /// 产品图片 URL。
        /// </summary>
        public string PictureUrl { get; set; }
    }
}

Basically, the command class contains all the data you need for performing a business transaction by using the domain model objects. Thus, commands are simply data structures that contain read-only data, and no behavior. The command's name indicates its purpose. In many languages like C#, commands are represented as classes, but they are not true classes in the real object-oriented sense.

基本上,命令类包含了你使用领域模型对象执行业务事务所需的所有数据。因此,命令仅仅是包含只读数据、不包含任何行为的数据结构。命令的名称表明了它的目的。在许多语言(如 C#)中,命令被表示为类,但它们并不是真正面向对象意义上的那种“类”(因为它们缺乏行为)。

As an additional characteristic, commands are immutable, because the expected usage is that they are processed directly by the domain model. They do not need to change during their projected lifetime. In a C# class, immutability can be achieved by not having any setters or other methods that change the internal state.

作为另一个特征,命令是不可变的(immutable),因为预期的使用方式是它们由领域模型直接处理。在它们预期的生命周期内,不需要发生任何改变。在 C# 类中,可以通过不设置任何 setter 或其他能够改变内部状态的方法来实现不可变性。

Keep in mind that if you intend or expect commands to go through a serializing/deserializing process, the properties must have a private setter, and the [DataMember] (or [JsonProperty]) attribute. Otherwise, the deserializer won't be able to reconstruct the object at the destination with the required values. You can also use truly read-only properties if the class has a constructor with parameters for all properties, with the usual camelCase naming convention, and annotate the constructor as [JsonConstructor]. However, this option requires more code.

请记住,如果你打算或预期命令会经历序列化/反序列化过程,那么属性必须具有私有 setter,并且需要加上 [DataMember](或 [JsonProperty])特性。否则,反序列化器将无法在目标端利用所需的值重新构造出该对象。如果你的类拥有一个为所有属性提供参数的构造函数(遵循通常的驼峰命名法 camelCase),并且使用 [JsonConstructor] 对该构造函数进行了标注,那么你也可以使用真正的只读属性。不过,这种选项需要编写更多的代码。

For example, the command class for creating an order is probably similar in terms of data to the order you want to create, but you probably do not need the same attributes. For instance, CreateOrderCommand does not have an order ID, because the order has not been created yet.

例如,用于创建订单的命令类,就其数据而言,可能与你想要创建的订单非常相似,但你可能并不需要完全相同的属性。例如,CreateOrderCommand 没有订单 ID(Order ID),因为订单尚未被创建。

Many command classes can be simple, requiring only a few fields about some state that needs to be changed. That would be the case if you are just changing the status of an order from "in process" to "paid" or "shipped" by using a command similar to the following:

许多命令类可以非常简单,仅需要几个关于某些需要更改的状态的字段。如果你只是想通过使用类似于下面这样的命令,将订单的状态从“处理中(in process)”更改为“已付款(paid)”或“已发货(shipped)”,就会是这种情况:

/// <summary>
/// 更新订单状态的命令类,封装了修改现有订单状态所需的所有参数。
/// </summary>
[DataContract]
public class UpdateOrderStatusCommand
    : IRequest<bool> // 继承 MediatR 的请求接口,定义该命令的返回类型为布尔值
{
    /// <summary>
    /// 目标订单的新状态(如:已支付、已发货等)。
    /// </summary>
    [DataMember]
    public string Status { get; private set; }

    /// <summary>
    /// 需要更新的订单的唯一标识符(ID)。
    /// </summary>
    [DataMember]
    public string OrderId { get; private set; }

    /// <summary>
    /// 购买者的身份唯一标识符(GUID),用于权限校验或业务逻辑处理。
    /// </summary>
    [DataMember]
    public string BuyerIdentityGuid { get; private set; }
}

Some developers make their UI request objects separate from their command DTOs, but that is just a matter of preference. It is a tedious separation with not much additional value, and the objects are almost exactly the same shape. For instance, in eShopOnContainers, some commands come directly from the client-side.

有些开发者会将 UI 层的请求对象与命令 DTO 区分开来,但这仅仅是个人偏好问题。这种强制分离显得相当繁琐,且并没有带来太多额外的价值,因为这两类对象的形状几乎是完全相同的。例如,在 eShopOnContainers 中,一些命令就是直接从客户端传来的。

The Command handler class    命令处理器类

You should implement a specific command handler class for each command. That is how the pattern works, and it's where you'll use the command object, the domain objects, and the infrastructure repository objects. The command handler is in fact the heart of the application layer in terms of CQRS and DDD. However, all the domain logic should be contained in the domain classes—within the aggregate roots (root entities), child entities, or domain services, but not within the command handler, which is a class from the application layer.

你应该为每一个命令实现一个特定的命令处理器类。这正是该模式的运作方式,也是你使用命令对象、领域对象以及基础设施仓储对象的地方。就 CQRS 和 DDD 而言,命令处理器实际上是应用层的核心。但是,所有的领域逻辑都应该包含在领域类中——即聚合根(根实体)、子实体或领域服务中,而不是在命令处理器中,因为命令处理器属于应用层的一个类。

The command handler class offers a strong stepping stone in the way to achieve the Single Responsibility Principle (SRP) mentioned in a previous section.

命令处理器类是实现上一节提到的单一职责原则(SRP)的一个强有力的垫脚石。

A command handler receives a command and obtains a result from the aggregate that is used. The result should be either successful execution of the command, or an exception. In the case of an exception, the system state should be unchanged.

命令处理器接收一个命令,并从所使用的聚合中获取一个结果。这个结果要么是命令的成功执行,要么是一个异常。如果出现异常,系统状态应该保持不变。

The command handler usually takes the following steps:

命令处理器通常执行以下步骤:

  • It receives the command object, like a DTO (from the mediator or other infrastructure object).

    它接收命令对象,比如一个 DTO(来自中介者或其他基础设施对象)。

  • It validates that the command is valid (if not validated by the mediator).

    它验证该命令是否有效(如果中介者没有预先验证的话)。

  • It instantiates the aggregate root instance that is the target of the current command.

    它实例化当前命令所针对的聚合根实例。

  • It executes the method on the aggregate root instance, getting the required data from the command.

    它在聚合根实例上执行相应的方法,并从命令中获取所需的数据。

  • It persists the new state of the aggregate to its related database. This last operation is the actual transaction.

    它将聚合的新状态持久化到相关的数据库中。这最后一步才是真正的事务操作。

Typically, a command handler deals with a single aggregate driven by its aggregate root (root entity). If multiple aggregates should be impacted by the reception of a single command, you could use domain events to propagate states or actions across multiple aggregates.

通常,一个命令处理器处理由聚合根(根实体)驱动的单个聚合。如果单个命令的接收应该影响多个聚合,你可以使用领域事件(Domain Events)在多个聚合之间传播状态或动作。

The important point here is that when a command is being processed, all the domain logic should be inside the domain model (the aggregates), fully encapsulated and ready for unit testing. The command handler just acts as a way to get the domain model from the database, and as the final step, to tell the infrastructure layer (repositories) to persist the changes when the model is changed. The advantage of this approach is that you can refactor the domain logic in an isolated, fully encapsulated, rich, behavioral domain model without changing code in the application or infrastructure layers, which are the plumbing level (command handlers, Web API, repositories, etc.).

这里的关键点是,当命令被处理时,所有的领域逻辑都应该位于领域模型(聚合)内部,实现完全封装,并准备好进行单元测试。命令处理器仅仅是充当一种方式:从数据库中获取领域模型,并作为最后一步,当模型发生改变时,通知基础设施层(仓储)去持久化这些更改。这种方法的优势在于,你可以重构领域逻辑——在一个隔离的、完全封装的、行为丰富的领域模型中进行——而无需更改应用层或基础设施层(即管道层,如命令处理器、Web API、仓储等)的代码。

When command handlers get complex, with too much logic, that can be a code smell. Review them, and if you find domain logic, refactor the code to move that domain behavior to the methods of the domain objects (the aggregate root and child entity).

当命令处理器变得过于复杂、包含太多逻辑时,这可能就是一个代码异味(Code Smell)。请仔细检查它们,如果你发现了领域逻辑,请重构代码,将这些领域行为移到领域对象(聚合根和子实体)的方法中。

As an example of a command handler class, the following code shows the same CreateOrderCommandHandler class that you saw at the beginning of this chapter. In this case, it also highlights the Handle method and the operations with the domain model objects/aggregates.

作为一个命令处理器类的示例,以下代码展示了本章开头你看到的那个相同的 CreateOrderCommandHandler 类。在这种情况下,它还重点展示了 Handle 方法以及对领域模型对象/聚合的操作。

/// <summary>
/// 创建订单命令处理器。
/// 负责处理 CreateOrderCommand,执行创建订单的核心业务逻辑并保存数据。
/// </summary>
public class CreateOrderCommandHandler
    : IRequestHandler<CreateOrderCommand, bool> // 实现 MediatR 的请求处理接口,处理 CreateOrderCommand 并返回布尔值结果
{
    /// <summary>
    /// 订单仓储接口,用于订单数据的持久化操作。
    /// </summary>
    private readonly IOrderRepository _orderRepository;

    /// <summary>
    /// 身份服务接口,用于获取当前用户信息。
    /// </summary>
    private readonly IIdentityService _identityService;

    /// <summary>
    /// MediatR 中介者接口,用于进程内的消息/事件分发。
    /// </summary>
    private readonly IMediator _mediator;

    /// <summary>
    /// 订单集成事件服务接口,用于处理和发布跨微服务的集成事件。
    /// </summary>
    private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;

    /// <summary>
    /// 日志记录器接口,用于记录当前类的运行日志。
    /// </summary>
    private readonly ILogger<CreateOrderCommandHandler> _logger;

    /// <summary>
    /// 构造函数,通过依赖注入(DI)注入基础设施和持久化相关的仓储及服务。
    /// </summary>
    /// <param name="mediator">MediatR 中介者实例。</param>
    /// <param name="orderingIntegrationEventService">订单集成事件服务实例。</param>
    /// <param name="orderRepository">订单仓储实例。</param>
    /// <param name="identityService">身份服务实例。</param>
    /// <param name="logger">日志记录器实例。</param>
    public CreateOrderCommandHandler(IMediator mediator,
        IOrderingIntegrationEventService orderingIntegrationEventService,
        IOrderRepository orderRepository,
        IIdentityService identityService,
        ILogger<CreateOrderCommandHandler> logger)
    {
        // 使用空合并运算符进行参数校验,若为 null 则抛出 ArgumentNullException 异常
        _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository)); 
        _identityService = identityService ?? throw new ArgumentNullException(nameof(identityService)); 
        _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); 
        _orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService)); 
        _logger = logger ?? throw new ArgumentNullException(nameof(logger)); 
    }

    /// <summary>
    /// 处理创建订单的命令请求。
    /// </summary>
    /// <param name="message">接收到的创建订单命令对象。</param>
    /// <param name="cancellationToken">用于取消操作的令牌。</param>
    /// <returns>如果订单成功保存,则返回 true;否则返回 false。</returns>
    public async Task<bool> Handle(CreateOrderCommand message, CancellationToken cancellationToken)
    {
        // 添加一个集成事件,用于通知其他服务清空该用户的购物车
        var orderStartedIntegrationEvent = new OrderStartedIntegrationEvent(message.UserId); 
        
        // 将集成事件添加到事件总线中并持久化保存
        await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStartedIntegrationEvent); 

        // DDD 模式注释:必须通过聚合根(Order Aggregate-Root)的方法和构造函数来添加子实体和值对象。
        // 这样可以确保验证、不变量和业务逻辑得到执行,从而保证整个聚合内部的数据一致性。

        // 根据传入的地址信息创建地址值对象(Value Object)
        var address = new Address(message.Street, message.City, message.State, message.Country, message.ZipCode); 
        
        // 通过构造函数创建订单聚合根实例,并在构造时完成初始状态的业务规则校验
        var order = new Order(message.UserId, message.UserName, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration); 

        // 遍历命令中包含的订单项 DTO
        foreach (var item in message.OrderItems)
        {
            // 通过聚合根的 AddOrderItem 方法添加子实体,确保子实体的创建受聚合根控制
            order.AddOrderItem(item.ProductId, item.ProductName, item.UnitPrice, item.Discount, item.PictureUrl, item.Units); 
        }

        // 记录正在创建订单的信息日志(使用结构化日志格式输出订单详情)
        _logger.LogInformation("----- Creating Order - Order: {@Order}", order); 

        // 将构建好的订单聚合根添加到仓储中进行跟踪
        _orderRepository.Add(order); 

        // 调用工作单元(Unit of Work)的 SaveEntitiesAsync 方法,将更改统一提交到数据库
        return await _orderRepository.UnitOfWork
            .SaveEntitiesAsync(cancellationToken); 
    }
}

These are additional steps a command handler should take:

这些是命令处理器应该采取的额外步骤:

  • Use the command's data to operate with the aggregate root's methods and behavior.

    使用命令中的数据来操作聚合根的方法和其行为。

  • Internally within the domain objects, raise domain events while the transaction is executed, but that is transparent from a command handler point of view.

    在事务执行期间,在领域对象内部引发(raise)领域事件,但从命令处理器的角度来看,这一点是透明的(即命令处理器无需显式去处理领域事件的触发)。

  • If the aggregate's operation result is successful and after the transaction is finished, raise integration events. (These might also be raised by infrastructure classes like repositories.)

    如果聚合的操作结果成功,并且在事务完成后,引发集成事件。(这些集成事件也可能由仓储等基础设施类来引发。)

The Command process pipeline: how to trigger a command handler    命令处理管道:如何触发命令处理器

The next question is how to invoke a command handler. You could manually call it from each related ASP.NET Core controller. However, that approach would be too coupled and is not ideal.

下一个问题就是:如何调用命令处理器?你可以在每一个相关的 ASP.NET Core 控制器中手动调用它。但是,这种方法会导致代码耦合度过高,并不是理想的选择。

The other two main options, which are the recommended options, are:

另外两种主要的(也是推荐的)选项是:

  • Through an in-memory Mediator pattern artifact.

    通过内存中的 Mediator(中介者)模式 组件。

  • With an asynchronous message queue, in between controllers and handlers.

    在控制器和处理器之间使用 异步消息队列。

Use the Mediator pattern (in-memory) in the command pipeline    在命令管道中使用 Mediator 模式(内存中)

As shown in Figure 7-25, in a CQRS approach you use an intelligent mediator, similar to an in-memory bus, which is smart enough to redirect to the right command handler based on the type of the command or DTO being received. The single black arrows between components represent the dependencies between objects (in many cases, injected through DI) with their related interactions.

如图 7-25 所示,在 CQRS 方法中,你会使用一个“智能”的中介者(Mediator),它类似于一个内存中的总线(Bus)。它足够智能,能够根据接收到的命令类型或 DTO 类型,将其重定向到正确的命令处理器。组件之间的单个黑色箭头代表了对象之间的依赖关系(在大多数情况下,通过依赖注入 DI 实现)及其相关的交互。

Diagram showing a more detailed data flow from client to database.

Figure 7-25. Using the Mediator pattern in process in a single CQRS microservice

图 7-25. 在单个 CQRS 微服务中运行时的 Mediator 模式

The above diagram shows a zoom-in from image 7-24: the ASP.NET Core controller sends the command to MediatR's command pipeline, so they get to the appropriate handler.

上图是图 7-24 的放大细节:ASP.NET Core 控制器 将命令发送给 MediatR 的命令处理管道,从而让命令最终到达相应的处理器。

The reason that using the Mediator pattern makes sense is that in enterprise applications, the processing requests can get complicated. You want to be able to add an open number of cross-cutting concerns like logging, validations, audit, and security. In these cases, you can rely on a mediator pipeline (see Mediator pattern) to provide a means for these extra behaviors or cross-cutting concerns.

之所以采用 Mediator 模式是很有道理的,因为在企业级应用中,请求处理过程可能会变得非常复杂。你往往需要添加大量横切关注点(Cross-cutting concerns),例如日志记录、数据验证、审计追踪和安全性检查。在这种情况下,你可以依赖中介者管道来统一处理这些额外的行为或横切关注点。

A mediator is an object that encapsulates the "how" of this process: it coordinates execution based on state, the way a command handler is invoked, or the payload you provide to the handler. With a mediator component, you can apply cross-cutting concerns in a centralized and transparent way by applying decorators (or pipeline behaviors since MediatR 3). For more information, see the Decorator pattern.

Mediator(中介者) 是一个封装了该过程“执行方式”的对象:它根据状态、调用命令处理器的方式,或者你提供给处理器的有效载荷(Payload)来协调执行。通过使用中介者组件,你可以通过应用 装饰器(Decorators)(在 MediatR 3+ 版本中称为 管道行为 / Pipeline Behaviors)来以一种集中式且透明的方式应用横切关注点。欲了解更多信息,请参阅装饰器模式。

Decorators and behaviors are similar to Aspect Oriented Programming (AOP), only applied to a specific process pipeline managed by the mediator component. Aspects in AOP that implement cross-cutting concerns are applied based on aspect weavers injected at compilation time or based on object call interception. Both typical AOP approaches are sometimes said to work "like magic," because it is not easy to see how AOP does its work. When dealing with serious issues or bugs, AOP can be difficult to debug. On the other hand, these decorators/behaviors are explicit and applied only in the context of the mediator, so debugging is much more predictable and easy.

装饰器和行为类似于 面向切面编程(AOP),但它们仅应用于由中介者组件管理的特定处理管道。AOP 中实现横切关注点的“方面(Aspects)”通常是基于在编译时注入的“织入器(Weavers)”或基于对象调用拦截来实现的。这两种典型的 AOP 方法有时被认为像“魔法”一样工作,因为很难直观地看出 AOP 到底是如何运作的。当处理严重问题或 Bug 时,AOP 可能会很难调试。另一方面,这些装饰器/行为是明确的(Explicit),并且仅在中介者的上下文中应用,因此调试过程更加可预测且容易。

For example, in the eShopOnContainers ordering microservice, has an implementation of two sample behaviors, a LogBehavior class and a ValidatorBehavior class. The implementation of the behaviors is explained in the next section by showing how eShopOnContainers uses MediatR behaviors.

例如,在 eShopOnContainers 的 Ordering(下单)微服务中,实现了两个示例行为:LogBehavior 类和 ValidatorBehavior 类。下一节将通过展示 eShopOnContainers 如何使用 MediatR 行为,来详细解释这些行为的具体实现。

Use message queues (out-of-proc) in the command's pipeline    在命令管道中使用消息队列(进程外处理)

Another choice is to use asynchronous messages based on brokers or message queues, as shown in Figure 7-26. That option could also be combined with the mediator component right before the command handler.

另一种选择是使用基于代理(Brokers)或消息队列的异步消息,如图 7-26 所示。这种选项也可以与命令处理器之前的中介者(Mediator)组件结合使用。

Diagram showing the dataflow using an HA message queue.

Figure 7-26. Using message queues (out of the process and inter-process communication) with CQRS commands

图 7-26. 在 CQRS 命令中使用消息队列(进程外及进程间通信)

Command's pipeline can also be handled by a high availability message queue to deliver the commands to the appropriate handler. Using message queues to accept the commands can further complicate your command's pipeline, because you will probably need to split the pipeline into two processes connected through the external message queue. Still, it should be used if you need to have improved scalability and performance based on asynchronous messaging. Consider that in the case of Figure 7-26, the controller just posts the command message into the queue and returns. Then the command handlers process the messages at their own pace. That is a great benefit of queues: the message queue can act as a buffer in cases when hyper scalability is needed, such as for stocks or any other scenario with a high volume of ingress data.

命令管道也可以通过高可用消息队列来处理,以便将命令传递给相应的处理器。使用消息队列来接收命令可能会进一步增加命令管道的复杂性,因为你可能需要将管道拆分为两个通过外部消息队列连接的进程。尽管如此,如果你需要基于异步消息来显著提升系统的可扩展性和性能,就应该使用这种方式。试想一下图 7-26 的场景:控制器只需将命令消息发布到队列中并立即返回。然后,命令处理器可以按照自己的节奏来处理这些消息。这正是队列的一大优势:当需要极高的可扩展性时(例如股票交易场景,或任何其他需要处理海量入口数据的场景),消息队列可以充当缓冲区。

However, because of the asynchronous nature of message queues, you need to figure out how to communicate with the client application about the success or failure of the command's process. As a rule, you should never use "fire and forget" commands. Every business application needs to know if a command was processed successfully, or at least validated and accepted.

然而,由于消息队列的异步特性,你需要想办法告知客户端应用程序命令处理的成功或失败。作为一条原则,你永远不应该使用“即发即忘(fire and forget)”的命令。每一个业务应用程序都需要知道一个命令是否被成功处理,或者至少知道它是否被验证和接受了。

Thus, being able to respond to the client after validating a command message that was submitted to an asynchronous queue adds complexity to your system, as compared to an in-process command process that returns the operation's result after running the transaction. Using queues, you might need to return the result of the command process through other operation result messages, which will require additional components and custom communication in your system.

因此,与进程内命令处理(在运行事务后直接返回操作结果)相比,在将命令消息提交到异步队列并验证后还能向客户端做出响应,这会增加你系统的复杂性。如果使用队列,你可能需要通过其他的“操作结果消息”来返回命令处理的结果,这将需要你在系统中引入额外的组件和自定义通信机制。

Additionally, async commands are one-way commands, which in many cases might not be needed, as is explained in the following interesting exchange between Burtsev Alexey and Greg Young in an online conversation:

此外,异步命令本质上是单向命令,而在很多情况下可能并不需要这样做。正如 Burtsev Alexey 和 Greg Young 在一次在线对话中的精彩交流所解释的那样:

[Burtsev Alexey] I find lots of code where people use async command handling or one-way command messaging without any reason to do so (they are not doing some long operation, they are not executing external async code, they do not even cross-application boundary to be using message bus). Why do they introduce this unnecessary complexity? And actually, I haven't seen a CQRS code example with blocking command handlers so far, though it will work just fine in most cases.

[Burtsev Alexey] 我发现很多代码中,人们使用异步命令处理或单向命令消息传递,却没有任何必要(他们并不执行耗时操作,也不调用外部异步代码,甚至没有跨越应用边界使用消息总线)。他们为什么要引入这种不必要的复杂性?事实上,到目前为止,我还没有见过任何使用阻塞式命令处理器的CQRS代码示例,尽管在大多数情况下它仍然可以正常工作。

[Greg Young] [...] an asynchronous command doesn't exist; it's actually another event. If I must accept what you send me and raise an event if I disagree, it's no longer you telling me to do something [that is, it's not a command]. It's you telling me something has been done. This seems like a slight difference at first, but it has many implications.

[Greg Young] [...] 其实并不存在异步命令这个概念,实际上它是另一种的事件。如果我必须接受你发送的内容,并在我不同意时触发一个事件,那么这不再是你指示我做什么(即不再是命令),而是你告诉我某件事已经完成。乍看之下,这似乎只是细微的差别,但它却有诸多深远的影响。

Asynchronous commands greatly increase the complexity of a system, because there is no simple way to indicate failures. Therefore, asynchronous commands are not recommended other than when scaling requirements are needed or in special cases when communicating the internal microservices through messaging. In those cases, you must design a separate reporting and recovery system for failures.

异步命令会极大地增加系统的复杂性,因为没有简单的方法来指示失败。因此,除非有扩展性需求,或者在通过消息传递进行内部微服务通信的特殊情况下,否则不建议使用异步命令。在这些情况下,你必须为失败情况设计一套独立的报告和恢复系统。

In the initial version of eShopOnContainers, it was decided to use synchronous command processing, started from HTTP requests and driven by the Mediator pattern. That easily allows you to return the success or failure of the process, as in the CreateOrderCommandHandler implementation.

在 eShopOnContainers 的最初版本中,决定使用同步命令处理,从 HTTP 请求开始,并由 Mediator 模式驱动。这样可以非常轻松地返回处理过程的成功或失败结果,就像在 CreateOrderCommandHandler 的实现中一样。

In any case, this should be a decision based on your application's or microservice's business requirements.

无论如何,这应该是一个基于你的应用程序或微服务业务需求来做的决策。

Implement the command process pipeline with a mediator pattern (MediatR)    使用 Mediator 模式(MediatR)实现命令处理管道

As a sample implementation, this guide proposes using the in-process pipeline based on the Mediator pattern to drive command ingestion and route commands, in memory, to the right command handlers. The guide also proposes applying behaviors in order to separate cross-cutting concerns.

作为一个示例实现,本指南建议采用基于 Mediator 模式的进程内管道来驱动命令的接收,并在内存中将命令路由到正确的命令处理器。本指南还建议应用“行为(Behaviors)”,以便将横切关注点分离出来。

For implementation in .NET, there are multiple open-source libraries available that implement the Mediator pattern. The library used in this guide is the MediatR open-source library (created by Jimmy Bogard), but you could use another approach. MediatR is a small and simple library that allows you to process in-memory messages like a command, while applying decorators or behaviors.

在 .NET 中,有多个开源库实现了中介者模式。本指南中使用的库是 MediatR 开源库(由 Jimmy Bogard 创建),但你也可以选择其他方案。MediatR 是一个小型且简单的库,可让你像处理命令一样处理内存中的消息,并同时应用装饰器或行为。

Using the Mediator pattern helps you to reduce coupling and to isolate the concerns of the requested work, while automatically connecting to the handler that performs that work—in this case, to command handlers.

使用中介者模式有助于减少耦合,并将请求处理的各个关注点进行隔离,同时自动连接到执行该操作的处理器——在此情况下,即命令处理器。

Another good reason to use the Mediator pattern was explained by Jimmy Bogard when reviewing this guide:

Jimmy Bogard 在审阅本指南时,还解释了使用 Mediator 模式的另一个充分理由:

I think it might be worth mentioning testing here – it provides a nice consistent window into the behavior of your system. Request-in, response-out. We've found that aspect quite valuable in building consistently behaving tests.

我认为这里有必要提到测试——它能为系统行为提供一个清晰且一致的窗口。请求输入,响应输出。我们发现这一方面在构建行为一致的测试时非常有价值。

First, let's look at a sample WebAPI controller where you actually would use the mediator object. If you weren't using the mediator object, you'd need to inject all the dependencies for that controller, things like a logger object and others. Therefore, the constructor would be complicated. On the other hand, if you use the mediator object, the constructor of your controller can be a lot simpler, with just a few dependencies instead of many dependencies if you had one per cross-cutting operation, as in the following example:

首先,让我们来看一个示例 WebAPI 控制器,在这个控制器中你将实际使用 mediator 对象。如果你不使用 mediator 对象,你就需要为该控制器注入所有依赖项,比如日志记录器对象等。因此,构造函数会变得非常复杂。另一方面,如果你使用 mediator 对象,你的控制器构造函数可以变得非常简单,只需少量依赖项,而不需要像针对每个横切操作都引入一个依赖项那样,拥有许多依赖项,如下面的示例所示:

/// <summary>
/// 微服务控制器,用于处理与该微服务相关的 HTTP 请求。
/// </summary>
public class MyMicroserviceController : Controller
{
    /// <summary>
    /// 初始化 MyMicroserviceController 类的新实例。
    /// 通过构造函数注入所需的依赖项(中介者和查询接口)。
    /// </summary>
    /// <param name="mediator">MediatR 中介者接口,用于分发命令(Commands)和查询(Queries)。</param>
    /// <param name="microserviceQueries">自定义的微服务查询接口,用于执行特定的数据读取操作。</param>
    public MyMicroserviceController(IMediator mediator,
                                    IMyMicroserviceQueries microserviceQueries)
    {
        // TODO: 在此处将注入的依赖项赋值给类的私有字段或属性
        // ...
    }
}

你可以看到,Mediator 让 Web API 控制器的构造函数变得干净又精简。此外,在控制器的方法内部,向 mediator 对象发送命令的代码也几乎只需要一行:

/// <summary>
/// 定义路由为 "new",并指定该接口仅接受 HTTP POST 请求。
/// </summary>
[Route("new")]
[HttpPost]
/// <summary>
/// 执行通用的业务操作。接收前端传来的命令对象,通过 MediatR 分发处理,并根据结果返回相应的 HTTP 状态码。
/// </summary>
/// <param name="runOperationCommand">从 HTTP 请求体中反序列化得到的业务操作命令对象。</param>
/// <returns>如果业务操作成功返回 200 OK,否则返回 400 BadRequest。</returns>
public async Task<IActionResult> ExecuteBusinessOperation([FromBody]RunOpCommand runOperationCommand)
{
    // 通过 MediatR 异步发送命令,由对应的 CommandHandler 执行业务逻辑并返回布尔类型的结果
    var commandResult = await _mediator.SendAsync(runOperationCommand);

    // 根据命令执行结果进行三元运算:若为 true 则返回 Ok()(HTTP 200),若为 false 则返回 BadRequest()(HTTP 400)
    return commandResult ? (IActionResult)Ok() : (IActionResult)BadRequest();
}

Implement idempotent Commands    实现幂等命令

In eShopOnContainers, a more advanced example than the above is submitting a CreateOrderCommand object from the Ordering microservice. But since the Ordering business process is a bit more complex and, in our case, it actually starts in the Basket microservice, this action of submitting the CreateOrderCommand object is performed from an integration-event handler named UserCheckoutAcceptedIntegrationEventHandler instead of a simple WebAPI controller called from the client App as in the previous simpler example.

在 eShopOnContainers 中,有一个比上面例子更进阶的场景,那就是提交 CreateOrderCommand 对象到下单(Ordering)微服务。不过,由于下单的业务流程相对复杂一些,而且在我们的案例中,这个流程实际上是从购物车(Basket)微服务发起的,所以提交 CreateOrderCommand 对象这个动作,是由一个名为 UserCheckoutAcceptedIntegrationEventHandler 的集成事件处理器来执行的,而不是像前面那个简单的例子那样,由客户端 App 直接调用一个简单的 WebAPI 控制器。

Nevertheless, the action of submitting the Command to MediatR is pretty similar, as shown in the following code.

尽管如此,向 MediatR 提交命令(Command)的动作其实是非常相似的,如下面的代码所示。

// 实例化创建订单命令对象,将集成事件消息(eventMsg)中的购物篮商品、用户信息及支付信息作为参数传入
var createOrderCommand = new CreateOrderCommand(eventMsg.Basket.Items,
                                                eventMsg.UserId, eventMsg.City,
                                                eventMsg.Street, eventMsg.State,
                                                eventMsg.Country, eventMsg.ZipCode,
                                                eventMsg.CardNumber,
                                                eventMsg.CardHolderName,
                                                eventMsg.CardExpiration,
                                                eventMsg.CardSecurityNumber,
                                                eventMsg.CardTypeId);

// 将业务命令包装为幂等命令(IdentifiedCommand),并附带网络请求的唯一标识(RequestId),用于防止因重试等原因导致同一订单被重复处理
var requestCreateOrder = new IdentifiedCommand<CreateOrderCommand,bool>(createOrderCommand,
                                                                        eventMsg.RequestId);
// 通过 MediatR 中介者发送该幂等命令,等待异步执行结果(如果已存在相同ID的请求则不会再次执行业务逻辑)
result = await _mediator.Send(requestCreateOrder);

However, this case is also slightly more advanced because we're also implementing idempotent commands. The CreateOrderCommand process should be idempotent, so if the same message comes duplicated through the network, because of any reason, like retries, the same business order will be processed just once.

不过,这个案例也稍微进阶了一些,因为我们还实现了幂等命令。CreateOrderCommand 的处理过程应该是幂等的,所以如果同一条消息由于某种原因(比如网络重试)在网络中重复传输,同一个业务订单也只会被处理一次。

This is implemented by wrapping the business command (in this case CreateOrderCommand) and embedding it into a generic IdentifiedCommand, which is tracked by an ID of every message coming through the network that has to be idempotent.

这是通过将业务命令(在本例中是 CreateOrderCommand)进行包装来实现的,具体做法是将其嵌入到一个通用的 IdentifiedCommand 中。对于那些在网络中传输且需要保证幂等性的消息,系统会通过一个 ID 对它们进行追踪。

In the code below, you can see that the IdentifiedCommand is nothing more than a DTO with and ID plus the wrapped business command object.

在下面的代码中,你可以看到 IdentifiedCommand 其实不过就是一个包含 ID 以及被包装的业务命令对象的数据传输对象(DTO)而已。

/// <summary>
/// 标识命令包装器类。
/// 用于为原始命令附加一个唯一标识符(通常为 GUID),以支持幂等性处理或防止重复执行。
/// </summary>
/// <typeparam name="T">内部封装的具体命令类型,必须实现 IRequest&lt;R&gt; 接口。</typeparam>
/// <typeparam name="R">命令执行后的返回结果类型。</typeparam>
public class IdentifiedCommand<T, R> : IRequest<R>
    where T : IRequest<R> // 泛型约束:确保传入的命令类型 T 能够返回 R 类型的结果
{
    /// <summary>
    /// 获取被包装的原始命令对象。
    /// </summary>
    public T Command { get; }

    /// <summary>
    /// 获取该命令的唯一标识符(GUID),通常用于幂等性校验。
    /// </summary>
    public Guid Id { get; }

    /// <summary>
    /// 初始化标识命令的新实例。
    /// </summary>
    /// <param name="command">需要执行的原始命令对象。</param>
    /// <param name="id">分配给此命令的全局唯一标识符 (GUID)。</param>
    public IdentifiedCommand(T command, Guid id)
    {
        Command = command; // 将传入的具体命令赋值给内部属性
        Id = id;           // 记录用于追踪或防重的唯一 ID
    }
}

Then the CommandHandler for the IdentifiedCommand named IdentifiedCommandHandler.cs will basically check if the ID coming as part of the message already exists in a table. If it already exists, that command won't be processed again, so it behaves as an idempotent command. That infrastructure code is performed by the _requestManager.ExistAsync method call below.

然后,名为 IdentifiedCommandHandler 的 IdentifiedCommand 的命令处理器会检查消息中包含的 ID 是否已存在于某个表中。如果该 ID 已存在,则不会再次处理此命令,因此它表现为一个幂等命令。该基础设施代码由下方的 _requestManager.ExistAsync 方法调用实现。

// IdentifiedCommandHandler.cs - 幂等命令处理器,用于确保相同请求ID的命令只被处理一次

/// <summary>
/// 泛型幂等命令处理器。通过检查请求 ID 是否已存在来防止重复执行相同的业务命令。
/// </summary>
/// <typeparam name="T">内部业务命令的类型。</typeparam>
/// <typeparam name="R">命令执行的返回结果类型。</typeparam>
public class IdentifiedCommandHandler<T, R> : IRequestHandler<IdentifiedCommand<T, R>, R>
    where T : IRequest<R> // 约束泛型 T 必须实现 MediatR 的 IRequest<R> 接口
{
    /// <summary>
    /// MediatR 中介者实例,用于将内部业务命令分发给对应的 CommandHandler。
    /// </summary>
    private readonly IMediator _mediator;

    /// <summary>
    /// 请求管理器,用于记录和查询已处理的请求 ID,以实现幂等性。
    /// </summary>
    private readonly IRequestManager _requestManager;

    /// <summary>
    /// 日志记录器,用于记录命令的执行过程和结果。
    /// </summary>
    private readonly ILogger<IdentifiedCommandHandler<T, R>> _logger;

    /// <summary>
    /// 初始化 IdentifiedCommandHandler 的新实例。
    /// </summary>
    /// <param name="mediator">MediatR 中介者实例。</param>
    /// <param name="requestManager">请求管理器实例。</param>
    /// <param name="logger">日志记录器实例。</param>
    public IdentifiedCommandHandler(
        IMediator mediator,
        IRequestManager requestManager,
        ILogger<IdentifiedCommandHandler<T, R>> logger)
    {
        _mediator = mediator; // 注入中介者服务
        _requestManager = requestManager; // 注入请求管理服务
        _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); // 注入日志服务,若为空则抛出异常
    }

    /// <summary>
    /// 当检测到重复请求时,创建并返回默认的响应结果。
    /// 子类可以重写此方法以提供自定义的重复请求返回值。
    /// </summary>
    /// <returns>类型 R 的默认值。</returns>
    protected virtual R CreateResultForDuplicateRequest()
    {
        return default(R); // 返回默认值(如引用类型为 null,数值类型为 0)
    }

    /// <summary>
    /// 处理带有唯一标识的命令。首先验证该请求 ID 是否已被处理过;
    /// 如果未处理过,则记录该 ID 并将内部的业务命令分发给相应的处理器。
    /// </summary>
    /// <param name="message">包含原始业务命令和请求 ID 的 IdentifiedCommand 对象。</param>
    /// <param name="cancellationToken">取消操作的令牌。</param>
    /// <returns>内部命令的执行结果;如果发现相同 ID 的请求已存在,则返回默认值。</returns>
    public async Task<R> Handle(IdentifiedCommand<T, R> message, CancellationToken cancellationToken)
    {
        // 异步检查当前请求 ID 是否已经存在于数据库中(即是否被处理过)
        var alreadyExists = await _requestManager.ExistAsync(message.Id);

        // 如果请求已存在,说明是重复消息,直接返回默认结果以保证幂等性
        if (alreadyExists)
        {
            return CreateResultForDuplicateRequest(); // 调用虚方法返回重复请求的结果
        }
        else // 如果是全新的请求
        {
            // 先将该请求 ID 记录到存储中,防止并发情况下的重复处理
            await _requestManager.CreateRequestForCommandAsync<T>(message.Id);

            try
            {
                // 获取被包装的内部业务命令对象
                var command = message.Command; 
                // 获取命令类型的泛型名称,用于日志输出
                var commandName = command.GetGenericTypeName(); 
                
                // 声明变量以提取命令中的关键业务 ID 属性
                string idProperty = string.Empty; 
                string commandId = string.Empty; 

                // 根据不同的命令类型,提取对应的业务标识符用于日志记录
                switch (command)
                {
                    case CreateOrderCommand createOrderCommand: // 如果是创建订单命令
                        idProperty = nameof(createOrderCommand.UserId); // 提取用户 ID 作为标识属性名
                        commandId = createOrderCommand.UserId; // 提取具体的用户 ID 值
                        break;

                    case CancelOrderCommand cancelOrderCommand: // 如果是取消订单命令
                        idProperty = nameof(cancelOrderCommand.OrderNumber); // 提取订单号作为标识属性名
                        commandId = $"{cancelOrderCommand.OrderNumber}"; // 提取具体的订单号值
                        break;

                    case ShipOrderCommand shipOrderCommand: // 如果是发货订单命令
                        idProperty = nameof(shipOrderCommand.OrderNumber); // 提取订单号作为标识属性名
                        commandId = $"{shipOrderCommand.OrderNumber}"; // 提取具体的订单号值
                        break;

                    default: // 对于其他未知的命令类型
                        idProperty = "Id?"; // 设置默认的标识属性名
                        commandId = "n/a"; // 标记为不可用
                        break;
                }

                // 记录信息级别日志:正在发送命令及其相关参数
                _logger.LogInformation(
                    "----- Sending command: {CommandName} - {IdProperty}: {CommandId} ({@Command})",
                    commandName,   // 命令名称
                    idProperty,    // 业务 ID 的属性名
                    commandId,     // 业务 ID 的值
                    command);      // 完整的命令对象(结构化日志会序列化它)

                // 将嵌入的业务命令发送给 MediatR,由其路由到对应的 CommandHandler 执行真正的业务逻辑
                var result = await _mediator.Send(command, cancellationToken); 

                // 记录信息级别日志:命令执行完成及返回结果
                _logger.LogInformation(
                    "----- Command result: {@Result} - {CommandName} - {IdProperty}: {CommandId} ({@Command})",
                    result,        // 命令的执行结果
                    commandName,   // 命令名称
                    idProperty,    // 业务 ID 的属性名
                    commandId,     // 业务 ID 的值
                    command);      // 完整的命令对象

                // 返回业务命令的实际执行结果
                return result; 
            }
            catch // 捕获所有异常
            {
                // 如果命令执行过程中发生异常,静默失败并返回默认值
                // TODO: 在实际生产环境中,这里通常需要更完善的异常处理和日志记录机制
                return default(R); 
            }
        }
    }
}

Since the IdentifiedCommand acts like a business command's envelope, when the business command needs to be processed because it is not a repeated ID, then it takes that inner business command and resubmits it to Mediator, as in the last part of the code shown above when running _mediator.Send(message.Command), from the IdentifiedCommandHandler.cs.

由于 IdentifiedCommand 充当了业务命令的“信封”,当业务命令需要被处理时(也就是因为它的 ID 不是重复的),处理器会取出那个内部的“业务命令”,并把它重新提交给 Mediator。这就像上面展示的代码最后一部分,在 IdentifiedCommandHandler.cs 中运行 _mediator.Send(message.Command) 时所做的那样。

When doing that, it will link and run the business command handler, in this case, the CreateOrderCommandHandler, which is running transactions against the Ordering database, as shown in the following code.

当执行这一步时,它就会连接并运行业务命令处理器,在本例中就是 CreateOrderCommandHandler。该处理器会针对下单(Ordering)数据库执行事务操作,如下面的代码所示。

// CreateOrderCommandHandler.cs

/// <summary>
/// 创建订单命令处理器,负责处理创建订单的核心业务逻辑。
/// </summary>
public class CreateOrderCommandHandler
    : IRequestHandler<CreateOrderCommand, bool> // 实现 MediatR 的请求处理接口,接收 CreateOrderCommand 并返回布尔值结果
{
    /// <summary>
    /// 订单仓储接口,用于执行订单的持久化操作。
    /// </summary>
    private readonly IOrderRepository _orderRepository;

    /// <summary>
    /// 身份服务接口,用于获取当前用户的身份信息。
    /// </summary>
    private readonly IIdentityService _identityService;

    /// <summary>
    /// MediatR 中介者接口,用于在应用层内部发送命令或事件。
    /// </summary>
    private readonly IMediator _mediator;

    /// <summary>
    /// 订单集成事件服务接口,用于保存和发布跨微服务的集成事件。
    /// </summary>
    private readonly IOrderingIntegrationEventService _orderingIntegrationEventService;

    /// <summary>
    /// 日志记录器接口,用于记录当前处理器的运行日志。
    /// </summary>
    private readonly ILogger<CreateOrderCommandHandler> _logger;

    /// <summary>
    /// 构造函数:通过依赖注入(DI)初始化基础设施和持久化相关的仓储与服务。
    /// </summary>
    /// <param name="mediator">MediatR 中介者实例。</param>
    /// <param name="orderingIntegrationEventService">订单集成事件服务实例。</param>
    /// <param name="orderRepository">订单仓储实例。</param>
    /// <param name="identityService">身份服务实例。</param>
    /// <param name="logger">日志记录器实例。</param>
    public CreateOrderCommandHandler(IMediator mediator,
        IOrderingIntegrationEventService orderingIntegrationEventService,
        IOrderRepository orderRepository,
        IIdentityService identityService,
        ILogger<CreateOrderCommandHandler> logger)
    {
        // 校验并赋值订单仓储,若为 null 则抛出参数异常
        _orderRepository = orderRepository ?? throw new ArgumentNullException(nameof(orderRepository));
        // 校验并赋值身份服务,若为 null 则抛出参数异常
        _identityService = identityService ?? throw new ArgumentNullException(nameof(identityService));
        // 校验并赋值中介者,若为 null 则抛出参数异常
        _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
        // 校验并赋值集成事件服务,若为 null 则抛出参数异常
        _orderingIntegrationEventService = orderingIntegrationEventService ?? throw new ArgumentNullException(nameof(orderingIntegrationEventService));
        // 校验并赋值日志记录器,若为 null 则抛出参数异常
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    /// <summary>
    /// 处理创建订单的命令,包含生成集成事件、构建订单聚合根以及持久化数据等操作。
    /// </summary>
    /// <param name="message">创建订单命令对象,包含订单所需的各项输入数据。</param>
    /// <param name="cancellationToken">取消令牌,用于支持异步操作的取消。</param>
    /// <returns>返回一个 Task<bool>,表示订单是否成功创建并保存。</returns>
    public async Task<bool> Handle(CreateOrderCommand message, CancellationToken cancellationToken)
    {
        // 添加集成事件以清理购物车(通知其他服务用户已开始下单)
        var orderStartedIntegrationEvent = new OrderStartedIntegrationEvent(message.UserId);
        // 将集成事件添加到队列并保存到数据库,确保与本地事务一致
        await _orderingIntegrationEventService.AddAndSaveEventAsync(orderStartedIntegrationEvent);

        // DDD 模式注释:通过 Order 聚合根的方法和构造函数来添加子实体和值对象,
        // 以确保验证规则、不变量和业务逻辑能够在整个聚合中保持状态一致性。

        // 根据传入的地址信息创建 Address 值对象
        var address = new Address(message.Street, message.City, message.State, message.Country, message.ZipCode);
        // 通过构造函数创建 Order 聚合根实例,封装买家信息及支付卡信息
        var order = new Order(message.UserId, message.UserName, address, message.CardTypeId, message.CardNumber, message.CardSecurityNumber, message.CardHolderName, message.CardExpiration);

        // 遍历命令中的订单项 DTO,通过聚合根的 AddOrderItem 方法逐一添加商品项
        foreach (var item in message.OrderItems)
        {
            order.AddOrderItem(item.ProductId, item.ProductName, item.UnitPrice, item.Discount, item.PictureUrl, item.Units);
        }

        // 记录信息级别的日志,输出正在创建的订单详情
        _logger.LogInformation("----- Creating Order - Order: {@Order}", order);

        // 将构建完成的订单聚合根添加到仓储中(标记为待插入状态)
        _orderRepository.Add(order);

        // 调用工作单元(UnitOfWork)的 SaveEntitiesAsync 方法提交事务,并将更改持久化到数据库
        return await _orderRepository.UnitOfWork
            .SaveEntitiesAsync(cancellationToken);
    }
}

Register the types used by MediatR    注册 MediatR 所使用的类型

In order for MediatR to be aware of your command handler classes, you need to register the mediator classes and the command handler classes in your IoC container. By default, MediatR uses Autofac as the IoC container, but you can also use the built-in ASP.NET Core IoC container or any other container supported by MediatR.

为了让 MediatR 能够识别你的命令处理器(Command Handler)类,你需要将中介者(Mediator)类和命令处理器类注册到你的 IoC(控制反转)容器中。默认情况下,MediatR 使用 Autofac 作为 IoC 容器,但你也可以使用内置的 ASP.NET Core IoC 容器,或者任何其他 MediatR 支持的容器。

The following code shows how to register Mediator's types and commands when using Autofac modules.

下面的代码展示了使用 Autofac 模块(Autofac modules)时,如何注册 Mediator 的类型和命令。

/// <summary>
/// MediatR 的 Autofac 依赖注入模块。
/// 用于集中注册 Mediator 核心类型以及所有的命令处理器(Command Handlers)。
/// </summary>
public class MediatorModule : Autofac.Module // 继承自 Autofac 的 Module 基类,以便将相关注册逻辑封装在一起
{
    /// <summary>
    /// 重写 Load 方法,在此处定义和配置需要向 Autofac 容器注册的组件。
    /// </summary>
    /// <param name="builder">Autofac 容器构建器,用于执行具体的服务注册操作。</param>
    protected override void Load(ContainerBuilder builder)
    {
        // 自动扫描并注册 IMediator 所在程序集中的所有类型,并将其映射到它们所实现的接口上。
        // 这一步确保了 MediatR 的核心基础设施(如中介者本身)能够被正确解析。
        builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly)
            .AsImplementedInterfaces();

        // Register all the Command classes (they implement IRequestHandler)
        // in assembly holding the Commands
        // 注册包含命令处理器的程序集:扫描 CreateOrderCommand 所在的程序集,
        // 找出所有实现了 IRequestHandler<,> 泛型接口的具体类,并将它们作为对应的封闭泛型类型进行注册。
        // 这样 MediatR 在发送命令时就能自动找到并调用正确的 Handler。
        builder.RegisterAssemblyTypes(typeof(CreateOrderCommand).GetTypeInfo().Assembly)
                .AsClosedTypesOf(typeof(IRequestHandler<,>));

        // Other types registration
        //...
        // TODO: 在此处继续注册其他所需的业务类型或跨领域关注点行为(Behaviors)
    }
}

This is where "the magic happens" with MediatR.

这里正是 MediatR “施展魔法”的地方。

As each command handler implements the generic IRequestHandler<T> interface, when you register the assemblies using RegisteredAssemblyTypes method all the types marked as IRequestHandler also gets registered with their Commands. For example:

由于每个命令处理器(Command Handler)都实现了通用的 IRequestHandler<T> 接口,所以当你使用 RegisteredAssemblyTypes 方法去注册程序集时,所有标记了 IRequestHandler 的类型,都会连同它们对应的命令(Commands)一起被自动注册进去。举个例子:

/// <summary>
/// 创建订单命令处理器。
/// 负责处理 CreateOrderCommand,执行具体的业务逻辑并返回操作结果。
/// </summary>
public class CreateOrderCommandHandler
    : IRequestHandler<CreateOrderCommand, bool> // 实现 MediatR 的请求处理接口,指定处理的命令类型和返回值类型
{
}

That is the code that correlates commands with command handlers. The handler is just a simple class, but it inherits from RequestHandler<T>, where T is the command type, and MediatR makes sure it is invoked with the correct payload (the command).

这就是将命令与命令处理器关联起来的代码。 处理器只是一个简单的类,但它继承自 RequestHandler<T>,其中 T 就是命令的类型。而 MediatR 会确保它被调用时,传入的是正确的有效载荷(也就是对应的命令)。

Apply cross-cutting concerns when processing commands with the Behaviors in MediatR    在使用 MediatR 的 Behaviors 处理命令时应用横切关注点

There is one more thing: being able to apply cross-cutting concerns to the mediator pipeline. You can also see at the end of the Autofac registration module code how it registers a behavior type, specifically, a custom LoggingBehavior class and a ValidatorBehavior class. But you could add other custom behaviors, too.

还有一件事:那就是能够为中介者管道(mediator pipeline)应用横切关注点。你也可以在 Autofac 注册模块代码的末尾看到,它是如何注册行为类型的,具体来说,就是一个自定义的 LoggingBehavior 类和一个 ValidatorBehavior 类。当然,你也可以添加其他自定义的行为。

/// <summary>
/// MediatR 模块配置类,继承自 Autofac.Module。
/// 用于在依赖注入容器中注册 MediatR 相关的类型、命令处理程序以及管道行为(Behaviors)。
/// </summary>
public class MediatorModule : Autofac.Module
{
    /// <summary>
    /// 重写 Load 方法,在此处完成所有与 MediatR 相关的服务注册。
    /// </summary>
    /// <param name="builder">Autofac 容器构建器</param>
    protected override void Load(ContainerBuilder builder)
    {
        // 注册包含 IMediator 接口的程序集中的所有类型,并将其映射到其实现的接口上
        builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly)
            .AsImplementedInterfaces();

        // Register all the Command classes (they implement IRequestHandler)
        // in assembly holding the Commands
        // 注册包含 CreateOrderCommand 的程序集中所有的命令处理程序类。
        // 这些类实现了泛型接口 IRequestHandler<,>,通过 AsClosedTypesOf 将它们作为对应的封闭泛型类型进行注册
        builder.RegisterAssemblyTypes(
                              typeof(CreateOrderCommand).GetTypeInfo().Assembly).
                                   AsClosedTypesOf(typeof(IRequestHandler<,>));
        
        // Other types registration
        //...
        // 其他类型的注册逻辑(省略)

        // 注册通用的日志记录行为(LoggingBehavior),将其作为管道行为(IPipelineBehavior)注入到 MediatR 的处理管线中
        builder.RegisterGeneric(typeof(LoggingBehavior<,>)).
                                                   As(typeof(IPipelineBehavior<,>));
        
        // 注册通用的验证行为(ValidatorBehavior),同样作为管道行为注入,用于在命令执行前进行数据校验
        builder.RegisterGeneric(typeof(ValidatorBehavior<,>)).
                                                   As(typeof(IPipelineBehavior<,>));
    }
}

That LoggingBehavior class can be implemented as the following code, which logs information about the command handler being executed and whether it was successful or not.

LoggingBehavior 类可以按照下面的代码来实现,它会记录关于正在执行的命令处理器的信息,以及该命令最终执行成功还是失败。

/// <summary>
/// 日志记录管道行为,用于在 MediatR 请求处理前后自动记录日志信息。
/// </summary>
/// <typeparam name="TRequest">请求类型</typeparam>
/// <typeparam name="TResponse">响应类型</typeparam>
public class LoggingBehavior<TRequest, TResponse>
         : IPipelineBehavior<TRequest, TResponse> // 实现 MediatR 的管道行为接口
{
    /// <summary>
    /// 泛型日志记录器实例。
    /// </summary>
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

    /// <summary>
    /// 构造函数,通过依赖注入获取日志记录器。
    /// </summary>
    /// <param name="logger">ILogger 日志记录器实例</param>
    public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger) =>
                                                                  _logger = logger; // 将注入的日志记录器赋值给私有字段

    /// <summary>
    /// 处理请求的管道方法,在执行实际业务逻辑的前后分别记录日志。
    /// </summary>
    /// <param name="request">当前传入的请求对象</param>
    /// <param name="next">指向管道中下一个行为或最终处理程序的委托</param>
    /// <returns>下游处理程序返回的响应结果</returns>
    public async Task<TResponse> Handle(TRequest request,
                                        RequestHandlerDelegate<TResponse> next)
    {
        // 记录开始处理请求的日志,输出当前请求类型的名称
        _logger.LogInformation($"Handling {typeof(TRequest).Name}");

        // 调用 next() 将请求传递给管道中的下一个行为(或最终的业务处理程序),并等待其执行完成
        var response = await next();

        // 记录请求处理完成的日志,输出当前响应类型的名称
        _logger.LogInformation($"Handled {typeof(TResponse).Name}");

        // 返回下游处理程序生成的响应结果
        return response;
    }
}

Just by implementing this behavior class and by registering it in the pipeline (in the MediatorModule above), all the commands processed through MediatR will be logging information about the execution.

只需实现这个行为(Behavior)类,并将其注册到管道中(在上面的 MediatorModule 里),所有通过 MediatR 处理的命令就都会自动记录关于执行过程的信息了。

The eShopOnContainers ordering microservice also applies a second behavior for basic validations, the ValidatorBehavior class that relies on the FluentValidation library, as shown in the following code:

eShopOnContainers 的下单(Ordering)微服务还应用了第二个行为来进行基础验证,也就是 ValidatorBehavior 类,它依赖于 FluentValidation 库,如下面的代码所示:

/// <summary>
/// 验证行为管道类,用于在命令执行前自动进行数据校验。
/// </summary>
/// <typeparam name="TRequest">请求(命令)的类型。</typeparam>
/// <typeparam name="TResponse">响应结果的类型。</typeparam>
public class ValidatorBehavior<TRequest, TResponse>
         : IPipelineBehavior<TRequest, TResponse> // 实现 MediatR 的管道行为接口,以便在请求处理管线中拦截请求
{
    /// <summary>
    /// 存储当前请求对应的所有验证器实例。
    /// </summary>
    private readonly IValidator<TRequest>[] _validators;

    /// <summary>
    /// 构造函数,通过依赖注入获取适用于当前请求类型的验证器数组。
    /// </summary>
    /// <param name="validators">包含一个或多个 FluentValidation 验证器的数组。</param>
    public ValidatorBehavior(IValidator<TRequest>[] validators) =>
                                                         _validators = validators; // 将注入的验证器赋值给私有字段

    /// <summary>
    /// 管道处理方法,在执行实际的命令处理程序之前运行所有的验证逻辑。
    /// </summary>
    /// <param name="request">传入的请求对象。</param>
    /// <param name="next">指向管道中下一个委托(即实际的命令处理程序)的回调方法。</param>
    /// <returns>如果验证通过,则返回实际处理程序的响应结果。</returns>
    public async Task<TResponse> Handle(TRequest request,
                                        RequestHandlerDelegate<TResponse> next)
    {
        // 遍历所有注入的验证器对当前请求进行验证,收集所有的错误信息并过滤掉空值
        var failures = _validators
            .Select(v => v.Validate(request)) // 依次调用每个验证器的 Validate 方法
            .SelectMany(result => result.Errors) // 将所有验证结果中的错误集合展平为一个列表
            .Where(error => error != null) // 过滤掉可能存在的空错误项
            .ToList(); // 转换为 List 以便后续判断和使用

        // 如果存在任何验证失败的情况
        if (failures.Any())
        {
            // 抛出自定义的领域异常,并将验证错误信息作为内部异常传递出去,阻止命令继续执行
            throw new OrderingDomainException(
                $"Command Validation Errors for type {typeof(TRequest).Name}", // 拼接包含请求类型的异常消息
                        new ValidationException("Validation exception", failures)); // 包装底层的验证异常及具体的错误列表
        }

        // 验证全部通过后,调用管道中的下一个处理器(即执行业务逻辑)并等待其完成
        var response = await next();
        
        // 返回业务逻辑处理的结果
        return response;
    }
}

Here the behavior is raising an exception if validation fails, but you could also return a result object, containing the command result if it succeeded or the validation messages in case it didn't. This would probably make it easier to display validation results to the user.

在这里,如果验证失败,该行为(Behavior)会直接抛出一个异常。不过,你也可以选择返回一个结果对象:如果验证成功,就包含命令的执行结果;如果失败了,就包含具体的验证错误信息。这样做可能会让用户端更方便地展示验证结果。

Then, based on the FluentValidation library, you would create validation for the data passed with CreateOrderCommand, as in the following code:

然后,基于 FluentValidation 库,你就可以为随 CreateOrderCommand 传递的数据创建具体的验证规则了,如下面的代码所示:

/// <summary>
/// 创建订单命令的验证器类。
/// 继承自 AbstractValidator,用于在执行创建订单逻辑前对输入数据进行合法性校验。
/// </summary>
public class CreateOrderCommandValidator : AbstractValidator<CreateOrderCommand>
{
    /// <summary>
    /// 构造函数,定义针对 CreateOrderCommand 属性的各项验证规则。
    /// </summary>
    public CreateOrderCommandValidator()
    {
        // 验证城市字段不能为空
        RuleFor(command => command.City).NotEmpty();
        
        // 验证街道地址字段不能为空
        RuleFor(command => command.Street).NotEmpty();
        
        // 验证州/省字段不能为空
        RuleFor(command => command.State).NotEmpty();
        
        // 验证国家字段不能为空
        RuleFor(command => command.Country).NotEmpty();
        
        // 验证邮政编码字段不能为空
        RuleFor(command => command.ZipCode).NotEmpty();
        
        // 验证信用卡号不能为空,且长度必须在 12 到 19 位之间
        RuleFor(command => command.CardNumber).NotEmpty().Length(12, 19);
        
        // 验证持卡人姓名不能为空
        RuleFor(command => command.CardHolderName).NotEmpty();
        
        // 验证信用卡过期时间不能为空,并且必须通过自定义方法 BeValidExpirationDate 进行有效性检查;若失败则返回指定的错误提示信息
        RuleFor(command => command.CardExpiration).NotEmpty().Must(BeValidExpirationDate).WithMessage("Please specify a valid card expiration date");
        
        // 验证信用卡安全码不能为空,且长度必须为 3 位
        RuleFor(command => command.CardSecurityNumber).NotEmpty().Length(3);
        
        // 验证信用卡类型 ID 不能为空
        RuleFor(command => command.CardTypeId).NotEmpty();
        
        // 验证订单项集合必须包含至少一个有效项(通过自定义方法 ContainOrderItems 判断);若为空则返回指定的错误提示信息
        RuleFor(command => command.OrderItems).Must(ContainOrderItems).WithMessage("No order items found");
    }

    /// <summary>
    /// 自定义验证方法:检查信用卡过期日期是否有效。
    /// </summary>
    /// <param name="dateTime">待验证的信用卡过期日期。</param>
    /// <returns>如果传入的日期大于或等于当前 UTC 时间,则返回 true;否则返回 false。</returns>
    private bool BeValidExpirationDate(DateTime dateTime)
    {
        return dateTime >= DateTime.UtcNow;
    }

    /// <summary>
    /// 自定义验证方法:检查订单中是否包含有效的订单项。
    /// </summary>
    /// <param name="orderItems">待验证的订单项集合。</param>
    /// <returns>如果集合中包含至少一个元素,则返回 true;否则返回 false。</returns>
    private bool ContainOrderItems(IEnumerable<OrderItemDTO> orderItems)
    {
        return orderItems.Any();
    }
}

You could create additional validations. This is a very clean and elegant way to implement your command validations.

你可以继续创建更多的验证规则。这是一种非常简洁且优雅的实现命令验证的方式。

In a similar way, you could implement other behaviors for additional aspects or cross-cutting concerns that you want to apply to commands when handling them.

以类似的方式,你还可以实现其他的行为(Behaviors),以便在处理命令时,为它们添加你需要的其他额外功能或横切关注点。

posted @ 2026-05-19 12:03  菜鸟吊思  阅读(9)  评论(0)    收藏  举报