在 CQRS 微服务中实现读取/查询

For reads/queries, the ordering microservice from the eShopOnContainers reference application implements the queries independently from the DDD model and transactional area. This implementation was done primarily because the demands for queries and for transactions are drastically different. Writes execute transactions that must be compliant with the domain logic. Queries, on the other hand, are idempotent and can be segregated from the domain rules.

在查询方面,eShopOnContainers 参考应用中的订购微服务将查询的实现独立于 DDD 模型和事务区域之外。这样做的主要原因是查询和事务的需求截然不同。写入操作需要执行必须符合领域逻辑的事务,而查询操作则是幂等的,可以与领域规则分离开来。

The approach is simple, as shown in Figure 7-3. The API interface is implemented by the Web API controllers using any infrastructure, such as a micro Object Relational Mapper (ORM) like Dapper, and returning dynamic ViewModels depending on the needs of the UI applications.

这一方法非常简单,如图 7-3 所示。API 接口由 Web API 控制器实现,它使用任意基础设施(例如像 Dapper 这样的微对象关系映射器(Micro-ORM)),并根据 UI 应用程序的需求返回动态的 ViewModel。

Diagram showing high-level queries-side in simplified CQRS.

Figure 7-3. The simplest approach for queries in a CQRS microservice

图 7-3 在简化 CQRS 的微服务中,查询端的最简方法

The simplest approach for the queries-side in a simplified CQRS approach can be implemented by querying the database with a Micro-ORM like Dapper, returning dynamic ViewModels. The query definitions query the database and return a dynamic ViewModel built on the fly for each query. Since the queries are idempotent, they won't change the data no matter how many times you run a query. Therefore, you don't need to be restricted by any DDD pattern used in the transactional side, like aggregates and other patterns, and that is why queries are separated from the transactional area. You query the database for the data that the UI needs and return a dynamic ViewModel that does not need to be statically defined anywhere (no classes for the ViewModels) except in the SQL statements themselves.

在简化版 CQRS 方法中,查询端的最简实现方式是使用像 Dapper 这样的微 ORM 查询数据库,并返回动态 ViewModel。查询定义直接查询数据库,并返回根据每个查询动态构建的 ViewModel。由于查询是幂等的,无论执行多少次都不会改变数据。因此,你不必受限于事务端使用的任何 DDD 模式(如聚合模式),这也是为什么查询要与事务区域分离的原因。你可以直接查询 UI 所需的数据,并返回一个无需在任何地方(除了 SQL 语句本身)静态定义的动态 ViewModel。

Since this approach is simple, the code required for the queries side (such as code using a micro ORM like Dapper) can be implemented within the same Web API project. Figure 7-4 shows this approach. The queries are defined in the Ordering.API microservice project within the eShopOnContainers solution.

由于这种方法非常简单,查询端所需的代码(例如使用微 ORM 的代码)可以直接在同一个 Web API 项目中实现。图 7-4 展示了这种方法,其中查询是在 eShopOnContainers 解决方案的 Ordering.API 微服务项目中定义的。

Screenshot of the Ordering.API project's Queries folder.

Figure 7-4. Queries in the Ordering microservice in eShopOnContainers

图 7-4 eShopOnContainers 中订购微服务的查询

Use ViewModels specifically made for client apps, independent from domain model constraints    针对客户端应用使用独立于领域模型约束的 ViewModel

Since the queries are performed to obtain the data needed by the client applications, the returned type can be specifically made for the clients, based on the data returned by the queries. These models, or Data Transfer Objects (DTOs), are called ViewModels.

由于查询的目的是获取客户端应用程序所需的数据,因此返回的类型可以专门针对客户端定制,其基础是查询返回的数据。这些模型或数据传输对象(DTO)被称为 ViewModel。

The returned data (ViewModel) can be the result of joining data from multiple entities or tables in the database, or even across multiple aggregates defined in the domain model for the transactional area. In this case, because you are creating queries independent of the domain model, the aggregates boundaries and constraints are ignored and you're free to query any table and column you might need. This approach provides great flexibility and productivity for the developers creating or updating the queries.

