Yesterday, my friends in the ". NET Big Bull Road" group talked about using EF Core to realize the warehousing model. I remembered reading an article written by a foreign boss before and thought it was very valuable. I translated it today for everyone to appreciate and learn from it.
Original: www.thereformedprogrammer.net/is-the-repository-pattern-useful-with-entity-framework-core/
Author: Jon P Smith
Translation: Exquisite Code Farmer-Wang Liang
Explanation: The original text was first released in February 2018 and was last updated in July 2020.
Text:
I wrote my first article on warehousing models in 2014, and it is still a very popular article. This article is an updated version of that article, based on the new release of EF Core in recent years and further research on EF Core database access patterns.
Original: Analyzing whether Repository pattern useful with Entity Framework (May 2014).
https://www.thereformedprogrammer.net/is-the-repository-pattern-useful-with-entity-framework/
First solution: Four months on-my solution to replace the Repository pattern (October 2014).
https://www.thereformedprogrammer.net/is-the-repository-pattern-useful-with-entity-framework-part-2/
THIS ARTICLE: Is the repository pattern useful with Entity Framework Core?
1 schematically
The answer is "no", the warehouse/unit of work model (Rep/UoW for short) is not useful for EF Core. EF Core already implements the Rep/UoW mode, so adding another Rep/UoW mode to EF Core is useless.
A better solution is to use EF Core directly, which allows you to use all of EF Core's features to build high-performance database access.
2 Purpose of this article
This article focuses on:
What do people think of EF's Rep/UoW model?
Advantages and disadvantages of using Rep/UoW pattern in EF.
Three ways to replace the Rep/UoW pattern with EF Core code.
How to make your EF Core database access code easy to discover and refactor.
A discussion about EF Core unit testing.
I will assume that you are familiar with C#and the EF 6.x or EF Core libraries. I specifically talked about EF Core in this article, but most of the content is also related to EF6.x.
3 Background
In 2013, I was involved in the development of a large web application dedicated to health care modeling. I used ASP.NET MVC4 and EF5, which was just out at the time and supported SQL Spatial types for processing geographical data. The popular database access model at the time was the Rep/UoW model-see Microsoft's 2013 article on accessing databases using EF Core and Rep/UoW models.
- Implementing the Repository and Unit of Work Patterns in an ASP.NET MVC Application
I used Rep/UoW to build my application, but found this to be a pain point during development. I had to constantly "tweak" the library code to fix small issues, and each time I "tweak" broke something else. This is what led me to look at how to better implement my database access code.
Having said that, I signed a contract with a newly formed company at the end of 2017 to help them solve performance issues with EF6.x applications. The main part of the performance problem turned out to be due to lazy loading, which was needed because the application used the Rep/UoW pattern.
As it turned out, a programmer involved in the launch project had used the Rep/UoW pattern. In a conversation with the company's founder, he said he found the Rep/UoW portion of the application to be quite opaque and difficult to manipulate.
4 How people view warehousing models
While researching the design of Spatial Modeller™, I discovered some blog posts that provided strong evidence for abandoning the warehousing model. The most convincing and well-thought-out article of this kind is "Repositories On Top UnitofWork Are Not a Good Idea". Rob Conery's main point is that Rep/UoW is just repeating what the Entity Framework (EF) DbContext gives you, so why hide a perfect framework under a seemingly worthless appearance? Rob calls it "overly abstract stupidity."
Another blog post is "Why Entity Framework renders the Repository pattern obsolete". In this article, Isaac Abraham adds that the warehousing model does not make testing easier, which is something it should have done. This is more practical in EF Core, as you will see later.
So, are they right?
5 My opinion on the Rep/UoW model
Let me try to review the pros and cons of the Rep/UoW model in the fairest way possible. Here are my views.
5.1 Advantages of the Rep/UoW model
Isolate your database access code. The biggest advantage of the warehouse model is that you know where all your database access code is. In addition, you will usually divide your warehouse into several sections, such as catalog libraries, order processing libraries, etc., which makes it easy to find code for specific queries that have errors or require performance tweaks. This is undoubtedly a great advantage.
Aggregation. Domain-Driven Design (DDD) is a method of designing systems that recommends that you have a root entity into which other related entities are included. The example I used in the book Entity Framework Core in Action is a Book entity with a collection of Review entities. These reviews are only meaningful when associated with Books, so DDD says you should change reviews only through the Book entity. The Rep/UoW pattern achieves this by providing a way to add/delete comments in the Book Repository.
Hide complex T-SQL commands. Sometimes you need to bypass EF Core and use T-SQL. This type of access should be hidden from high levels but easy to find to help maintain or refactor. I should point out that Rob Conery's post Command/Query Objects can also address this issue.
Easy to simulate/test. It's easy to simulate a separate resource library, which makes it easier to access the database's unit test code. This was true a few years ago, but now there are other ways to solve this problem, which I will describe later.
You will notice that I did not propose "replacing EF Core with another database access library." That's one of the ideas behind Rep/UoW, but I think it's a misunderstanding because a) it's difficult to replace a database access library, and b) would you really swap such a key library in your application?
5.2 Disadvantages of Rep/UoW model
The first three projects are all around performance. I'm not saying you can't write an efficient Rep/UoW, but it's a tough job, and I see many implementations with inherent performance issues (including Microsoft's old Rep/UoW implementation). Here is a list of shortcomings I found in the Rep/UoW pattern.
性能--处理实体关系。一个资源库通常会返回一种类型的
IEnumerable/IQueryable结果,例如在微软的例子中的一个 Student 实体类。假设你想从 Student 的关系中显示信息,比如他们的地址?在这种情况下,仓储库中最简单的方法是使用懒加载来读取学生的地址实体,我看到人们经常这样做。问题是懒加载会导致每一个关系都要单独往返于数据库,这比把所有的数据库访问合并成一次数据库往返要慢。(另一种方法是有多个不同返回类型的查询方法,但这将使你的资源库变得非常大和麻烦--见第 4 点)。Data does not meet the required format. Because warehousing components are typically created from a database, the data returned may not be in the exact format required by the service or user. You may be able to adjust the output of the warehouse, but this is the second stage you have to write. I think it would be better to form your query closer to the front end and include any adjustments to the data you need.
Performance-Update: Many Rep/UoW implementations attempt to hide EF Core, but doing so does not take advantage of all of its capabilities. For example, Rep/UoW updates an entity using EF Core's Update method, which saves every attribute in the entity. Using EF Core's built-in change tracking feature, it will only update attributes that have changed.
It's so universal. The temptation of Rep/UoW comes from the fact that you can write a common repository and use it to build all sub-repositories, such as catalog libraries, order processing libraries, etc. This should minimize the amount of code you need to write, but my experience is that a universal repository works well at the beginning, but as things become more and more complex, you end up having to add more and more code to each individual repository.“The more reusable the code is, the less usable it is.”--Neil Ford
** To sum up the downside-Rep/UoW hides EF Core, which means you can't use EF Core's capabilities to write simple but efficient database access code. **
6 How to retain the advantages of Rep/UoW and use EF Core
In the previous strengths section, I listed isolation, aggregation, concealment, and unit testing, and Rep/UoW did a good job. In this section, I will talk about some of the different software patterns and practices that combine with good architectural design to provide the same isolation, aggregation, etc. when you use EF Core directly.
I will explain the implementation of each advantage and then put them into a layered software architecture.
- Query objects: A way to isolate and hide database reads
Database access can be divided into four types. Add, read, update and delete-known as CRUD. For me, the reading part, known as queries in EF Core, is often the most difficult to build and performance adjust. Many applications rely on good, quick queries, such as a list of products to buy, a list of things to do, etc. The solution people come up with is the query object.
I first came into contact with them in a 2013 article by Rob Conery (mentioned earlier), where he mentioned command/query objects. In addition, Jimmy Bogard published an article in 2012 titled "Favorite query objects over repositories." Using. NET's IQueryable type and extension method, we can improve the query object pattern in Rob and Jimmy's examples.
The following list gives a simple example of a query object that can choose how to sort an integer list.
public static class MyLinqExtension
{
public static IQueryable<int> MyOrder
(this IQueryable<int> queryable, bool ascending)
{
return ascending
? queryable.OrderBy(num => num)
: queryable.OrderByDescending(num => num);
}
}
下面这是这个 MyOrder 查询对象使用示例:
var numsQ = new[] { 1, 5, 4, 2, 3 }.AsQueryable();
var result = numsQ
.MyOrder(true)
.Where(x => x > 3)
.ToArray();
MyOrder 查询对象的工作原理是,IQueryable 类型持有一个命令列表,当我应用 ToArray 方法时,这些命令会被执行。在我的简单例子中,我没有使用数据库,但如果我们用应用程序的 DbContext 的 DbSet<T> 属性替换 numsQ 变量,那么 IQueryable<T> 类型中的命令将被转换为数据库命令。
因为 IQueryable<T> 类型直到最后才被执行,所以你可以将多个查询对象连锁起来。让我从我的书《Entity Framework Core in Action》中给你一个更复杂的数据库查询的例子。在下面的代码中,使用了四个查询对象链在一起,对一些图书的数据进行选择、排序、过滤和分页。你可以在实时网站 efcoreinaction.com 看到这些。
public IQueryable<BookListDto> SortFilterPage
(SortFilterPageOptions options)
{
var booksQuery = _context.Books
.AsNoTracking()
.MapBookToDto()
.OrderBooksBy(options.OrderByOptions)
.FilterBooksBy(options.FilterBy,
options.FilterValue);
options.SetupRestOfDto(booksQuery);
return booksQuery.Page(options.PageNum-1,
options.PageSize);
}
查询对象提供了比 Rep/UoW 模式更好的隔离性,因为你可以把复杂的查询分割成一系列的查询对象,并把它们连在一起。这使得它更容易编写和理解、重构和测试。另外,如果你有一个需要原始 SQL 的查询,你可以使用 EF Core 的 FromSql 方法,它也可以返回 IQueryable<T>。
- Add, update and delete database access methods
查询对象处理了 CRUD 的读取部分,但是新增、更新和删除部分呢,也就是你向数据库写入的部分?我将向你展示运行 CUD 操作的两种方法:直接使用 EF Core 命令,以及使用实体类中的 DDD 方法。让我们来看看非常简单的更新例子:在我的图书应用中添加一个评论(见efcoreinaction.com)。
注:如果你想尝试添加评论,有一个与我的书配套的 GitHub repo(
github.com/JonPSmith/EfCoreInAction),选择分支 Chapter05(每章都有一个分支)并在本地运行该应用程序。你会看到每本书旁边都有一个管理按钮,有几个 CUD 命令。
** Method 1: Use EF Core commands directly **
最明显的方法是使用 EF Core 方法来完成数据库的更新。下面是一个方法,它将为一本书添加一个新的评论,其中包括用户提供的评论信息。注意:ReviewDto 是一个持有用户填写完评论信息后返回的信息类。
public Book AddReviewToBook(ReviewDto dto)
{
var book = _context.Books
.Include(r => r.Reviews)
.Single(k => k.BookId == dto.BookId);
var newReview = new Review(dto.numStars, dto.comment, dto.voterName);
book.Reviews.Add(newReview);
_context.SaveChanges();
return book;
}
注:AddReviewToBook方法是在一个叫做AddReviewService的类中,这个类在我的ServiceLayer中。这个类被注册为一个服务,并且有一个构造函数,它接收应用程序的DbContext,这个DbContext是通过 DI 注入的。注入的值被存储在私有字段_context中,AddReviewToBook方法可以使用它来访问数据库。
This will add new comments to the database, which works well, but there is another way to build using more DDD methods.
** Method 2: DDD style entity class **
EF Core provides us with a new place to write your updated code-inside entity classes. EF Core has a feature called backing fields, which makes it possible to build DDD entities. The backup field allows you to control access to any relationship structure. This is actually impossible in EF6.x.
DDD 谈到了聚合(前面提到过),所有的聚合只能通过根实体中的方法来改变,我把它称为访问方法。在 DDD 术语中,评论是图书实体的聚合,所以我们应该通过图书实体类中的一个名为 AddReview 的访问方法来添加一个评论。这样一来,上面的代码就变成了 Book 实体中的一个方法,在这里:
public Book AddReviewToBook(ReviewDto dto)
{
var book = _context.Find<Book>(dto.BookId);
book.AddReview(dto.numStars, dto.comment,
dto.voterName, _context);
_context.SaveChanges();
return book;
}
Book 实体类中的 AddReview 访问方法看起来像这样:
public class Book
{
private HashSet<Review> _reviews;
public IEnumerable<Review> Reviews => _reviews?.ToList();
//...other properties left out
//...constructors left out
public void AddReview(int numStars, string comment,
string voterName, DbContext context = null)
{
if (_reviews != null)
{
_reviews.Add(new Review(numStars, comment, voterName));
}
else if (context == null)
{
throw new ArgumentNullException(nameof(context),
"You must provide a context if the Reviews collection isn't valid.");
}
else if (context.Entry(this).IsKeySet)
{
context.Add(new Review(numStars, comment, voterName, BookId));
}
else
{
throw new InvalidOperationException("Could not add a new review.");
}
}
//...
}
这个方法更复杂,因为它可以处理两种不同的情况:一种是已经加载了 Review,另一种是还没有。但如果评论还没有被加载,它比原来的情况要快,因为它使用了“通过外键创建关系”的方法。
因为访问方法的代码在实体类里面,如果需要的话可以更复杂,因为它将是你需要写的唯一版本的代码。在方式一中,你可以在不同的地方重复相同的代码,只要你需要更新 Book 的 Review 集合。
Note: I wrote an article titled "Creating Domain-Driven Design entity classes with Entity Framework Core", which is all about DDD style entity classes. This article provides a more detailed introduction to this topic. I also updated my article on how to write business logic in EF Core to use the same DDD-style entity classes.
为什么实体类中的方法不调用 SaveChanges?在方式一中,一个方法包含了所有的部分:a)加载实体,b)更新实体,c)调用 SaveChanges 来更新数据库。我可以这样做,因为我知道它是由一个网络请求调用的,而这就是我想做的全部。对于 DDD 实体方法,你不能在实体方法中调用 SaveChanges,因为你不能确定操作已经完成。例如,如果你从备份中加载一本书,你可能想创建这本书,添加作者,添加任何评论,然后调用 SaveChanges,这样所有的东西都在一起被提交到数据库。
** Method 3: GenericServices library **
还有第三种方式。我注意到在我构建的 ASP.NET 应用程序中使用 CRUD 命令时有一个标准模式,早在 2014 年我就建立了一个名为 GenericServices 的库,它可以与 EF6.x 一起使用。2018 年我为 EF Core 建立了一个更全面的版本,名为 EfCore.GenericServices,见这篇关于 EfCore.GenericServices 的文章:
- GenericServices: A library to provide CRUD front-end services from a EF Core database
这些库并没有真正实现仓储模式,而是在实体类和前端需要实际数据之间充当适配器模式。我曾使用过原始的 EF6.x,GenericServices 为我节省了几个月的枯燥的前端代码编写。新的 EfCore.GenericServices 甚至更好,因为它可以与标准风格的实体类和 DDD 风格的实体类一起工作。
- Which method is better
方式一(直接使用 EF Core 代码)要写的代码最少,但有可能出现重复,因为应用程序的不同部分可能要对一个实体应用 CUD 命令。例如,当用户通过改变事物时,你可能会通过 ServiceLayer 进行更新,但外部 API 可能不会通过 ServiceLayer,所以你必须重复 CUD 代码。
方式二(DDD 风格的实体类)将关键的更新部分放在实体类内,所以代码对任何能得到实体实例的人都是可用的。事实上,由于 DDD 风格的实体类“锁定”了对属性和集合的访问,每个人都必须使用 Book 实体的 AddReview 访问方法,如果他们想更新 Review 集合的话。出于许多原因,这是我想在未来的应用中使用的方法(见我的文章中关于利弊的讨论)。其(轻微的)缺点是它需要一个单独的加载/保存部分,这意味着更多的代码。
方式三(GenericServices 库)是我的首选方法,尤其是现在我已经建立了 EfCore.GenericServices 版本,可以处理 DDD 风格的实体类。正如你在关于 EfCore.GenericServices 的文章中所看到的,这个库极大地减少了你在 Web/移动/桌面应用程序中需要编写的代码。当然,你仍然需要在你的业务逻辑中访问数据库,但这是另一回事。
7 Organize your CRUD code
One benefit of the Rep/UoW pattern is that it puts all your data access code in one place. When switching to EF Core directly, you can put your data access code anywhere, but this makes it difficult for you or other team members to find it. Therefore, I recommend having a clear plan for where your code will be placed and stick to it.
The figure below shows a hierarchical or hexagonal architecture with only three assemblies shown (I left out the business logic, in a hexagonal architecture, you will have more assemblies). The three assemblies shown are:
ASP.NET Core。This is the presentation layer that provides HTML pages or a web API. This has no database access code, but relies on various methods in the Service Layer and Business Layer.
Service layer. It contains database access code, including query objects and adding, updating and deleting methods. The service layer uses adapter patterns and command patterns to connect the data layer and the ASP.NET Core (presentation) layer.
Data layer. It contains the application's DbContext and entity classes. Then, the DDD-style entity class contains access methods to allow the root entity and its aggregates to be modified.

