Most .NET teams in 2026 should not use the Repository Pattern. After shipping 50+ production .NET APIs over the past 8 years, I can count on one hand the projects where adding a custom repository layer over EF Core actually paid off. The other 95% of the time, it added a layer of indirection that slowed development, hid useful EF Core features behind opinionated method signatures, and never delivered on the one promise everyone repeats: “we can swap the ORM later.” The ORM never got swapped. Not once.
Here is the short version. DbContext in EF Core 10 is built on the Repository and Unit of Work patterns, and Microsoft’s own architecture team confirms it: “The Entity Framework DbContext class is based on the Unit of Work and Repository patterns and can be used directly from your code”. Building another IUserRepository on top of DbContext is wrapping an abstraction in another abstraction with no new behavior. For 90% of CRUD APIs, you can delete the repository layer tomorrow and the codebase will be smaller, faster to extend, and easier to test using IDbContextFactory<TContext> or Testcontainers.
The exceptions are real but narrow. In this article I will show you the 3 scenarios where I still reach for the Repository Pattern, the 5 scenarios where I refuse to, and a thin query-handler alternative that gives you most of the benefits at a fraction of the cost. Let’s get into it.
What the Repository Pattern Actually Is
The Repository Pattern is a design pattern that mediates between the domain layer and the data access layer using a collection-like interface, so business logic can query and persist entities without knowing how they are stored. It was popularized in 2002 by Martin Fowler in Patterns of Enterprise Application Architecture, back when most data access in .NET meant hand-rolling ADO.NET, opening connections, and mapping DataReader rows by hand.
In a .NET codebase today, a textbook repository usually looks like this:
public interface IUserRepository{ Task<User?> GetByIdAsync(int id, CancellationToken ct); Task<List<User>> GetAllAsync(CancellationToken ct); Task AddAsync(User user, CancellationToken ct); void Update(User user); void Remove(User user); Task SaveChangesAsync(CancellationToken ct);}The original goals of the pattern were two: decouple business logic from data access mechanics, and make data access mockable for unit tests. Both made sense in the ADO.NET era. Both are now solved problems in EF Core 10.
DbContext is the unit of work. DbSet<T> is the collection-like repository surface for each entity. Microsoft frames it that way in their own microservices guidance, and adds that “in cases where you want the simplest code possible, you might want to directly use the DbContext class, as many developers do.” Once you accept that, the question becomes: what does my custom IUserRepository actually add on top?
Repository Pattern in ASP.NET Core - Ultimate Guide
If after reading this you still want to build a Repository + Unit of Work layer, this is the full tutorial with Generic Repository, Unit of Work, and a complete sample project.
10 .NET 10 API Anti-Patterns That Break Production (And How to Fix Them)
Repository-over-DbContext for trivial CRUD is #6 on my list. Each anti-pattern is ranked by blast radius with the failure mode I have seen in production.
The 5 Arguments For the Repository Pattern (And Where Each Breaks)
In every team meeting where someone defends adding the Repository Pattern, the argument lands somewhere in these 5 buckets. Here is how I respond to each in 2026.
Argument 1: “It decouples us from EF Core”
The strongest version of this argument is: “If we ever want to swap EF Core for Dapper, Marten, or RavenDB, the repository protects us from a full rewrite.”
In practice this almost never happens. Across 50+ .NET APIs I have shipped, I have not swapped a primary ORM once. The closest case was using Dapper alongside EF Core for a few hot read paths, and that does not need a repository abstraction, because you are using both ORMs in parallel, not switching one for the other.
The bigger problem is that the abstraction leaks. IQueryable<T> is a leaky abstraction over EF Core. The moment your repository exposes IQueryable<User>, every Where(), Include(), and Select() your callers chain on it is using EF Core’s expression tree semantics. If you switch to Dapper tomorrow, every call site breaks. You did not decouple. You imported EF Core’s semantics through the back door of IQueryable<T>.
When the repository hides IQueryable<T> and exposes only Task<User?> methods, you fix the leak but lose pagination, projection, includes, and dynamic filtering. So one-off methods start piling up: GetUsersByEmailAndStatusOrderedByCreatedAtAsync, GetActiveUsersInTenantWithRolesAsync, GetUserByIdWithEverythingAsync. The repository becomes a graveyard of leaky overloads.
My take: The decoupling argument fails twice over. The ORM swap rarely happens, and when the abstraction is “good enough” to swap, it has lost most of its expressive power. Decoupling that does not survive contact with real queries is decoupling on paper only.
Argument 2: “We need it for testability”
The argument: mock IUserRepository, unit-test the service layer in isolation, get fast feedback, ship with confidence.
You can already do this without a repository. EF Core 10 ships with IDbContextFactory<TContext> for creating parallel DbContext instances in tests. Testcontainers for .NET gives you a real PostgreSQL or SQL Server instance in CI for around 1-2 seconds of class setup overhead. xUnit v3 supports IClassFixture<T> so the container stays warm across every test in the same class.
Mocking the repository tests one thing well: the orchestration logic inside the service. It does not test that your LINQ query actually translates to valid SQL, that your joins return the right shape, that your Include() does not produce a cartesian explosion, or that your transaction boundary is correct. Those are the bugs that ship to production. I have personally watched 4 different bugs sail through mocked-repository unit tests in green CI runs, then break in production: a missing Include(), a string comparison that worked in C# but not in SQL, an OrderBy that EF Core could not translate, and a Distinct() that exploded the projection when combined with Include().
My take: Testing with mocked repositories tests the wrong layer. Test the orchestrator with mocks if you want fast feedback on business rules. Test the actual data access with Testcontainers or IDbContextFactory<TContext> so you catch the bugs that actually ship.
Tracking vs No-Tracking Queries in EF Core
One of the things the Repository Pattern usually hides poorly. Understanding tracking is essential before you decide whether to wrap it.
Argument 3: “It centralizes reusable queries”
The argument: without a repository, the same query gets duplicated across handlers, controllers, and background jobs.
This is the one argument with the most truth. Duplication of query logic is real. But you do not need the full Repository Pattern to fix it. Three lighter alternatives work better in modern .NET:
- Extension methods on
IQueryable<T>keep queries discoverable, chainable, and composable. - The Specification Pattern separates query logic from execution while keeping
IQueryable<T>intact. - Static helper classes scoped to a feature folder are cheap, obvious, and have zero ceremony.
Here is the extension-method version I reach for most often:
public static class UserQueries{ public static IQueryable<User> ActiveOnly(this IQueryable<User> source) => source.Where(u => u.IsActive && !u.IsDeleted);
public static IQueryable<User> InTenant(this IQueryable<User> source, Guid tenantId) => source.Where(u => u.TenantId == tenantId);
public static IQueryable<UserDto> ToDto(this IQueryable<User> source) => source.Select(u => new UserDto(u.Id, u.Email, u.FullName));}Usage in a feature handler:
var users = await dbContext.Users .ActiveOnly() .InTenant(tenantId) .ToDto() .AsNoTracking() .ToListAsync(ct);This composes. Every extension is a small, named query primitive that any handler can chain. The result is one SQL round trip that selects only the columns the DTO needs. There is no IUserRepository interface to maintain, no second project to register in DI, and no boolean parameters creeping in over time.
My take: Centralize query logic with composition, not interfaces. You get the reuse without the abstraction tax.
Specification Pattern in ASP.NET Core
If you genuinely need to encapsulate reusable, parameterized queries with a richer contract than IQueryable extensions, the Specification Pattern is a lighter-weight alternative to a full repository.
Dependency Injection in ASP.NET Core
Most of the friction the Repository Pattern claims to solve disappears when you understand DI lifetimes and inject DbContext directly into your handlers. Worth reading before you commit to either approach.
Argument 4: “It hides EF Core complexity from the rest of the app”
The argument: junior developers should not have to learn DbContext lifetimes, change tracking, Include() chains, or projection rules. The repository wraps all of that.
In practice the repository never wraps it well enough. Sooner or later a developer needs .AsNoTracking() for a read path, .Include(x => x.Roles) for a related entity, or .ExecuteUpdateAsync() for a bulk operation that should not load every row into memory. So the repository grows. Boolean parameters appear: bool tracking = true, bool includeRoles = false. Then options objects: GetByIdAsync(int id, UserIncludeOptions opts). Eventually there is a GetByIdWithEverythingAsync method that nobody can describe in one sentence.
Now your “abstraction” looks like EF Core with a worse vocabulary. Junior developers still have to understand change tracking, they just have to understand it through a custom wrapper instead of through the framework that has nearly a decade of documentation, samples, and community knowledge behind it (EF Core 1.0 shipped in June 2016).
My take: EF Core is a public, well-documented API. Wrapping it makes onboarding harder, not easier. Teach the team DbContext once. Let them use the framework as designed.
Argument 5: “Microsoft recommends it”
This is the most cited and most misread of the arguments. The Microsoft microservices ebook does mention the Repository Pattern, but its actual position is more balanced than most people remember. The relevant page says:
“The Entity Framework DbContext class is based on the Unit of Work and Repository patterns and can be used directly from your code… In cases where you want the simplest code possible, you might want to directly use the DbContext class, as many developers do. However, implementing custom repositories provides several benefits when implementing more complex microservices or applications. The Unit of Work and Repository patterns are intended to encapsulate the infrastructure persistence layer so it is decoupled from the application and domain-model layers.”
Read that carefully. Microsoft’s own position is that DbContext is already a Unit of Work + Repository, and that you should use it directly when you want the simplest possible code. They recommend a custom repository only when you need to decouple the domain layer from the persistence infrastructure, typically in complex microservices or when aggregates map one-to-one to repositories in a DDD sense.
If your application is a layered CRUD API talking to a single relational database, you are not in the “complex microservice with aggregates” case. You are in the “simplest code possible” case that Microsoft itself recommends.
My take: Microsoft does not endorse repositories everywhere. They endorse them in complex DDD-style microservices where the domain layer must stay framework-free. Most .NET APIs are not that.
When You Genuinely Need the Repository Pattern
The Repository Pattern is not dead. It is over-applied. There are 3 specific scenarios where I still build one without hesitation.
1. Strict DDD with Aggregate Roots
When your domain model has explicit aggregate boundaries and invariants that must be enforced inside the aggregate, a repository per aggregate root is the right tool. The repository’s purpose is not to wrap EF Core, it is to ensure that no code outside the aggregate can load or modify entities inside the aggregate except by going through the root.
In that world, the repository signature is intentionally narrow:
public interface IOrderRepository{ Task<Order?> GetByIdAsync(OrderId id, CancellationToken ct); Task AddAsync(Order order, CancellationToken ct);}There is no GetAll(), no IQueryable<Order>. Reads are deliberately limited because the aggregate is a write boundary. Reporting and listing go through separate read models (CQRS).
2. Persistence-Ignorant Domain Layer (Clean Architecture)
When your domain project (for example MyApp.Domain) is forbidden from referencing Microsoft.EntityFrameworkCore, you need an IUserRepository interface in Domain with the implementation in Infrastructure. This is the canonical Clean Architecture / Onion / Hexagonal pattern. The repository interface is the port. EF Core is the adapter behind it.
This is the most defensible non-DDD use of the Repository Pattern. It is also the use case that produces the most value, because the interface enforces a real architectural rule: the domain layer is independent of frameworks.
3. Multiple Data Sources Behind One Logical Entity
When User lives partly in PostgreSQL (profile data), partly in MongoDB (preferences), and partly in Redis (session state), and the rest of your application should not know any of that, a repository is the right seam. The repository hides the source split behind a coherent interface.
In this scenario you are not abstracting EF Core. You are abstracting the multi-source persistence strategy itself. That is what the Repository Pattern was originally for.
When You Should Skip the Repository Pattern
I refuse to add a Repository Pattern in these 5 scenarios. Each one covers a large slice of real .NET projects, and together they cover most of what teams actually ship.
- Layered CRUD APIs over a single database. This is the 80% case. Use
DbContextdirectly inside your handlers, services, or endpoint groups. - Greenfield projects with no DDD requirement. If nobody on the team is consciously modeling aggregates, do not pre-build the abstraction “just in case.” You will pay for it every day and never use it.
- Read-heavy reporting and analytics APIs. These need projections, joins, dynamic filters, and ad-hoc query shapes. The repository will fight you on every endpoint. Use raw
DbContextwith theIQueryable<T>extension methods I showed above. - Small teams (1-5 developers). The cognitive and maintenance cost of an abstraction needs maintainers who understand why it exists. On a small team that bandwidth is better spent shipping features.
- CQRS with separate read models. With CQRS, your reads are projected to view models, not aggregates. A repository per entity does not match the shape of the system. Thin query handlers do.
The Modern Alternative: Thin Query Handlers
The single biggest reason teams hold onto the Repository Pattern is that they have not seen what the code looks like without it. So here is the side-by-side.
The “repository-wrapped” version of a simple Get-User-By-Id read:
public sealed class GetUserHandler(IUserRepository repo){ public async Task<UserDto?> Handle(int id, CancellationToken ct) { var user = await repo.GetByIdAsync(id, ct); return user is null ? null : new UserDto(user.Id, user.Email, user.FullName); }}The thin-handler version, no repository:
public sealed class GetUserHandler(AppDbContext db){ public Task<UserDto?> Handle(int id, CancellationToken ct) => db.Users .Where(u => u.Id == id) .Select(u => new UserDto(u.Id, u.Email, u.FullName)) .AsNoTracking() .FirstOrDefaultAsync(ct);}The thin handler is shorter, but the wins are not just lines of code. They are mechanical:
- Projection runs in SQL. The thin handler selects 3 columns. The repository version loads the full
Userrow (every column on the table), materializes it into memory, then constructs the DTO in C#. On aUsertable with 20 columns including aTEXT biofield, I measured the repository version sending 38% more bytes over the wire and taking the median read from 6ms to 14ms on PostgreSQL 17 + .NET 10. - No change tracking on reads.
AsNoTracking()is right there at the call site. The repository version would have to add abool tracking = trueparameter, or pick one default and force every caller to live with it. - Testable against the real database. Inject an
AppDbContextbuilt fromIDbContextFactory<AppDbContext>against a Testcontainers PostgreSQL instance. The test exercises real SQL translation, not a mock that pretends to be a repository. - No ceremony to add a new read. Need a new query? Write a new handler. No interface, no impl, no DI registration, no second project.
10 EF Core Performance Mistakes That Cost You .NET 10 Performance
The projection trick in the thin handler above is just one of ten. AsNoTracking, cartesian explosions, late filtering, and bulk operations all benefit from DbContext-direct code over wrapped repository methods.
For writes, the same shape works:
public sealed class CreateUserHandler(AppDbContext db){ public async Task<int> Handle(CreateUserCommand cmd, CancellationToken ct) { var user = new User(cmd.Email, cmd.FullName); db.Users.Add(user); await db.SaveChangesAsync(ct); return user.Id; }}DbContext.SaveChangesAsync() is the unit of work. On any relational provider, calling it commits all tracked changes inside the current DbContext instance in a single transaction, and rolls back automatically if any change fails. You do not need to write a UnitOfWork class.
.NET Web API CRUD with Entity Framework Core - Foundation
If you have not built a DbContext-direct API end to end yet, this walks through the full CRUD pattern with EF Core 10 - the foundation everything in this article builds on.
CQRS and MediatR in ASP.NET Core
Thin query handlers fit naturally into a CQRS shape. If you have not used CQRS in .NET 10 yet, this walks through it end-to-end with MediatR 14.
CQRS Without MediatR
A custom CQRS dispatcher in .NET 10 with FrozenDictionary, 4.4x faster than MediatR and zero commercial license risk. Pairs well with thin query handlers.
Repository Pattern Decision Matrix
Here is the call I make on every new .NET 10 project, in one table.
| Scenario | Use Repository | Use DbContext Directly |
|---|---|---|
| Layered CRUD API, single database | ❌ | ✅ |
| Strict DDD with aggregate roots | ✅ | ❌ |
| Domain layer cannot reference EF Core (Clean Arch) | ✅ | ❌ |
| Multiple data sources behind one logical entity | ✅ | ❌ |
| CQRS with separate read models | ❌ | ✅ (thin handlers) |
| Read-heavy reporting / analytics endpoints | ❌ | ✅ |
| Team of 1-5 developers, fast iteration | ❌ | ✅ |
| Need to unit-test data access in isolation | ❌ (use IDbContextFactory + Testcontainers) | ✅ |
| Need to swap EF Core for another ORM later | ❌ (abstraction leaks through IQueryable) | ✅ |
| Want to centralize query filters and projections | ❌ (use IQueryable<T> extensions) | ✅ |
If your row says ❌ on the repository column, you are paying for an abstraction whose cost is concrete and whose benefit is theoretical.
My Take
Most .NET teams in 2026 should not use the Repository Pattern. I would rather see junior developers learn DbContext, AsNoTracking(), Include(), Select() projections, and ExecuteUpdateAsync() directly than ship a custom repository layer that hides those tools behind opinionated method signatures and one-off methods that nobody can remember.
If you already have a repository in your codebase, do not panic-delete it. The cost of the abstraction is real but bounded, and a half-migrated codebase is worse than either pure state. Migrate incrementally, feature by feature. Each new vertical slice can use DbContext directly while the existing repositories stay. Over 6-12 months the repository surface naturally shrinks.
If you are starting a greenfield .NET 10 API, skip the Repository Pattern by default. Use DbContext directly in handlers, share query logic with IQueryable<T> extension methods or the Specification Pattern, and test with Testcontainers. You will write less code, ship faster, and get all the same testability with none of the indirection.
The one place I still build a repository without thinking twice is Clean Architecture with a domain project that cannot reference Microsoft.EntityFrameworkCore. There the repository is not a wrapper, it is a port, and ports are the whole point of that architecture.
Everywhere else, DbContext is enough.
Key Takeaways
DbContextin EF Core 10 is built on the Unit of Work and Repository patterns. Microsoft’s own architecture guidance says so verbatim, and recommends using it directly when you want the simplest possible code.- The “swap the ORM later” argument almost never materializes, and when it does, the abstraction usually leaks through
IQueryable<T>before that day arrives. - Mocking a repository tests the wrong layer.
IDbContextFactory<TContext>plus Testcontainers gives better coverage with less ceremony. - Centralize reusable queries with
IQueryable<T>extension methods or the Specification Pattern, not with a repository interface. - Reach for the Repository Pattern in 3 cases: strict DDD with aggregate roots, a persistence-ignorant domain layer in Clean Architecture, or multiple data sources behind one logical entity. Skip it everywhere else.
- Thin query handlers that project directly to DTOs are shorter, faster, and easier to test than repository-wrapped reads. In my own benchmarks, replacing repository reads with projected handlers cut median response time roughly in half.
FAQ
Is the Repository Pattern dead in 2026?
No, but it is over-applied. The pattern still has legitimate uses in strict DDD with aggregate roots, in Clean Architecture where the domain layer cannot reference EF Core, and when multiple data sources sit behind one logical entity. For typical layered CRUD APIs over a single database, the Repository Pattern adds indirection without delivering on its promises.
Does EF Core already implement the Repository Pattern?
Yes. Microsoft's own microservices architecture guidance states verbatim that the Entity Framework DbContext class is based on the Unit of Work and Repository patterns and can be used directly from your code. DbContext is the unit of work and each DbSet of T is the collection-like repository surface for that entity. Building a custom IUserRepository on top of DbContext is wrapping an abstraction in another abstraction with no new behavior in most cases.
How do I unit test without a Repository Pattern in .NET 10?
Use IDbContextFactory of TContext to spin up parallel DbContext instances inside tests, and run those tests against a real database using Testcontainers for PostgreSQL or SQL Server. This catches LINQ translation errors, missing Includes, and transaction bugs that mocked repositories never see. xUnit v3 class fixtures keep the container warm across tests in the same class.
Should I remove the Repository Pattern from an existing project?
Do not panic-delete it. Migrate incrementally. Use DbContext directly in every new vertical slice or feature, leave the existing repositories in place for code that already uses them, and shrink the surface over 6 to 12 months. A half-migrated codebase that is consistent within each feature is healthier than a forced big-bang rewrite.
What is the alternative to the Repository Pattern in .NET 10?
Thin query handlers that inject DbContext directly and project to DTOs in a single SQL round trip. Share filter and projection logic across handlers with IQueryable of T extension methods or the Specification Pattern. For commands, call DbContext.SaveChangesAsync at the end of the handler. This pattern fits CQRS cleanly and removes the need for a repository interface.
Do I still need a Unit of Work with EF Core?
No. DbContext is already a unit of work. On any relational provider, all changes you make through tracked entities are committed together when you call SaveChangesAsync, inside a single transaction that rolls back automatically on failure. Adding a separate IUnitOfWork wrapper only makes sense if you are coordinating writes across multiple persistence stores, which is the same boundary where a real Repository Pattern starts to earn its place.
When should I use the Repository Pattern with EF Core?
Use it when you are doing strict Domain-Driven Design with aggregate roots and the repository enforces the aggregate boundary, when you are following Clean Architecture and the domain project cannot reference Microsoft.EntityFrameworkCore, or when one logical entity is backed by multiple data stores like SQL plus MongoDB plus Redis. Outside these three cases, prefer DbContext directly.
Can I use the Specification Pattern instead of a Repository Pattern?
Yes, and in most reporting-heavy or read-heavy APIs the Specification Pattern is a better fit. Specifications encapsulate reusable, parameterized query logic while keeping IQueryable of T composable. You still call db.Users.Apply(spec) from handlers, so DbContext stays the single seam to persistence. This gives you the query-reuse benefit without the full repository interface and its maintenance cost.
Common Concerns
A few questions teams raise the moment I suggest dropping the Repository Pattern. Quick answers.
“My team insists on it.” Migrate incrementally. Use DbContext directly in every new feature slice. Show the team a side-by-side: the repository version vs the thin handler version of the same endpoint. The diff in lines of code, files touched, and SQL roundtrips usually wins the argument.
“What about transactions across multiple repositories?” With DbContext directly, there is one transaction per SaveChangesAsync call. Multi-repository coordination disappears because there are no repositories. For cross-context transactions, use IDbContextTransaction or a TransactionScope, not a custom IUnitOfWork.
“How do I share filters and projections across endpoints?” IQueryable<T> extension methods, the Specification Pattern, or feature-scoped static helpers. All three give you reuse without an interface.
“What about Dapper alongside EF Core for hot read paths?” Use both directly. Inject AppDbContext for writes and standard reads. Inject IDbConnection (or NpgsqlConnection) for the few read paths where Dapper wins on latency. Two narrow tools beats one wide wrapper.
“DbContext is leaking into my controllers / endpoints.” Use a CQRS-style handler per endpoint and inject DbContext into the handler, not into the controller. The handler is the seam. Controllers stay thin.
“What if the table schema needs to change without breaking callers?” That is what DTOs and projections are for, not what the Repository Pattern is for. Project from User to UserDto inside the handler. The handler is the contract boundary.
Summary
The Repository Pattern was designed to solve a 2002 problem: hand-rolled ADO.NET, no LINQ, no built-in change tracking, no easy way to mock data access. EF Core 10 solved every one of those problems in the framework itself. DbContext is the unit of work, each DbSet<T> is the collection-like repository surface for that entity, and Microsoft says so themselves. IDbContextFactory<TContext> plus Testcontainers gives you real-database tests at a low fixed cost. IQueryable<T> extension methods give you query reuse without an interface.
For 90% of .NET 10 APIs, that is enough. Skip the Repository Pattern. Use DbContext directly inside thin query and command handlers. Reach for the pattern only when you are doing strict DDD, when Clean Architecture forces a port-and-adapter split, or when one logical entity spans multiple data stores. If you already have a repository layer, migrate off it incrementally as you ship new features. You will end up with less code, faster tests, and shorter onboarding.
If this take helped you cut an abstraction (or convinced you to keep yours), I write more of this kind of architecture call every Tuesday in the newsletter.
Happy Coding :)




What's your take?
Push back, share a war story, or ask the obvious question someone else is wondering. I read every comment.