返回的数据(ViewModel)可以是数据库中多个实体或表连接的结果,甚至可以跨越事务区域领域模型中定义的多个聚合。在这种情况下,因为你正在创建独立于领域模型的查询,所以聚合的边界和约束被忽略,你可以自由地查询所需的任何表和列。这种方法为开发和更新查询的开发人员提供了极大的灵活性和生产力。

The ViewModels can be static types defined in classes (as is implemented in the ordering microservice). Or they can be created dynamically based on the queries performed, which is agile for developers.

ViewModel 可以是在类中定义的静态类型(如订购微服务中的实现方式),也可以根据执行的查询动态创建,这对开发人员来说非常敏捷。

Use Dapper as a micro ORM to perform queries    使用 Dapper 作为微 ORM 执行查询

You can use any micro ORM, Entity Framework Core, or even plain ADO.NET for querying. In the sample application, Dapper was selected for the ordering microservice in eShopOnContainers as a good example of a popular micro ORM. It can run plain SQL queries with great performance, because it's a light framework. Using Dapper, you can write a SQL query that can access and join multiple tables.

对于查询,你可以使用任何微 ORM、Entity Framework Core,甚至是原生的 ADO.NET。在示例应用程序中,Dapper 被选为 eShopOnContainers 中订购微服务的 ORM,它是流行的微 ORM 的典范。它可以通过原生 SQL 查询实现高性能,因为它是一个轻量级框架。使用 Dapper,你可以编写直接访问和连接多个表的 SQL 查询。

Dapper is an open-source project (original created by Sam Saffron), and is part of the building blocks used in Stack Overflow. To use Dapper, you just need to install it through the Dapper NuGet package, as shown in the following figure:

Dapper 是一个开源项目(最初由 Sam Saffron 创建),也是 Stack Overflow 使用的基础组件之一。要使用 Dapper,你只需通过 NuGet 包安装即可,如下图所示:

Screenshot of the Dapper package in the NuGet packages view.

You also need to add a using directive so your code has access to the Dapper extension methods.

你还需要添加一个 using 指令,以便代码可以访问 Dapper 的扩展方法。

When you use Dapper in your code, you directly use the SqlConnection class available in the Microsoft.Data.SqlClient namespace. Through the QueryAsync method and other extension methods that extend the SqlConnection class, you can run queries in a straightforward and performant way.

在代码中使用 Dapper 时,你直接使用 Microsoft.Data.SqlClient 命名空间中的 SqlConnection 类。通过 QueryAsync 方法以及扩展 SqlConnection 类的其他扩展方法,你可以以一种直接且高效的方式运行查询。

Dynamic versus static ViewModels    动态 ViewModel 与静态 ViewModel

When returning ViewModels from the server-side to client apps, you can think about those ViewModels as DTOs (Data Transfer Objects) that can be different to the internal domain entities of your entity model because the ViewModels hold the data the way the client app needs. Therefore, in many cases, you can aggregate data coming from multiple domain entities and compose the ViewModels precisely according to how the client app needs that data.

当将 ViewModel 从服务器端返回给客户端应用时,你可以将这些 ViewModel 视为 DTO(数据传输对象),它们可能与实体模型中的内部领域实体不同,因为 ViewModel 持有的数据是客户端应用所需的形式。因此,在许多情况下,你可以聚合来自多个领域实体的数据,并根据客户端应用需要数据的方式精确地组合 ViewModel。

Those ViewModels or DTOs can be defined explicitly (as data holder classes), like the OrderSummary class shown in a later code snippet. Or, you could just return dynamic ViewModels or dynamic DTOs based on the attributes returned by your queries as a dynamic type.

这些 ViewModel 或 DTO 可以在类中显式定义(作为数据持有者类),就像后面代码片段中展示的 OrderSummary 类一样。或者,你也可以根据查询返回的属性,仅返回动态 ViewModel 或动态 DTO。

ViewModel as dynamic type    将 ViewModel 作为动态类型

As shown in the following code, a ViewModel can be directly returned by the queries by just returning a dynamic type that internally is based on the attributes returned by a query. That means that the subset of attributes to be returned is based on the query itself. Therefore, if you add a new column to the query or join, that data is dynamically added to the returned ViewModel.

如下面的代码所示,ViewModel 可以通过仅返回动态类型直接由查询返回,该动态类型在内部基于查询返回的属性。这意味着返回的属性子集是基于查询本身的。因此,如果你向查询中添加新列或连接,该数据将动态添加到返回的 ViewModel 中。