注:前面提到的库
GenericServices(EF6.x)和EfCore.GenericServices(EF Core)实际上是一个提供ServiceLayer功能的库,即在DataLayer和你的 Web/移动/桌面应用程序之间充当适配器模式和命令模式。
从这个图中我想说的是,通过使用不同的程序集,一个简单的命名标准(见图中黑体字 Book)和文件夹,你可以建立一个应用程序,其中你的数据库代码是独立的,很容易找到。随着你的应用程序的增长,这可能是至关重要的。
8 EF Core Method Unit Testing
The last part to look at is unit testing applications that use EF Core. One of the advantages of the warehousing model is that you can replace it with a simulation during testing. Therefore, using EF Core directly loses the option of simulation (technically you can simulate EF Core, but it's hard to do it well).
Fortunately, EF Core has made progress now, and you can use in-memory databases to simulate databases. In-memory databases are created faster and have a default starting point (that is, empty), so it is much easier to write tests against it. See my article "Using in-memory databases for unit testing EF Core applications" for details on how to do this, and there is also a NuGet package called EfCore.TestSupport, which provides ways to make writing EF Core unit tests faster.
9 Conclusion
我上一个使用 Rep/UoW 模式的项目要追溯到 2013 年,从那以后我再也没有使用过 Rep/UoW 模式。我尝试过一些方法,一个基于 EF6.x 名为 GenericServices 的自定义库,以及现在一个更标准的基于 EF Core 实现查询对象和 DDD 风格的实体类方法的 EfCore.GenericServices 自定义库。它们使得编写代码更容易,而且通常表现良好。但如果它们很慢,就很容易定位并对单个数据库访问进行性能调整。
In my book for Manning Press, there is a chapter on performance tweaks to an ASP.NET Core application that "sells" books. This process uses query objects and DDD entity methods and shows that it can produce good performance database access (see my article "Entity Framework Core performance tuning-a worked example").
My own job is to use query objects to read and use DDD-style entity classes and their access methods to CUD and business logic. I do need to use these things in an appropriate application to truly know if they are effective. Please follow my blog to learn more about DDD-style entity classes and the architectures that benefit from them, and maybe a new library:).
Enjoy coding!