Is the Repository Pattern Still Applicable to EF Core?

Is the Repository Pattern Still Applicable to EF Core?

Yesterday, the friends in the '.NET Big Cow Road' group discussed the topic of implementing the repository pattern using EF Core. I remembered an article written by a foreign expert that I had read before, and I think it's very valuable for reference.

Last updated 5/4/2022 4:04 PM
liamwang 精致码农
24 min read
Category
EF Core
Tags
.NET C# EF Core ORM

Yesterday, the group members of the '.NET Big Shot Road' group discussed the topic of implementing the repository pattern using EF Core. I recalled reading an article written by a foreign expert that I found very valuable, and today I have translated it for your appreciation.

Original: https://www.thereformedprogrammer.net/is-the-repository-pattern-useful-with-entity-framework-core/

Author: Jon P Smith

Translation: Jingzhi Programmer - Wang Liang

Note: The original article was first published in February 2018 and last updated in July 2020.

Body:

I wrote my first article on the repository pattern in 2014, and it remains a popular article. This article is an updated version of that one, based on new releases of EF Core in recent years and further research into EF Core database access patterns.

  1. Original: Analysing whether Repository pattern useful with Entity Framework (May 2014). https://www.thereformedprogrammer.net/is-the-repository-pattern-useful-with-entity-framework/

  2. First solution: Four months on – my solution to replacing the Repository pattern (October 2014). https://www.thereformedprogrammer.net/is-the-repository-pattern-useful-with-entity-framework-part-2/

  3. THIS ARTICLE: Is the repository pattern useful with Entity Framework Core?

1 Summary

The answer is "no", the Repository/Unit of Work pattern (abbreviated as Rep/UoW) is not useful for EF Core. EF Core already implements the Rep/UoW pattern, so adding another Rep/UoW pattern on top of EF Core is unnecessary.

A better solution is to use EF Core directly, which allows you to leverage all of EF Core's features to build high-performance database access.

2 Purpose of This Article

This article focuses on:

  • What people think about the Rep/UoW pattern for EF.
  • The pros and cons of using the 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 on unit testing with EF Core.

I will assume you are familiar with C# and the EF 6.x or EF Core libraries. While I specifically discuss EF Core, most of the content also applies to EF6.x.

3 Background

In 2013, I worked on a large web application dedicated to healthcare modeling. I used ASP.NET MVC4 and EF 5, which had just been released and supported the SQL Spatial type for handling geographic data. The popular database access pattern at the time was the Rep/UoW pattern—see Microsoft's 2013 article on using EF Core with the Rep/UoW pattern for database access.

I built my application using Rep/UoW, but during development, I found it to be a real pain point. I constantly had to "adjust" the repository code to fix minor issues, and each adjustment broke something else. This led me to research how to better implement my database access code.

That said, I contracted with a new company in late 2017 to help them resolve performance issues in an EF6.x application. A major part of the performance problem turned out to be due to lazy loading, which was required because the application used the Rep/UoW pattern.

It turned out that a programmer involved in the startup project had used the Rep/UoW pattern before. When speaking with the company's founder, he mentioned that he found the Rep/UoW part of the application quite opaque and difficult to work with.

4 How People View the Repository Pattern

During the design of Spatial Modeller™, I came across several blog posts that provided strong evidence against the repository pattern. The most convincing and thoughtful of these is "Repositories On Top UnitOfWork Are Not a Good Idea". Rob Conery's main point is that Rep/UoW simply duplicates what Entity Framework (EF) DbContext provides, so why hide a perfect framework behind a facade that adds no value. Rob calls it "the stupidity of over-abstracting".

Another blog is "Why Entity Framework renders the Repository pattern obsolete". In it, Isaac Abraham adds that the repository pattern does not make testing easier, which is one thing it's supposed to do. This is even more practical in EF Core, as you will see later.

So, are they right?

5 My View on the Rep/UoW Pattern

Let me try to review the pros and cons of the Rep/UoW pattern as fairly as possible. Here are my opinions.