using Dapper; // 引入 Dapper 轻量级 ORM 库,用于简化数据库访问操作
using Microsoft.Extensions.Configuration; // 引入配置扩展库,用于读取应用配置文件
using System.Data.SqlClient; // 引入 SQL Server 数据提供程序,用于连接和操作数据库
using System.Threading.Tasks; // 引入异步任务支持,用于实现异步编程模型
using System.Dynamic; // 引入动态类型支持,用于处理返回的动态对象
using System.Collections.Generic; // 引入泛型集合命名空间,支持 IEnumerable 等接口

/// <summary>
/// 订单查询服务类。
/// <para>封装了与订单相关的数据访问逻辑,实现了 IOrderQueries 接口。</para>
/// </summary>
public class OrderQueries : IOrderQueries
{
    /// <summary>
    /// 异步获取所有订单列表及其汇总信息。
    /// </summary>
    /// <returns>包含订单编号、下单日期、订单状态及总金额等信息的动态对象集合。</returns>
    public async Task<IEnumerable<dynamic>> GetOrdersAsync()
    {
        // 使用传入的连接字符串创建一个新的 SQL Server 数据库连接实例
        using (var connection = new SqlConnection(_connectionString))
        {
            // 打开数据库连接以准备执行查询
            connection.Open();

            // 执行异步 SQL 查询,将多个表进行左连接并按订单分组聚合计算总金额,最后映射为动态对象返回
            return await connection.QueryAsync<dynamic>(
                @"SELECT o.[Id] as ordernumber,       -- 订单ID作为订单号
                  o.[OrderDate] as [date],           -- 订单日期
                  os.[Name] as [status],             -- 订单状态名称
                  SUM(oi.units * oi.unitprice) as total -- 计算该订单下所有商品的总价(数量乘以单价)
                  FROM [ordering].[Orders] o         -- 主表:订单表
                  LEFT JOIN [ordering].[orderitems] oi ON o.Id = oi.orderid     -- 左连接:订单项明细表
                  LEFT JOIN [ordering].[orderstatus] os ON o.OrderStatusId = os.Id -- 左连接:订单状态字典表
                  GROUP BY o.[Id], o.[OrderDate], os.[Name]"); -- 按订单维度进行分组聚合
        }
    }
}

The important point is that by using a dynamic type, the returned collection of data is dynamically assembled as the ViewModel.

重点在于,通过使用动态类型,返回的数据集合会被动态地组装成 ViewModel。

Pros: This approach reduces the need to modify static ViewModel classes whenever you update the SQL sentence of a query, making this design approach agile when coding, straightforward, and quick to evolve in regard to future changes.

优点:这种方法减少了每次更新查询的 SQL 语句时都需要去修改静态 ViewModel 类的麻烦,使得这种设计在编码时非常敏捷、直接,并且能快速适应未来的变化。

Cons: In the long term, dynamic types can negatively impact the clarity and the compatibility of a service with client apps. In addition, middleware software like Swashbuckle cannot provide the same level of documentation on returned types if using dynamic types.

缺点:从长远来看,动态类型可能会对服务的清晰度以及与客户端应用的兼容性产生负面影响。此外,如果使用动态类型,像 Swashbuckle 这样的中间件软件也无法就返回的类型提供同等水平的文档说明。

ViewModel as predefined DTO classes    将 ViewModel 作为预定义的 DTO 类

Pros: Having static, predefined ViewModel classes, like "contracts" based on explicit DTO classes, is definitely better for public APIs but also for long-term microservices, even if they are only used by the same application.

优点:拥有静态的、预定义的 ViewModel 类(即基于显式 DTO 类的“契约”),不仅对公共 API 绝对更有利,即使微服务仅供同一个应用内部使用,从长远来看也更好。

If you want to specify response types for Swagger, you need to use explicit DTO classes as the return type. Therefore, predefined DTO classes allow you to offer richer information from Swagger. That improves the API documentation and compatibility when consuming an API.

如果你想为 Swagger 指定响应类型,就需要使用显式的 DTO 类作为返回类型。因此,预定义的 DTO 类可以让你通过 Swagger 提供更丰富的信息。这在消费 API 时,能极大地改善 API 文档的完善度和兼容性。

Cons: As mentioned earlier, when updating the code, it takes some more steps to update the DTO classes.

缺点:如前所述,在更新代码时,更新 DTO 类需要多走几个步骤。

Tip based on our experience: In the queries implemented at the Ordering microservice in eShopOnContainers, we started developing by using dynamic ViewModels as it was straightforward and agile on the early development stages. But, once the development was stabilized, we chose to refactor the APIs and use static or pre-defined DTOs for the ViewModels, because it is clearer for the microservice's consumers to know explicit DTO types, used as "contracts".

基于我们经验的建议:在 eShopOnContainers 的订购微服务中实现查询时,我们最初开发阶段使用的是动态 ViewModel,因为这在早期开发中非常直接且敏捷。但是,一旦开发进入稳定期,我们选择重构 API 并改用静态或预定义的 DTO 类作为 ViewModel,因为这样对于微服务的消费者来说,明确知道使用了哪些 DTO 类型作为“契约”会更加清晰。

In the following example, you can see how the query is returning data by using an explicit ViewModel DTO class: the OrderSummary class.

在下面的示例中,你可以看到查询是如何通过使用一个显式的 ViewModel DTO 类(即 OrderSummary 类)来返回数据的。

using Dapper; // 引入 Dapper 库,用于简化数据库操作
using Microsoft.Extensions.Configuration; // 引入配置扩展库
using System.Data.SqlClient; // 引入 SQL Server 数据提供程序
using System.Threading.Tasks; // 引入异步任务支持
using System.Dynamic; // 引入动态对象支持
using System.Collections.Generic; // 引入泛型集合支持

/// <summary>
/// 订单查询服务类,实现 IOrderQueries 接口。
/// 负责从数据库中检索订单相关的数据。
/// </summary>
public class OrderQueries : IOrderQueries
{
    /// <summary>
    /// 异步获取所有订单的摘要信息列表。
    /// </summary>
    /// <returns>包含订单摘要信息的可枚举集合。</returns>
    public async Task<IEnumerable<OrderSummary>> GetOrdersAsync()
    {
        // 使用连接字符串创建并初始化一个新的数据库连接
        using (var connection = new SqlConnection(_connectionString))
        {
            // 打开与数据库的连接
            connection.Open();

            // 执行 SQL 查询并异步映射为 OrderSummary 对象集合
            return await connection.QueryAsync<OrderSummary>(
                  @"SELECT o.[Id] as ordernumber,       -- 查询订单编号
                  o.[OrderDate] as [date],              -- 查询订单日期
                  os.[Name] as [status],                -- 查询订单状态名称
                  SUM(oi.units*oi.unitprice) as total   -- 计算订单总金额(数量乘以单价求和)
                  FROM [ordering].[Orders] o            -- 主表:订单表
                  LEFT JOIN[ordering].[orderitems] oi ON  o.Id = oi.orderid      -- 左连接:订单明细表
                  LEFT JOIN[ordering].[orderstatus] os on o.OrderStatusId = os.Id -- 左连接:订单状态表
                  GROUP BY o.[Id], o.[OrderDate], os.[Name] -- 按订单ID、日期和状态进行分组
                  ORDER BY o.[Id]");                    -- 按订单ID升序排列
        } // 离开 using 块时自动释放数据库连接资源
    }
}

Describe response types of Web APIs    描述 Web API 的响应类型

Developers consuming web APIs and microservices are most concerned with what is returned—specifically response types and error codes (if not standard). The response types are handled in the XML comments and data annotations.

对于调用 Web API 和微服务的开发者来说,他们最关心的就是接口返回的内容——具体来说就是响应类型和错误代码(如果不是标准状态码的话)。这些响应类型通常是通过 XML 注释和数据注解(data annotations)来处理的。

Without proper documentation in the Swagger UI, the consumer lacks knowledge of what types are being returned or what HTTP codes can be returned. That problem is fixed by adding the Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute, so Swashbuckle can generate richer information about the API return model and values, as shown in the following code:

如果在 Swagger UI 中没有合适的文档说明,调用者就无法知道接口到底会返回什么类型的数据,或者可能返回哪些 HTTP 状态码。通过在代码中添加 Microsoft.AspNetCore.Mvc.ProducesResponseTypeAttribute 特性,就可以完美解决这个问题。这样一来,Swashbuckle 就能生成关于 API 返回模型和数值的更丰富的信息,如下面的代码所示:

namespace Microsoft.eShopOnContainers.Services.Ordering.API.Controllers
{
    // 设置该控制器的 API 路由模板,并应用授权特性(要求用户必须通过身份验证)
    [Route("api/v1/[controller]")]
    [Authorize]
    public class OrdersController : Controller
    {
        // ... 其他附加代码 ...

