Repository Pattern vs Direct DbContext in ASP.NET Core: Which Should Your .NET Team Use in 2026?

Search for a command to run...

No comments yet. Be the first to comment.
Most of the .NET conversation about the Model Context Protocol is about building servers: expose your API as MCP tools, point Claude or VS Code at it, done. That is the half that gets written about. T

The first RAG system I shipped answered beautifully about concepts and failed completely on part numbers. Ask it "how do I reset a stuck deployment" and it nailed the answer. Ask it "what does error P

AutoMapper has been the default object mapper in .NET for over a decade, and for most of that time nobody thought about it. Version 15.0.0 changed that. It ships under a dual license now, with a free

You built the pipeline. Documents are chunked, embedded, and sitting in a vector store. Retrieval returns results. And your RAG answers still hallucinate in .NET, confidently telling users about a stu

You upgraded a working API to .NET 10, hit build, and Swashbuckle fell apart. Missing namespaces, OpenApiSchema refusing to compile, AddSecurityRequirement complaining about a delegate it never wanted

Coding Droplets
304 posts
Coding Droplets is your go-to resource for .NET and ASP.NET Core development. Whether you're just starting out or building production systems, you'll find practical guides, real-world patterns, and clear explanations that actually make sense.
From beginner-friendly tutorials to advanced architecture decisions. We publish fresh .NET content every day to help you grow at every stage of your career.
Few architectural debates are as persistent in the .NET community as this one: should your ASP.NET Core application wrap data access in a Repository Pattern, or should it use DbContext directly? Both camps have vocal proponents, and both are defending something real. The repository pattern vs direct DbContext question is not about dogma - it is a practical decision with genuine trade-offs that depend on your team size, codebase complexity, and long-term maintenance goals.
The full production implementation of both approaches - complete with service layer integration, unit-of-work variants, and a working test suite - is available on Patreon. The code maps directly to what enterprise .NET teams ship.
Understanding this decision in isolation is useful, but seeing how data access patterns fit into a complete production API - alongside authentication, validation, error handling, and testing - is where it really clicks. That end-to-end context is exactly what the ASP.NET Core Web API: Zero to Production course provides, starting from Chapter 3 where the repository interface in Domain and its EF Core implementation in Infrastructure are covered step by step.
The Repository Pattern introduces an abstraction layer between your domain or application logic and your data access technology. At its core, it is an interface - IProductRepository, for example - that defines data operations (find, add, update, remove) without exposing how those operations are implemented. Your application code depends on the interface; your infrastructure layer provides the EF Core implementation.
What it is not is a generic wrapper over DbSet<T> that mirrors every EF Core operation. That variant - the generic repository - adds indirection without adding abstraction. It does not hide EF Core; it just adds boilerplate between your code and it.
The distinction matters because the strongest arguments against repositories are almost always arguments against generic repositories specifically, not against domain-focused repositories that actually model your bounded context's operations.
Direct DbContext usage means injecting YourDbContext or IDbContextFactory<YourDbContext> into your service, handler, or endpoint - and using LINQ, AsNoTracking(), FindAsync(), and EF Core's full API surface directly, without any wrapping interface.
In small-to-medium codebases, this is honest and practical. EF Core's DbContext is already a Unit of Work; DbSet<T> is already a repository of sorts. Adding another layer on top of it risks hiding the very features - compiled queries, split queries, ExecuteUpdate, ExecuteDelete - that make EF Core 10 worth using in the first place. You can read more about those query performance features in EF Core 10 Query Performance: AsNoTracking, Compiled Queries and Split Queries Explained.
The case for the repository pattern is strongest in these specific conditions:
Your domain has meaningful aggregate boundaries. If you are practicing Domain-Driven Design and your application has aggregates - Order, Customer, Shipment - then a repository per aggregate root (IOrderRepository, ICustomerRepository) is architecturally correct. The repository is not a data access convenience; it is a domain contract. The implementation happens to use EF Core.
Your application will be tested extensively with unit tests. Mocking DbContext directly is technically possible but ergonomically painful. A repository interface with a clearly defined set of operations (GetByIdAsync, GetPagedAsync, AddAsync) is trivial to mock in Moq or NSubstitute. If your team writes unit tests for application layer handlers or services - as they should - repository interfaces make those tests clean and fast.
You are working in a Clean Architecture or layered structure where Domain and Infrastructure are separate projects. In this structure, Domain cannot reference Infrastructure. The only way to depend on data access from Domain or Application is through an interface. The repository interface lives in Application; the EF Core implementation lives in Infrastructure. This is the pattern used in Clean Architecture with CQRS + MediatR in ASP.NET Core: The Complete Guide (2026) and it is sound.
Your team plans to test data access behaviour in isolation. If you want to write handler unit tests that do not spin up a database, the repository interface is your seam.
Direct DbContext is the right default in these situations:
You are building a small-to-medium API without DDD constraints. If your application is primarily CRUD, your team is small, and your architecture is a single project or a simple layered app without bounded contexts - direct DbContext is cleaner. You get full access to EF Core's API, no abstraction tax, and faster onboarding for new developers.
You are building read-heavy endpoints where query composition matters. Complex filtered, sorted, and paginated queries are harder to model behind a repository interface. With direct DbContext you compose IQueryable<T> chains freely. The moment you put those queries behind an interface, you either leak IQueryable (defeating the abstraction) or you duplicate every filter combination as a new method. Neither is good.
You want to leverage EF Core 10's newest primitives freely. ExecuteUpdate, ExecuteDelete, JSON columns, complex type projections, and new bulk operations do not slot neatly into a Add/Update/Delete repository interface. You can include them, but you either expose them directly (leaking EF Core into the interface) or you miss them entirely.
You are prototyping or iterating quickly. Repository interfaces slow down early development. You should not pay an abstraction tax until you know the shape of your domain.
| Concern | Repository Pattern | Direct DbContext |
|---|---|---|
| Testability (unit tests) | ✅ Easy - mock the interface | ⚠️ Harder - DbContext mocking is verbose |
| EF Core feature access | ⚠️ Constrained by interface contract | ✅ Full access to all EF Core APIs |
| Abstraction clarity | ✅ Domain operations, not DB operations | ❌ EF Core leaks into application code |
| Onboarding cost | ⚠️ Higher - extra layer to understand | ✅ Lower - just inject and use |
| Query composition | ⚠️ Limited by interface design | ✅ Flexible IQueryable composition |
| Clean Architecture compatibility | ✅ Required for domain/infrastructure separation | ❌ Cross-project references break layering |
| Risk of over-engineering | ⚠️ Generic repositories are common misuse | ✅ None - WYSIWYG |
| Maintenance burden | ✅ Stable interfaces, swappable implementations | ✅ Simpler codebase, fewer layers |
The most common mistake teams make is building a GenericRepository<T> that wraps DbSet<T> with methods like GetAll(), GetById(), Add(), Update(), Delete() - and calling it "the repository pattern."
This pattern provides no real abstraction. It does not hide EF Core (it wraps it transparently). It does not model domain operations (CRUD is not a business language). It blocks access to EF Core features (you cannot call ExecuteUpdate through a generic interface without leaking it). And it creates maintenance overhead for no benefit.
If you find yourself writing a generic repository, stop. Either go direct DbContext or commit to domain-specific repository interfaces that model actual aggregate operations.
When teams want the testability benefit of an interface but resist full repository abstraction, the Specification Pattern is worth considering. A Specification<T> encapsulates a query predicate, include rules, and ordering. You pass specifications into a thin generic query executor that applies them against IQueryable<T>.
This approach gives you testable, reusable query objects without leaking EF Core into your application layer, while still giving the EF Core implementation the freedom to optimise how the specification is translated into SQL. It is particularly useful for read models in CQRS architectures where the write side uses domain repositories and the read side uses direct queries with specifications.
Ask these questions in order:
Are you using Clean Architecture or DDD with separate Domain and Infrastructure projects? → Use repository interfaces. They are the only architectural fit.
Do you need extensive unit testing of application logic without a database? → Use repository interfaces. The mock-ability benefit is real.
Is your application primarily CRUD with few complex domain rules? → Use direct DbContext. There is no abstraction to justify.
Do you need full access to EF Core 10 bulk operations and advanced query features? → Favour direct DbContext, or design repository interfaces that expose these operations explicitly.
Is your team small and iteration speed matters more than long-term separation? → Start with direct DbContext. You can introduce repository interfaces when complexity demands it.
The answer for most enterprise .NET teams in 2026: use domain-specific repository interfaces when your architecture calls for them, and direct DbContext everywhere else. The mistake is applying one pattern everywhere regardless of context.
EF Core's DbContext is already a Unit of Work. Calling SaveChangesAsync() commits all tracked changes in one transaction. There is rarely a need to wrap this in a separate IUnitOfWork interface unless:
You are abstracting multiple DbContext instances across bounded contexts in the same transaction
You need to control SaveChanges behaviour explicitly from the application layer
Your testing strategy requires mocking the commit boundary separately from repository operations
In most applications, injecting DbContext directly and calling SaveChangesAsync() in your service or handler is perfectly correct. The Unit of Work abstraction becomes valuable when you are orchestrating across multiple aggregates or bounded contexts in a single operation. For deeper context on EF Core performance considerations in high-traffic APIs, see EF Core Performance Tuning Checklist for High-Traffic APIs.
If you are starting a new ASP.NET Core API today, consider this layering rule of thumb:
Single-project or simple layered app: Use DbContext directly in your service classes. Add AsNoTracking() on all read queries. Call SaveChangesAsync() explicitly. Keep it simple.
Clean Architecture (Domain / Application / Infrastructure / API): Repository interfaces belong in Domain or Application. Implementations belong in Infrastructure. Only the infrastructure project references EF Core.
CQRS with MediatR: Command handlers typically need repository interfaces (they mutate domain aggregates through repository contracts). Query handlers often use direct DbContext or Dapper - they are read-only and performance-sensitive, so the full query API matters.
This separation is practical and scales well. It also avoids the classic mistake of putting EF Core references in the wrong layer.
Should I use the repository pattern for every entity in my application? No. Repository interfaces should map to aggregate roots in a DDD context, or to meaningful data access contracts in a layered architecture. Creating a repository for every entity - including simple lookup tables and configuration data - adds overhead without value. Apply repository interfaces where the abstraction boundary genuinely matters.
Does EF Core's DbContext already implement the Repository and Unit of Work patterns? Conceptually yes. DbSet<T> behaves like a repository (it tracks and queries entities of a specific type), and DbContext behaves like a Unit of Work (it tracks all changes and commits them as a single transaction via SaveChangesAsync()). This is why direct DbContext usage is architecturally defensible - you are not bypassing a pattern, you are using the framework's built-in implementation of it.
Can I mix repository pattern and direct DbContext in the same application? Yes, and this is often the right approach. Use repository interfaces for your write model (command side, domain aggregates) where testability and domain isolation matter. Use direct DbContext or Dapper for your read model (query side) where query composition flexibility and performance are the priority. This hybrid is commonly used in CQRS architectures.
What is the performance difference between repository pattern and direct DbContext? The repository interface itself introduces no measurable performance overhead - it is just an interface dispatch. The performance difference, if any, comes from how the implementation is written. A direct DbContext call with AsNoTracking() and ExecuteUpdate is faster than one routed through a repository method that uses full entity tracking unnecessarily - but that is a usage issue, not a pattern issue.
How do I unit test services that use DbContext directly without a repository interface? You have two options. The first is to use EF Core's in-memory provider (UseInMemoryDatabase) or SQLite in-memory mode in your unit tests - these are fast and require no mocking infrastructure. The second is to use Microsoft.EntityFrameworkCore.InMemory as a lightweight test double. Both are valid. The trade-off is that these tests become integration tests (they use the real EF Core stack) rather than pure unit tests. If your team values the distinction, repository interfaces give you a cleaner seam.
Is the repository pattern dead in 2026? No. The generic repository pattern is largely obsolete - it was always a misunderstanding of what the pattern is for. But domain-focused repository interfaces in Clean Architecture and DDD applications remain the correct approach. The pattern is not dead; it is often misapplied. Understanding the difference is what makes the difference.
Does the repository pattern make it easier to swap out EF Core for another ORM? In theory yes - your application code depends only on the interface, not EF Core. In practice, ORM swaps are rare enough that this benefit rarely justifies the abstraction cost on its own. The more practical benefit is test isolation: you can swap a real EF Core repository for a mock in unit tests, which you do constantly. Design for testability first; ORM portability is a secondary benefit.
Celin Daniel is Co-founder of Coding Droplets with 13+ years of hands-on experience building, shipping, and operating .NET and ASP.NET Core systems in production. The guidance here comes from real projects and production incidents, not theory.