5.1 Advantages of the Rep/UoW Pattern

  1. Isolate your database access code. The biggest advantage of the repository pattern is that you know where all your database access code is. Additionally, you typically split your repositories into parts, such as catalog repository, order processing repository, etc., making it easy to find code for a specific query that has a bug or needs performance tuning. This is definitely a big plus.

  2. Aggregation. Domain-Driven Design (DDD) is a way to design systems that suggests you have a root entity to which other related entities belong. The example I used in my book Entity Framework Core in Action is a Book entity with a collection of Review entities. These Reviews only make sense in relation to a Book, so DDD says you should only change Reviews through the Book entity. The Rep/UoW pattern implements this by providing a way to add/remove reviews within the Book Repository.

  3. Hide complex T-SQL commands. Sometimes you need to bypass EF Core and use T-SQL. This type of access should be hidden from upper layers but easy to find to aid maintenance or refactoring. I should note that Rob Conery's post on Command/Query Objects can handle this as well.

  4. Easy to mock/test. It's easy to mock a single repository, making it simpler to unit test code that accesses the database. This was true several years ago, but now there are other ways to handle this, which I will introduce later.

You might notice I did not mention "replacing EF Core with another database access library". This is one of the ideas behind Rep/UoW, but I consider it a misconception because a) replacing a database access library is difficult, and b) would you really swap such a critical library in your application?

5.2 Disadvantages of the Rep/UoW Pattern

The first three items revolve around performance. I'm not saying you can't write an efficient Rep/UoW, but it's hard work, and I've seen many implementations with inherent performance issues (including Microsoft's old Rep/UoW implementation). Here is a list of disadvantages I've found in the Rep/UoW pattern.

  1. Performance – managing entity relationships. A repository typically returns a IEnumerable/IQueryable result of one type, e.g., a Student entity class in Microsoft's example. Suppose you want to display information from Student's relationships, like their address? In this case, the simplest approach in the repository is to use lazy loading to read the student's address entity, which I've seen people do often. The problem is that lazy loading causes a separate database round trip for each relationship, which is slower than combining all database access into a single database round trip. (Another approach is to have multiple query methods with different return types, but this makes your repository very large and cumbersome – see point 4.)

  2. Data not in required format. Because repositories are typically created based on the database, the returned data may not be in the exact format needed by the service or user. You might adjust the repository's output, but that's a second phase you have to write. I think it's better to shape your query closer to the front end, including any adjustments to the data you need.

  3. Performance – Updates: Many Rep/UoW implementations try to hide EF Core but fail to utilize all its features. For example, Rep/UoW might use EF Core's Update method to update an entity, which saves every property in the entity. With EF Core's built-in change tracking, only the properties that have changed are updated.

  4. Too generic. The appeal of Rep/UoW comes from being able to write a generic repository and then use it to build all sub-repositories (e.g., catalog repository, order processing repository). This should minimize the code you need to write, but in my experience, a generic repository works initially, but as things get more complex, you end up adding more and more code for each individual repository. "The more reusable the code is, the less usable it is." --Neil Ford

Summary of the downsides – Rep/UoW hides EF Core, meaning you cannot use EF Core's features to write simple yet efficient database access code.

6 How to Keep the Advantages of Rep/UoW While Using EF Core

In the advantages section earlier, I listed isolation, aggregation, hiding, and unit testing as things Rep/UoW does well. In this section, I will discuss some different software patterns and practices that, when combined with good architectural design, provide the same isolation, aggregation, etc., when you use EF Core directly.

I will explain the implementation of each advantage and then place them into a layered software architecture.

1. Query Objects: A Way to Isolate and Hide Database Reads

Database access can be divided into four types: Create, Read, Update, and Delete – known as CRUD. For me, the read part, called queries in EF Core, is often the hardest to build and performance-tune. Many applications rely on good, fast queries, e.g., a list of products to buy, a list of tasks to do, etc. The solution people came up with is query objects.

I first encountered them in 2013 in Rob Conery's article (mentioned earlier), where he mentions Command/Query Objects. Additionally, Jimmy Bogard published an article in 2012 titled "Favor query objects over repositories". Using .NET's IQueryable type and extension methods, we can improve the query object pattern over Rob and Jimmy's examples.

The following list gives a simple example of a query object that can choose a sort order for a list of integers.

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);
    }
}

Below is an example of using this MyOrder query object:

