Read time: 8 minutes
The Repository Pattern is everywhere in .NET codebases.
Walk into any “enterprise” C# shop and you’ll find layers of IRepository<T>
interfaces, generic base classes, and dependency injection configurations that make simple database operations feel like rocket science.
The worst part? Most developers think this is “best practice.” They’ve been told the Repository Pattern provides “clean architecture,” “testability,” and “database independence.”
But here’s what they don’t tell you: the Repository Pattern is solving problems that don’t exist while creating new ones that absolutely do.
Entity Framework Core already provides everything the Repository Pattern promises—and does it better. You’re building abstractions on top of abstractions, writing 8x more code, and calling it “clean.”
Today I’m going to show you why the Repository Pattern has become cargo cult programming in the .NET world, and what you should do instead.
If you’ve ever wondered whether all those repository interfaces are actually worth it, this one’s for you.
What is the Repository Pattern?
The Repository Pattern creates an abstraction layer between your .NET application and your data storage.
Your application code depends only on an IRepository interface—it has no idea whether data comes from SQLite, Azure SQL, or any other source.
Each database gets its own concrete implementation that handles the specific details of talking to that particular storage system.
The promise is beautiful: write your business logic once, and it works with any database. Need to switch from SQLite to Azure SQL? Just change one line in your dependency injection configuration.
Your .NET app stays database-agnostic, your code follows SOLID principles, and testing becomes trivial since you can easily mock the interface.
Now let’s see one common way to implement and use the repository pattern these days.
The abstractions
To make things as reusable as possible, we’ll start by introducing a base entity with a single ID property:
Now we can implement our generic IRepository interface, taking advantage of our base entity:
Finally, let’s add a default Entity Framework Core repository implementation, which hopefully can handle the most common data access scenarios:
Great. Now, let’s add more concrete stuff.
The Games repository
Time to add our first entity, Game, meant to represent one of the games in our catalog.
For completeness, let’s also add our Genre entity, which represents one of our supported game genres and is associated with every game:
Now, ideally, we would just use **IRepository
Therefore, we’ll need a new interface:
And a new concrete implementation:
Notice how Repository
Now let’s use our new repository components.
Using the repository
We can now use our handy IGamesRepository interface across all our endpoints or controllers.
I usually keep these endpoints in separate feature folders, as I covered in my Vertical Slice Architecture article, but here I’ll place them in one file for easier reading:
And let’s not forget to register the repository, along with the EF Core DBContext, on application startup:
Different teams would implement their repositories in slightly different ways, but you get the idea.
Now, there’s something most devs miss.
What most devs miss
It turns out that EF Core’s DBContext is already a repository implementation, clearly mentioned in the class documentation:
The DBContext is meant to abstract your code from the underlying DB engine, exactly what the repository pattern is meant to bring to the table.
By adding a repository layer on top of the DBContext, you are:
- Building an abstraction on top of the abstraction
- Writing 8 times more code than needed for simple scenarios
- Ready to swap your DB engine, but that might never happen
What if you just take advantage of the built-in repository capabilities of DBContext?
Using the DBContext directly
Let’s inject the DBContext directly into our endpoints and simplify the code accordingly:
Notice the new flexibility allowed across all endpoints and the fact that we no longer need to maintain a bunch of abstractions that were there just in case:
There’s nothing as satisfying as deleting code that’s no longer needed :)
Now let’s address a couple of common questions.
What if I change my DB later?
You’re not going to change databases. In 15+ years of building .NET applications, I’ve seen this happen exactly zero times in production.
Once you’re running with real data, real users, and real business processes, switching databases becomes a massive undertaking involving data migration, schema differences, and retraining teams.
The repository pattern doesn’t solve any of that—it just makes you feel better about a problem that doesn’t exist.
And, if you do change databases, you most likely want to take advantage of the strengths of your new database, so you are up for a full rewrite anyway.
Plus, today, even that is not that much of a big deal given the massive amount of assistance you’ll get from your AI companion like Cursor or GitHub Copilot to get such a task done in a fraction of the time that it would take you to maintain all those repositories for years.
What about unit testing?
A common argument for using the repository pattern is that it allows for isolating your database-specific logic so that you can easily unit test your ASP.NET Core endpoints or controllers.
However, you should realize that by unit testing your controllers, you are testing the wrong thing, as I mentioned in this previous article.
Save those unit tests for complex business logic and algorithms, which is where the real value lives. Your controllers are better served with a small set of integration tests.
Wrapping Up
The repository pattern was never the answer to .NET’s data access problems. It’s a solution in search of a problem, creating complexity where none needs to exist.
EF Core’s DbContext already is your repository. It provides abstraction and everything else the repository pattern promises—without the ceremony, without the boilerplate, and without the maintenance burden.
Stop building abstractions on top of abstractions. Use the platform. Write clear, direct code that does what it says and says what it does.
Your future self will thank you when you’re shipping features instead of maintaining repository interfaces that add zero value.
And that’s all for today.
See you next Saturday.
P.S. If you want to see how to build complete production-ready APIs using EF Core and zero unnecessary abstractions, check out my ASP.NET Core Essentials course. Just solid, maintainable code that actually works.
Whenever you’re ready, there are 4 ways I can help you:
-
Containers & .NET Aspire: Build production-ready apps from day 1 and leave ‘but it works on my machine’ behind.
-
Browse all courses: Everything you need to build, deploy, and maintain production .NET applications.
-
Get the full source code: Download the working project from this newsletter, grab exclusive course discounts, and join a private .NET community.
-
Promote your business to 25,000+ developers by sponsoring this newsletter.