设计基础设施持久化层
Data persistence components provide access to the data hosted within the boundaries of a microservice (that is, a microservice's database). They contain the actual implementation of components such as repositories and Unit of Work classes, like custom Entity Framework (EF) DbContext objects. EF DbContext implements both the Repository and the Unit of Work patterns.
数据持久化组件提供了对微服务边界内所托管数据的访问(也就是微服务的数据库)。它们包含了诸如仓储(repositories)和工作单元(Unit of Work)类(比如自定义的 Entity Framework (EF) DbContext 对象)等组件的实际实现。EF DbContext 同时实现了仓储模式和工作单元模式。
The Repository pattern 仓储模式
The Repository pattern is a Domain-Driven Design pattern intended to keep persistence concerns outside of the system's domain model. One or more persistence abstractions - interfaces - are defined in the domain model, and these abstractions have implementations in the form of persistence-specific adapters defined elsewhere in the application.
仓储模式是一种领域驱动设计(DDD)模式,旨在将持久化相关的问题隔离在系统的领域模型之外。一个或多个持久化抽象(即接口)会在领域模型中定义,而这些抽象的实现则以持久化特定适配器的形式,定义在应用程序的其他地方。
Repository implementations are classes that encapsulate the logic required to access data sources. They centralize common data access functionality, providing better maintainability and decoupling the infrastructure or technology used to access databases from the domain model. If you use an Object-Relational Mapper (ORM) like Entity Framework, the code that must be implemented is simplified, thanks to LINQ and strong typing. This lets you focus on the data persistence logic rather than on data access plumbing.
仓储的实现类封装了访问数据源所需的逻辑。它们集中了通用的数据访问功能,提供了更好的可维护性,并将访问数据库所使用的基础设施或技术与领域模型解耦。如果你使用像 Entity Framework 这样的对象关系映射器(ORM),得益于 LINQ 和强类型特性,需要编写的代码会大大简化。这让你能够专注于数据持久化逻辑,而不是数据访问的底层管道。
The Repository pattern is a well-documented way of working with a data source. In the book Patterns of Enterprise Application Architecture, Martin Fowler describes a repository as follows:
仓储模式是一种处理数据源的成熟且文档齐全的方式。在《企业应用架构模式》一书中,Martin Fowler 对仓储是这样描述的:
A repository performs the tasks of an intermediary between the domain model layers and data mapping, acting in a similar way to a set of domain objects in memory. Client objects declaratively build queries and send them to the repositories for answers. Conceptually, a repository encapsulates a set of objects stored in the database and operations that can be performed on them, providing a way that is closer to the persistence layer. Repositories, also, support the purpose of separating, clearly and in one direction, the dependency between the work domain and the data allocation or mapping.
仓储在领域模型层和数据映射层之间扮演着中介的角色,其作用类似于内存中的一组领域对象。客户端对象以声明式的方式构建查询,并将其发送给仓储以获取结果。从概念上讲,仓储封装了一组存储在数据库中的对象以及可以在这些对象上执行的操作,提供了一种更接近持久化层的方式。此外,仓储也支持清晰且单向地分离工作领域与数据分配(或映射)之间的依赖关系这一目标。
Define one repository per aggregate 为每个聚合定义一个仓储
For each aggregate or aggregate root, you should create one repository class. You may be able to leverage C# Generics to reduce the total number concrete classes you need to maintain (as demonstrated later in this chapter). In a microservice based on Domain-Driven Design (DDD) patterns, the only channel you should use to update the database should be the repositories. This is because they have a one-to-one relationship with the aggregate root, which controls the aggregate's invariants and transactional consistency. It's okay to query the database through other channels (as you can do following a CQRS approach), because queries don't change the state of the database. However, the transactional area (that is, the updates) must always be controlled by the repositories and the aggregate roots.
对于每个聚合或聚合根,你都应该创建一个仓储类。你或许可以利用 C# 的泛型(Generics)来减少需要维护的具体类的总数(本章稍后会进行演示)。在一个基于领域驱动设计(DDD)模式的微服务中,你更新数据库应该使用的唯一通道就是仓储。这是因为它们与聚合根之间存在一对一的关系,而聚合根负责控制聚合的不变性(invariants)和事务一致性。通过其他通道查询数据库是可以的(就像你在使用 CQRS 方法时那样),因为查询不会改变数据库的状态。但是,事务区域(也就是更新操作)必须始终由仓储和聚合根来控制。
Basically, a repository allows you to populate data in memory that comes from the database in the form of the domain entities. Once the entities are in memory, they can be changed and then persisted back to the database through transactions.
基本上,仓库允许你以领域实体的形式,将数据库中的数据填充到内存中。一旦实体进入内存,它们就可以被修改,然后通过事务持久化回数据库。
As noted earlier, if you're using the CQS/CQRS architectural pattern, the initial queries are performed by side queries out of the domain model, performed by simple SQL statements using Dapper. This approach is much more flexible than repositories because you can query and join any tables you need, and these queries aren't restricted by rules from the aggregates. That data goes to the presentation layer or client app.
如前所述,如果你使用的是 CQS/CQRS 架构模式,初始的查询是由领域模型之外的辅助查询(side queries)来执行的,这些查询使用 Dapper 和简单的 SQL 语句来完成。这种方法比仓储灵活得多,因为你可以查询和连接任何你需要的表,而且这些查询不受聚合规则的约束。这些数据会流向表示层或客户端应用。
If the user makes changes, the data to be updated comes from the client app or presentation layer to the application layer (such as a Web API service). When you receive a command in a command handler, you use repositories to get the data you want to update from the database. You update it in memory with the data passed with the commands, and you then add or update the data (domain entities) in the database through a transaction.
如果用户进行了修改,待更新的数据会从客户端应用或表示层流向应用层(例如 Web API 服务)。当你在命令处理程序(command handler)中接收到一个命令时,你使用仓储从数据库中获取你想要更新的数据。你用命令中携带的数据在内存中对其进行更新,然后通过事务将数据(领域实体)添加或更新到数据库中。
It's important to emphasize again that you should only define one repository for each aggregate root, as shown in Figure 7-17. To achieve the goal of the aggregate root to maintain transactional consistency between all the objects within the aggregate, you should never create a repository for each table in the database.
需要再次强调的是,你应该只为每个聚合根定义一个仓储,如图 7-17 所示。为了实现聚合根维护聚合内所有对象之间事务一致性的目标,你绝不应该为数据库中的每张表都创建一个仓储。

Figure 7-17. The relationship between repositories, aggregates, and database tables
图 7-17. 仓储、聚合与数据库表之间的关系
The above diagram shows the relationships between Domain and Infrastructure layers: Buyer Aggregate depends on the IBuyerRepository and Order Aggregate depends on the IOrderRepository interfaces, these interfaces are implemented in the Infrastructure layer by the corresponding repositories that depend on UnitOfWork, also implemented there, that accesses the tables in the Data tier.
上图展示了领域层(Domain)与基础设施层(Infrastructure)之间的关系:买家聚合(Buyer Aggregate)依赖于 IBuyerRepository 接口,订单聚合(Order Aggregate)依赖于 IOrderRepository 接口。这些接口由基础设施层中对应的仓储类来实现,而这些仓储又依赖于同样在基础设施层实现的工作单元(UnitOfWork),最终由工作单元来访问数据层(Data tier)中的各个表。
Enforce one aggregate root per repository 强制每个仓储对应一个聚合根
It can be valuable to implement your repository design in such a way that it enforces the rule that only aggregate roots should have repositories. You can create a generic or base repository type that constrains the type of entities it works with to ensure they have the IAggregateRoot marker interface.
在设计仓储时,如果能强制规定“只有聚合根才能拥有仓储”这一规则,将会非常有价值。你可以创建一个泛型或基础仓储类型,通过约束其处理的实体类型,来确保它们都带有 IAggregateRoot 标记接口。
Thus, each repository class implemented at the infrastructure layer implements its own contract or interface, as shown in the following code:
因此,在基础设施层实现的每个仓储类都会实现其专属的契约(或接口),如下面的代码所示:
namespace Microsoft.eShopOnContainers.Services.Ordering.Infrastructure.Repositories // 定义订单服务的基础设施层仓储命名空间
{
/// <summary>
/// 订单仓储的具体实现类。
/// 负责提供对订单聚合根的持久化操作,并实现 IOrderRepository 接口中定义的契约。
/// </summary>
public class OrderRepository : IOrderRepository // 继承并实现订单仓储接口
{
// ...其他仓储成员及方法实现...
}
}
Each specific repository interface implements the generic IRepository interface:
每个具体的仓储接口都实现了通用的 IRepository 接口。
/// <summary>
/// 订单聚合根的仓储接口。
/// 继承自通用仓储基类,提供针对订单实体的特定数据访问契约。
/// </summary>
public interface IOrderRepository : IRepository<Order> // 继承通用的 Order 实体仓储接口,复用基础的 CRUD 操作定义
{
/// <summary>
/// 将一个新的订单聚合根添加到仓储中。
/// </summary>
/// <param name="order">待添加的订单实体对象。</param>
/// <returns>添加后的订单实体对象。</returns>
Order Add(Order order);
// ...其他订单特定的查询或操作方法...
}
However, a better way to have the code enforce the convention that each repository is related to a single aggregate is to implement a generic repository type. That way, it's explicit that you're using a repository to target a specific aggregate. That can be easily done by implementing a generic IRepository base interface, as in the following code:
不过,要想让代码自动强制遵循“每个仓储只对应一个聚合”这一规范,有一个更好的办法,那就是实现一个泛型的仓储类型。这样一来,就能明确地表明你正在使用一个仓储来针对某个特定的聚合。这可以通过实现一个泛型的 IRepository 基础接口来轻松完成,就像下面的代码一样:
/// <summary>
/// 泛型仓储接口,定义了针对聚合根的基础数据访问契约。
/// </summary>
/// <typeparam name="T">必须实现 IAggregateRoot 接口的聚合根类型。</typeparam>
public interface IRepository<T> where T : IAggregateRoot // 通过泛型约束确保该仓储仅用于操作领域驱动设计中的聚合根实体
{
//....
}
The Repository pattern makes it easier to test your application logic 仓储模式让测试应用逻辑变得更加容易
The Repository pattern allows you to easily test your application with unit tests. Remember that unit tests only test your code, not infrastructure, so the repository abstractions make it easier to achieve that goal.
仓储模式允许你通过单元测试轻松地测试你的应用程序。请记住,单元测试只测试你自己的代码,而不测试基础设施,因此仓储的抽象层让这一目标更容易实现。
As noted in an earlier section, it's recommended that you define and place the repository interfaces in the domain model layer so the application layer, such as your Web API microservice, doesn't depend directly on the infrastructure layer where you've implemented the actual repository classes. By doing this and using Dependency Injection in the controllers of your Web API, you can implement mock repositories that return fake data instead of data from the database. This decoupled approach allows you to create and run unit tests that focus the logic of your application without requiring connectivity to the database.
正如前面章节提到的,建议你在领域模型层中定义并放置仓储接口,这样应用层(比如你的 Web API 微服务)就不会直接依赖于你实现了具体仓储类的基础设施层。通过这样做,并在 Web API 的控制器中使用依赖注入,你可以实现返回模拟数据而不是数据库数据的模拟仓储(mock repositories)。这种解耦的方法允许你创建并运行专注于应用程序逻辑的单元测试,而无需连接到数据库。
Connections to databases can fail and, more importantly, running hundreds of tests against a database is bad for two reasons. First, it can take a long time because of the large number of tests. Second, the database records might change and impact the results of your tests, especially if your tests are running in parallel, so that they might not be consistent. Unit tests typically can run in parallel; integration tests may not support parallel execution depending on their implementation. Testing against the database isn't a unit test but an integration test. You should have many unit tests running fast, but fewer integration tests against the databases.
数据库连接可能会失败,而且更重要的是,针对数据库运行成百上千个测试是非常糟糕的,原因有两个。首先,由于测试数量庞大,这可能会花费很长时间。其次,数据库记录可能会发生改变并影响你的测试结果,尤其是当你的测试并行运行时,结果可能会不一致。单元测试通常可以并行运行;而集成测试根据其实现方式,可能不支持并行执行。针对数据库进行测试并不是单元测试,而是集成测试。你应该拥有许多运行速度很快的单元测试,而只拥有少量针对数据库的集成测试。
In terms of separation of concerns for unit tests, your logic operates on domain entities in memory. It assumes the repository class has delivered those. Once your logic modifies the domain entities, it assumes the repository class will store them correctly. The important point here is to create unit tests against your domain model and its domain logic. Aggregate roots are the main consistency boundaries in DDD.
就单元测试的关注点分离而言,你的逻辑是在内存中的领域实体上运行的。它假定仓储类已经交付了这些实体。一旦你的逻辑修改了领域实体,它会假定仓储类会正确地存储它们。这里的关键点是针对你的领域模型及其领域逻辑创建单元测试。聚合根是 DDD 中主要的一致性边界。
The repositories implemented in eShopOnContainers rely on EF Core's DbContext implementation of the Repository and Unit of Work patterns using its change tracker, so they don't duplicate this functionality.
eShopOnContainers 中实现的仓储依赖于 EF Core 的 DbContext 对仓储和工作单元模式的实现(使用了它的变更追踪器),因此它们并没有重复实现这些功能。
The difference between the Repository pattern and the legacy Data Access class (DAL class) pattern 仓储模式与传统的“数据访问类(DAL 类)”模式的区别
A typical DAL object directly performs data access and persistence operations against storage, often at the level of a single table and row. Simple CRUD operations implemented with a set of DAL classes frequently do not support transactions (though this is not always the case). Most DAL class approaches make minimal use of abstractions, resulting in tight coupling between application or Business Logic Layer (BLL) classes that call the DAL objects.
一个典型的 DAL 对象直接针对存储执行数据访问和持久化操作,通常是在单表和单行的级别上。使用一组 DAL 类实现的简单 CRUD 操作通常不支持事务(尽管并不总是如此)。大多数 DAL 类的方法很少使用抽象,导致调用 DAL 对象的应用程序或业务逻辑层(BLL)类之间产生紧密耦合。
When using repository, the implementation details of persistence are encapsulated away from the domain model. The use of an abstraction provides ease of extending behavior through patterns like Decorators or Proxies. For instance, cross-cutting concerns like caching, logging, and error handling can all be applied using these patterns rather than hard-coded in the data access code itself. It's also trivial to support multiple repository adapters which may be used in different environments, from local development to shared staging environments to production.
当使用仓储时,持久化的实现细节被从领域模型中封装隔离出去了。使用抽象层可以通过诸如装饰器(Decorators)或代理(Proxies)等模式轻松地扩展行为。例如,像缓存、日志记录和错误处理这样的横切关注点,都可以使用这些模式来应用,而不是在数据访问代码本身中硬编码。此外,支持多个仓库适配器也十分简单,这些适配器可在不同环境中使用,从本地开发到共享的预发布环境,再到生产环境。
Implementing Unit of Work 实现工作单元
A unit of work refers to a single transaction that involves multiple insert, update, or delete operations. In simple terms, it means that for a specific user action, such as a registration on a website, all the insert, update, and delete operations are handled in a single transaction. This is more efficient than handling multiple database operations in a chattier way.
工作单元指的是涉及多次插入、更新或删除操作的单个事务。简单来说,这意味着对于特定的用户操作(例如在网站上的注册),所有的插入、更新和删除操作都在一个事务中处理。这比以频繁交互的方式处理多个数据库操作要高效得多。
These multiple persistence operations are performed later in a single action when your code from the application layer commands it. The decision about applying the in-memory changes to the actual database storage is typically based on the Unit of Work pattern. In EF, the Unit of Work pattern is implemented by a DbContext and is executed when a call is made to SaveChanges.
这些多个持久化操作会在稍后由你的应用层代码发出指令时,在单个动作中执行。关于将内存中的更改应用到实际数据库存储的决定,通常基于工作单元模式。在 EF 中,工作单元模式由 DbContext 实现,并在调用 SaveChanges 时执行。
In many cases, this pattern or way of applying operations against the storage can increase application performance and reduce the possibility of inconsistencies. It also reduces transaction blocking in the database tables, because all the intended operations are committed as part of one transaction. This is more efficient in comparison to executing many isolated operations against the database. Therefore, the selected ORM can optimize the execution against the database by grouping several update actions within the same transaction, as opposed to many small and separate transaction executions.
在许多情况下,这种针对存储应用操作的模式或方式可以提高应用程序的性能,并减少不一致的可能性。它还减少了数据库表中的事务阻塞,因为所有预期的操作都是作为单个事务的一部分提交的。与针对数据库执行许多孤立的操作相比,这种方式更高效。因此,选定的 ORM 可以通过将多个更新操作分组到同一个事务中,而不是许多小型且独立的事务执行,来优化针对数据库的执行。
The Unit of Work pattern can be implemented with or without using the Repository pattern.
工作单元模式可以在使用或不使用仓储模式的情况下实现。
Repositories shouldn't be mandatory 仓储不应该是强制性的
Custom repositories are useful for the reasons cited earlier, and that is the approach for the ordering microservice in eShopOnContainers. However, it isn't an essential pattern to implement in a DDD design or even in general .NET development.
自定义仓储很有用,原因正如前面所述,这也是 eShopOnContainers 中订单微服务所采用的方法。然而,它并不是 DDD 设计甚至一般 .NET 开发中必须实现的本质模式。
For instance, Jimmy Bogard, when providing direct feedback for this guide, said the following:
例如,Jimmy Bogard 在为这份指南提供直接反馈时,说了以下这番话:
This'll probably be my biggest feedback. I'm really not a fan of repositories, mainly because they hide the important details of the underlying persistence mechanism. It's why I go for MediatR for commands, too. I can use the full power of the persistence layer, and push all that domain behavior into my aggregate roots. I don't usually want to mock my repositories – I still need to have that integration test with the real thing. Going CQRS meant that we didn't really have a need for repositories any more.
这大概是我最大的反馈意见了。我真的不太喜欢仓储,主要是因为它们隐藏了底层持久化机制的重要细节。这也是为什么我在处理命令时也倾向于使用 MediatR。我可以使用持久化层的全部功能,并将所有领域行为推入我的聚合根中。我通常不想模拟我的仓储——我仍然需要使用真实的东西来进行集成测试。采用 CQRS 意味着我们实际上不再真正需要仓储了。
提示
Jimmy Bogard 的这段话深刻反映了他对领域驱动设计(DDD)和现代 ORM(如 Entity Framework Core)在实战中的务实态度。结合相关资料,我们可以从以下两个方面来解答您的疑问:
一、 Jimmy Bogard 为什么反对仓储模式(Repository Pattern)?
他之所以表示“不是仓储模式的粉丝”,核心原因在于过度抽象带来的功能受限与测试痛点。具体可以归结为以下几点:
隐藏了底层持久化机制的关键细节
仓储模式试图将数据访问逻辑封装起来,但这往往会导致无法充分利用底层 ORM(如 EF Core)的强大功能。例如,EF Core 内置了强大的变化跟踪(Change Tracking)功能,能精准识别并只更新发生改变的属性;而传统的通用仓储往往会使用简单的Update方法,导致保存实体时更新所有属性,造成性能浪费。此外,为了保持仓储接口的纯粹性,开发者通常会避免暴露IQueryable,从而丧失了动态组合查询的能力。阻碍了集成测试的真实性
Bogard 提到他通常不想模拟(Mock)仓储,而是希望对真实数据进行集成测试。如果强行引入仓储层,不仅增加了不必要的间接调用层级,还会让测试变得繁琐且难以验证真实的数据库交互行为。CQRS 架构下仓储显得多余
当采用了 CQRS(命令查询职责分离)架构后,读写操作被彻底拆分。对于复杂的查询,可以使用专门的查询对象(Query Objects)直接利用 EF Core 进行高效投影和过滤;而对于写操作,所有的领域行为都被推送到聚合根(Aggregate Roots)内部处理。在这种模式下,传统的 CRUD 仓储已经失去了存在的必要。二、 他如何使用 MediatR 来实现持久化?
需要澄清的是,MediatR 本身并不负责或实现数据的持久化。它是一个轻量级的进程内消息传递库,用于实现中介者模式(Mediator Pattern),其核心作用是解耦请求发送方与具体的处理逻辑。
Bogard 的实际做法是将 MediatR 作为应用程序层的调度器,并在 Command Handler(命令处理器)中直接完成持久化。具体流程如下:
通过 MediatR 路由命令
控制器(Controller)不再注入任何服务或仓储,它仅仅是一个路由转发点。控制器接收用户的 HTTP 请求后,将其封装为一个 Command(命令),然后通过IMediator.Send()发送给内存中的对应处理器。在 Handler 中直接使用 DbContext 进行持久化
在具体的 Command Handler 内部,Bogard 会直接注入并使用DbContext(即工作单元)。他将所有的业务校验和状态变更规则都封装在 DDD 的聚合根中,然后在 Handler 里调用聚合根的方法执行这些领域行为。最后,直接在 Handler 中调用DbContext.SaveChangesAsync()来提交事务,将内存中的更改合并为单个操作写入数据库。利用管道行为(Behaviors)处理横切关注点
借助 MediatR 的管道行为特性,他可以将日志记录、缓存、错误处理以及跨切面的事务管理等逻辑抽离出来,而不是硬编码在数据访问代码或仓储中。总结来说,Bogard 的理念是:用 MediatR 解决应用层的分发与解耦问题,用丰富的聚合根承载业务逻辑,然后毫无保留地拥抱 EF Core 的原生能力来完成最终的持久化。
Repositories might be useful, but they are not critical for your DDD design in the way that the Aggregate pattern and a rich domain model are. Therefore, use the Repository pattern or not, as you see fit.
仓储可能很有用,但它们对于你的 DDD 设计来说,并不像聚合模式和丰富的领域模型那样至关重要。因此,使用或不使用仓储模式,请根据你自己的判断来决定。

浙公网安备 33010602011771号