        // 定义获取订单列表的路由(空字符串表示匹配控制器基础路由),仅允许 HTTP GET 请求
        [Route("")]
        [HttpGet]
        // 声明当接口返回成功状态码(200)时,将返回 IEnumerable<OrderSummary> 类型的数据
        [ProducesResponseType(typeof(IEnumerable<OrderSummary>), (int)HttpStatusCode.OK)]
        /// <summary>
        /// 异步获取当前已登录用户的订单列表。
        /// </summary>
        /// <returns>包含当前用户所有订单摘要信息的 IActionResult 响应结果。</returns>
        public async Task<IActionResult> GetOrders()
        {
            // 从身份服务中提取当前登录用户的唯一标识符(User ID)
            var userid = _identityService.GetUserIdentity();
            
            // 调用订单查询服务,根据解析出的用户 GUID 异步获取该用户名下的所有订单
            var orders = await _orderQueries.GetOrdersFromUserAsync(Guid.Parse(userid));
            
            // 返回包含订单数据的 HTTP 200 OK 响应
            return Ok(orders);
        }
    }
}

However, the ProducesResponseType attribute cannot use dynamic as a type but requires to use explicit types, like the OrderSummary ViewModel DTO, shown in the following example:

不过,ProducesResponseType 特性无法使用 dynamic 作为类型,而是要求使用显式的类型,比如下面这个示例中展示的 OrderSummary ViewModel DTO:

/// <summary>
/// 订单摘要类,用于封装订单的核心汇总信息。
/// </summary>
public class OrderSummary
{
    /// <summary>
    /// 获取或设置订单编号。
    /// </summary>
    public int ordernumber { get; set; } // 订单的唯一标识符