var numsQ = new[] { 1, 5, 4, 2, 3 }.AsQueryable();

var result = numsQ
    .MyOrder(true)
    .Where(x => x > 3)
    .ToArray();

The MyOrder query object works because the IQueryable type holds a list of commands that are executed when I apply the ToArray method. In my simple example, I didn't use a database, but if we replace the numsQ variable with an application's DbContext DbSet<T> property, the commands in the IQueryable<T> type will be translated to database commands.

Because IQueryable<T> is not executed until the end, you can chain multiple query objects together. Let me give you a more complex database query example from my book Entity Framework Core in Action. In the code below, four query objects are chained together to select, sort, filter, and paginate some book data. You can see this live at the website 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);
}

Query objects provide better isolation than the Rep/UoW pattern because you can split complex queries into a series of query objects and chain them together. This makes them easier to write, understand, refactor, and test. Additionally, if you have a query that requires raw SQL, you can use EF Core's FromSql method, which can also return IQueryable<T>.

2. Methods for Create, Update, and Delete Database Access

Query objects handle the read part of CRUD, but what about the create, update, and delete parts (CUD), i.e., writing to the database? I'll show you two ways to perform CUD operations: using EF Core commands directly, and using DDD methods within entity classes. Let's look at a very simple update example: adding a review to my book application (see efcoreinaction.com).

Note: If you want to try adding a review, there is a GitHub repo (github.com/JonPSmith/EfCoreInAction) that accompanies my book. Choose branch Chapter05 (each chapter has a branch) and run the application locally. You'll see a manage button next to each book, with several CUD commands.

Method 1: Directly Using EF Core Commands

The most obvious way is to use EF Core methods to perform database updates. Below is a method that adds a new review to a book, using the review information provided by the user. Note: ReviewDto is a data transfer object that holds the information returned after the user fills in the review.

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;
}

Note: The AddReviewToBook method is in a class called AddReviewService, which resides in my ServiceLayer. This class is registered as a service and has a constructor that takes the application's DbContext injected via DI. The injected value is stored in the private field _context, which the AddReviewToBook method can use to access the database.

This adds the new review to the database, which works, but there is another way to structure this using a more DDD approach.

Method 2: DDD-Style Entity Classes

EF Core provides us with a new place to write your update code – inside entity classes. EF Core has a feature called backing fields that makes building DDD entities possible. Backing fields allow you to control access to any relationship structure. This was actually not possible in EF6.x.

DDD talks about aggregates (mentioned earlier), where all changes to aggregates can only be made through methods in the root entity, which I call access methods. In DDD terminology, reviews are aggregates of the Book entity, so we should add a review through an access method in the Book entity class called AddReview. This way, the above code becomes a method in the Book entity:

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;
}

The AddReview access method in the Book entity class looks like this:

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.");
        }
    }
    //...
}

This method is more complex because it can handle two different situations: one where Review is already loaded and one where it hasn't been loaded yet. But if the reviews haven't been loaded, it is faster than the original approach because it uses the "create relationship via foreign key" method.

Since the access method's code is inside the entity class, it can be more complex if needed, and it will be the only version of the code you need to write. With method 1, you could repeat the same code in different places whenever you need to update the Review collection of a Book.

Note: I wrote an article titled "Creating Domain-Driven Design entity classes with Entity Framework Core" that is entirely about DDD-style entity classes. That article covers this topic in more detail. I also updated an article on how to write business logic with EF Core to use the same DDD-style entity classes.

Why don't the methods inside the entity class call SaveChanges? In method 1, a method contains all parts: a) loading the entity, b) updating the entity, and c) calling SaveChanges to update the database. I can do this because I know it is invoked by a web request and that's all I want to do. For DDD entity methods, you cannot call SaveChanges inside the entity method because you cannot be sure the operation is complete. For example, if you are loading a book from backup, you might want to create the book, add an author, add any reviews, and then call SaveChanges so that everything is committed to the database together.

Method 3: GenericServices Library

There is a third way. I noticed a standard pattern when using CRUD commands in ASP.NET applications I built, and as early as 2014, I built a library called GenericServices that worked with EF6.x. In 2018, I built a more comprehensive version for EF Core called EfCore.GenericServices – see this article about EfCore.GenericServices:

