DEV Community

Cover image for EF Core Is Already a Repository. Stop Wrapping It in Another One.
qodors
qodors

Posted on • Originally published at linkedin.com

EF Core Is Already a Repository. Stop Wrapping It in Another One.

Open a lot of .NET projects and you'll find the repository pattern sitting on top of Entity Framework Core. IProductRepository, GetById, Add, Save, the whole set. Underneath, every method just calls the EF Core DbContext and passes the result straight back. The wrapper adds a name and nothing else.

So do you need the repository pattern with EF Core? For most apps, no. EF Core already gives you one. DbSet is a repository. It already does the thing the pattern is for.

The reason this keeps happening is that the repository pattern got taught alongside EF, so people assume you need one to use the other. You don't.

What the Pattern Was For
The repository pattern came before EF Core. It started in a time when data access meant hand-written SQL, SqlCommand, and mapping DataReader rows to objects by hand. Wrapping all of that behind an interface was worth it. It hid a real mess, and it let you swap what was underneath without the rest of the app noticing.

EF Core already does that. DbContext is the unit of work. DbSet is the repository. SaveChanges() is the commit. The pattern you're adding is one the tool already gives you.

What the Wrapper Actually Does
Here's the shape you see in most codebases:

public class ProductRepository : IProductRepository
{
   private readonly AppDbContext _db;
   public ProductRepository(AppDbContext db) => _db = db;
   public async Task<Product?> GetById(int id) =>
          await _db.Products.FindAsync(id);
   public async Task<List<Product>> GetAll() =>
          await _db.Products.ToListAsync();
   public void Add(Product product) => _db.Products.Add(product);
}
Enter fullscreen mode Exit fullscreen mode

Read what each method does. GetById calls FindAsync. GetAll calls ToListAsync. Add calls Add. It's a passthrough. Every line hands the call straight to EF Core and returns whatever comes back. You wrote an interface, a class, and a registration to rename methods that already existed. This is what people mean by a generic repository over EF Core, and it's the most common version you'll find.

Where It Starts to Hurt
The renaming is harmless enough. The real cost shows up the moment someone needs a query the repository didn't plan for.

Say you need products in a category, over a price, ordered by date, with the supplier included. With EF Core in the service you write that in one LINQ query and move on. Behind a repository you can't, because the service only sees the methods on the interface. So you do one of these:

Add a new method to the interface for this exact query, and do it again for the next one
Add a generic Find(Expression>) and hand IQueryable back out — at which point the repository is hiding nothing and you've just made EF harder to reach
Pull the whole table with GetAll() and filter in memory, which is how a repository quietly turns into a performance problem

Every one of those is worse than just using the DbContext. The wrapper that was supposed to simplify data access is now the thing standing between you and the query you need to write.

The We Can Swap the Database Argument
The usual defense is that the repository lets you swap the database later without touching the app. It almost never happens, and the abstraction doesn't deliver it anyway.

EF Core is already the layer that lets you change database providers. Switching from SQL Server to PostgreSQL is a provider and connection-string change, not a rewrite of your data access. The repository on top adds nothing to that. And if you ever moved to something EF doesn't support, your repository interfaces — built around EF's own behavior — wouldn't still work anyway. You'd be rewriting them too.

You're holding an abstraction for a swap that probably won't come, and that the abstraction wouldn't actually save you from.

Testing Is the One Fair Reason
The one real reason left is testing. Mocking a DbContext is awkward, so people put a repository in front of it to get a clean interface to mock. That's a real pain, and it's the strongest case for the pattern.

But there are lighter ways to handle it. The EF Core in-memory provider and SQLite in-memory both let you test against a real DbContext without a repository in the way, and they catch things a mock never will — because a mock only tests that you called the method you thought you called, not that the query actually works. If your only reason for the repository is testing, compare it to just testing the DbContext directly. Often that's the better test anyway.

Our Take
At Qodors, the generic Repository on top of EF Core is one of the most common things we find that's there out of habit. It doesn't break anything. It just sits there — a layer everyone has to go through, adding method names on top of methods that already worked.

The tell is simple. Open the repository and read the method bodies. If every one is a single line handing the call to the DbContext, the layer isn't abstracting anything. It's a rename with extra files.

There are real repository implementations that do genuine work — ones that combine sources, add caching, or hold logic that isn't just a query. Those earn their place. The passthrough wrapper around a single DbSet isn't one of them. Before you add a repository to an EF Core project, check whether you're solving a problem or repeating a pattern from a tutorial.

Quick Checklist

  • Read your repository method bodies — if they're one-line passthroughs to DbContext, the layer isn't doing anything

  • DbSet is already a repository and DbContext is already a unit of work

  • Database-swap portability comes from EF Core's providers, not from your wrapper

  • If the repository blocks a query, you'll hand IQueryable back out or filter in memory — both worse than direct EF

  • Testing is the fair reason — but the in-memory or SQLite provider often tests better than a mock

  • A repository that caches, combines sources, or holds real logic earns its place; a passthrough doesn't

EF Core came with a repository and a unit of work already built in. Wrapping it in another one to get names you like is a lot of files for a rename. Use the one it already gives you.

DotNet #CSharp #EFCore #EntityFramework #SoftwareArchitecture #RepositoryPattern #BackendDevelopment #DotNetCore #CleanCode #QodorsEdge

Written by the team at Qodors — we untangle over-abstracted .NET codebases for a living. → www.qodors.com

Top comments (0)