    /// <summary>
    /// 获取或设置订单创建日期。
    /// </summary>
    public DateTime date { get; set; } // 订单生成的时间戳

    /// <summary>
    /// 获取或设置订单当前状态。
    /// </summary>
    public string status { get; set; } // 例如:待支付、已发货、已完成等

    /// <summary>
    /// 获取或设置订单总金额。
    /// </summary>
    public double total { get; set; } // 包含所有商品及费用的最终金额
}

// 或者使用 C# 8 引入的 record(记录)类型来实现不可变数据模型:
/// <summary>
/// 订单摘要记录类型,提供与 OrderSummary 相同的数据结构但具有值相等性语义。
/// </summary>
/// <param name="ordernumber">订单编号</param>
/// <param name="date">订单日期</param>
/// <param name="status">订单状态</param>
/// <param name="total">订单总金额</param>
public record OrderSummary(int ordernumber, DateTime date, string status, double total);

This is another reason why explicit returned types are better than dynamic types, in the long term. When using the ProducesResponseType attribute, you can also specify what is the expected outcome regarding possible HTTP errors/codes, like 200, 400, etc.

这也是为什么从长远来看,显式的返回类型要比动态类型更好的另一个原因。在使用 ProducesResponseType 特性时,你还可以指定预期的 HTTP 错误码或状态码(比如 200、400 等)。

In the following image, you can see how Swagger UI shows the ResponseType information.

在下面的图片中,你可以看到 Swagger UI 是如何展示 ResponseType 信息的。

Screenshot of the Swagger UI page for the Ordering API.

Figure 7-5. Swagger UI showing response types and possible HTTP status codes from a Web API

图 7-5. Swagger UI 展示了 Web API 的响应类型以及可能的 HTTP 状态码

The image shows some example values based on the ViewModel types and the possible HTTP status codes that can be returned.

这张图片展示了一些基于 ViewModel 类型的示例值,以及可能返回的 HTTP 状态码。

posted @ 2026-05-17 17:33  菜鸟吊思  阅读(15)  评论(0)    收藏  举报