Skip to main content

Command Palette

Search for a command to run...

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

Updated
โ€ข12 min read
Repository Pattern vs Direct DbContext in ASP.NET Core: Which Should Your .NET Team Use in 2026?

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.

ASP.NET Core Web API: Zero to Production

What the Repository Pattern Actually Is (and What It Is Not)

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.

What Direct DbContext Means in Practice

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.

When to Use the Repository Pattern

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.

When to Use Direct DbContext

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.

Side-By-Side Trade-Off Comparison

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 Generic Repository Anti-Pattern

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.

The Specification Pattern: A Middle Ground

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.

Decision Matrix: Which Should Your Team Use?

Ask these questions in order:

  1. Are you using Clean Architecture or DDD with separate Domain and Infrastructure projects? โ†’ Use repository interfaces. They are the only architectural fit.

  2. Do you need extensive unit testing of application logic without a database? โ†’ Use repository interfaces. The mock-ability benefit is real.

  3. Is your application primarily CRUD with few complex domain rules? โ†’ Use direct DbContext. There is no abstraction to justify.

  4. 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.

  5. 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.

What About the Unit of Work Pattern?

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.

Integrating the Decision Into Your Architecture

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.


โ˜• Prefer a one-time tip? Buy us a coffee โ€” every bit helps keep the content coming!


FAQ

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.

More from this blog

C

Coding Droplets

219 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.