These libraries do not really implement the repository pattern, but instead act as an adapter pattern between entity classes and what the front end actually needs. I used the original EF6.x, and GenericServices saved me months of tedious front-end code writing. The new EfCore.GenericServices is even better because it can work with both standard-style entity classes and DDD-style entity classes.

3. Which Method Is Better

Method 1 (directly using EF Core code) requires the least code, but there is potential for duplication because different parts of the application might apply CUD commands to an entity. For example, when users make changes, you might update through the ServiceLayer, but an external API might not go through the ServiceLayer, so you would have to repeat the CUD code.

Method 2 (DDD-style entity classes) puts the key update parts inside the entity class, so the code is available to anyone who can get an entity instance. In fact, because DDD-style entity classes "lock down" access to properties and collections, everyone must use the AddReview access method of the Book entity if they want to update the Review collection. For many reasons, this is the method I want to use in future applications (see my article for a discussion of pros and cons). Its (slight) disadvantage is that it requires a separate load/save part, meaning more code.

Method 3 (GenericServices library) is my preferred method, especially now that I have built the EfCore.GenericServices version that handles DDD-style entity classes. As you can see in the article about EfCore.GenericServices, this library greatly reduces the amount of code you need to write in web/mobile/desktop applications. Of course, you still need to access the database in your business logic, but that's another matter.

7 Organizing 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 using EF Core directly, you can put your data access code anywhere, which can make it hard for you or other team members to find it. Therefore, I recommend having a clear plan for where your code goes and sticking to it.

The diagram below shows a layered or hexagonal architecture, displaying only three assemblies (I've omitted business logic; in a hexagonal architecture, you would have more assemblies). The three assemblies shown are:

  • ASP.NET Core. This is the presentation layer, providing HTML pages or a web API. It contains no database access code but relies on various methods in the ServiceLayer and BusinessLayer.

  • ServiceLayer. It contains database access code, including query objects and methods for create, update, and delete. The service layer uses the adapter pattern and command pattern to connect the data layer and the ASP.NET Core (presentation) layer.

  • DataLayer. It contains the application's DbContext and entity classes. DDD-style entity classes then contain access methods to allow modifications to root entities and their aggregates.

Note: The previously mentioned libraries GenericServices (EF6.x) and EfCore.GenericServices (EF Core) are actually libraries that provide ServiceLayer functionality, acting as an adapter pattern and command pattern between the DataLayer and your web/mobile/desktop application.

What I want to convey from this diagram is that by using different assemblies, a simple naming convention (see the bold Book in the diagram), and folders, you can build an application where your database code is isolated and easy to find. As your application grows, this can be crucial.

8 Unit Testing EF Core Methods

The final aspect to consider is unit testing applications that use EF Core. One advantage of the repository pattern is that you can replace it with a mock during testing. Using EF Core directly loses the option of mocking (technically you can mock EF Core, but it's hard to do well).

Fortunately, modern EF Core has improved, and you can use an in-memory database to simulate the database. In-memory databases are faster to create and have a default starting point (i.e., empty), making it much easier to write tests against them. See my article "Using in-memory databases for unit testing EF Core applications" for details on how to do this, along with a NuGet package called EfCore.TestSupport that provides methods to make writing EF Core unit tests faster.

9 Conclusion

My last project using the Rep/UoW pattern dates back to 2013, and I haven't used it since. I've tried a few methods: a custom library called GenericServices based on EF6.x, and now a more standard custom library called EfCore.GenericServices based on EF Core implementing query objects and DDD-style entity methods. They make writing code easier and generally perform well. If they are slow, it's easy to locate and performance-tune individual database accesses.

In my book for Manning Publications, there is a chapter on performance tuning an ASP.NET Core application that "sells" books. The process uses query objects and DDD entity methods and shows that it can produce well-performing database access (see my article "Entity Framework Core performance tuning – a worked example").

My own work uses query objects for reads and DDD-style entity classes with their access methods for CUD and business logic. I do need to use these in a proper application to truly know if they work. Stay tuned to my blog for more about DDD-style entity classes, the architecture that benefits from them, and perhaps a new library :).

Happy coding!

Keep Exploring

Related Reading

More Articles