Repository Pattern vs Direct DbContext Usage in .NET (2026 Edition)
When building production systems with ASP.NET Core + Entity Framework Core, one architectural debate keeps resurfacing:
Should we inject
DbContextdirectly, or wrap it in a Repository?
This is not a beginner question anymore.
In 2026, most experienced .NET engineers understand that EF Core already implements both Repository and Unit of Work patterns internally. Yet enterprise systems still adopt custom repositories — sometimes wisely, sometimes blindly.
This article is not dogmatic.
It is architectural.
We will examine:
- What Direct
DbContextusage really means - What a Repository abstraction truly adds (and removes)
- The real trade-offs in enterprise systems
- Code-level implications
- Where each approach breaks
- How to decide like a senior engineer
The analysis is grounded in code. Because architecture without code is opinion.
The Baseline: Direct DbContext Usage
Let’s start with the most honest implementation possible.
public class ProductService
{
private readonly AppDbContext _context;
public ProductService(AppDbContext context)
{
_context = context;
}
public async Task<List<Product>> GetProductsAsync()
{
return await _context.Products.ToListAsync();
}
}
There is no abstraction layer.
No interface.
No indirection.
The service speaks directly to EF Core.
This is the simplest possible approach — and simplicity matters.
What This Code Really Means
That constructor:
public ProductService(AppDbContext context)
establishes a direct dependency on EF Core infrastructure.
This service:
- Knows about
DbSet<T> - Knows about EF tracking behavior
- Knows about LINQ-to-Entities translation
- Is bound to relational persistence
For small systems, that is perfectly acceptable.
For large systems, that is a boundary decision.
Advantages of Direct DbContext Usage
1. Minimal Surface Area
No interface.
No duplicated method signatures.
No additional registration.
services.AddDbContext<AppDbContext>();
services.AddScoped<ProductService>();
You move fast.
2. Full EF Core Power
You keep full access to:
IncludeAsNoTrackingExecuteUpdateAsyncExecuteDeleteAsync- Raw SQL
- Compiled queries
- Change tracker configuration
No abstraction gets in your way.
Example:
return await _context.Products
.AsNoTracking()
.Where(p => p.IsActive)
.OrderBy(p => p.Name)
.ToListAsync();
No wrapper hiding performance details.
3. Transparency of Behavior
You see exactly what EF Core is doing.
No “magic” behind a repository method like GetActiveProducts() whose implementation is hidden elsewhere.
Transparency reduces cognitive load.
The Real Disadvantages of Direct DbContext
The downside is not technical — it is architectural.
1. Tight Coupling
Your application layer depends on EF Core types.
AppDbContext
DbSet<T>
EntityState
If tomorrow you decide to:
- Replace EF
- Introduce CQRS read models
- Use Dapper for read paths
- Introduce multi-database support
Refactoring becomes invasive.
2. Query Sprawl
Without discipline, queries spread across services.
_context.Products.Where(...)
_context.Products.Include(...)
_context.Products.Any(...)
Business rules can fragment.
This is not EF’s fault — it is a boundary problem.
3. Testing Reality
Mocking DbContext properly is difficult.
You either:
- Use EF InMemory (not production-accurate)
- Use SQLite in-memory (better)
- Use integration tests (best)
For CRUD systems, this is fine.
For domain-heavy systems, isolation matters more.
Enter the Repository Pattern
Now let’s wrap the persistence layer.
Repository Interface
public interface IProductRepository
{
Task<List<Product>> GetAllAsync();
Task<Product?> GetByIdAsync(int id);
Task AddAsync(Product product);
}
Repository Implementation
public class ProductRepository : IProductRepository
{
private readonly AppDbContext _context;
public ProductRepository(AppDbContext context)
{
_context = context;
}
public async Task<List<Product>> GetAllAsync()
{
return await _context.Products.ToListAsync();
}
public async Task<Product?> GetByIdAsync(int id)
{
return await _context.Products.FindAsync(id);
}
public async Task AddAsync(Product product)
{
_context.Products.Add(product);
await _context.SaveChangesAsync();
}
}
Service Layer
public class ProductService
{
private readonly IProductRepository _repository;
public ProductService(IProductRepository repository)
{
_repository = repository;
}
}
We have introduced a boundary.
But what did we actually gain?
What Repository Actually Changes
The service now depends on:
IProductRepository
instead of:
AppDbContext
That is a directional shift.
Infrastructure is no longer visible in the service layer.
This aligns with:
- Clean Architecture
- DDD
- Dependency Inversion Principle
But the benefits only materialize if the repository encapsulates meaningful behavior.
Advantages of Repository Pattern
1. Separation of Concerns
The service layer no longer cares about:
- Tracking behavior
- SaveChanges semantics
- Query translation
It cares about business rules.
2. Easier Unit Testing
You can mock:
var repoMock = new Mock<IProductRepository>();
You isolate domain logic.
That matters in complex systems with domain invariants.
3. Centralized Data Access Logic
If you change how products are retrieved:
- Add caching
- Add soft delete filtering
- Add multi-tenant filtering
You modify one place.
That is powerful.
The Hidden Cost of Repositories
Now the uncomfortable truth.
EF Core already is:
- A repository (
DbSet<T>) - A unit of work (
DbContext)
If your repository only mirrors EF:
Task<List<T>> GetAllAsync();
Task AddAsync(T entity);
you have added an indirection layer without adding behavior.
That is accidental complexity.
The Generic Repository Trap
Many developers introduce:
public interface IRepository<T>
{
Task<List<T>> GetAllAsync();
Task<T?> GetByIdAsync(int id);
Task AddAsync(T entity);
Task RemoveAsync(T entity);
}
This is usually a mistake.
Why?
Because EF already provides:
_context.Set<T>()
Generic repositories often:
- Hide
Include - Hide projection
- Hide optimized queries
- Prevent compiled queries
- Reduce expressive LINQ
They remove power without adding value.
When Direct DbContext Is the Right Call
Use direct DbContext injection when:
- Your system is CRUD-heavy
- You are building internal tools
- You prioritize delivery speed
- You don’t need strict layering
- You accept EF as your permanent ORM
Modern ASP.NET Core applications frequently fall into this category.
Microsoft itself demonstrates this style in many official samples.
Simplicity scales surprisingly well when the domain is simple.
When Repository Pattern Is the Right Call
Use repositories when:
- You follow Clean Architecture
- You implement DDD aggregates
- You have domain invariants
- You plan multiple persistence strategies
- You anticipate scaling complexity
- You want domain isolation from infrastructure
In these systems, repositories become meaningful boundaries, not wrappers.
Advanced Scenario: Domain-Driven Example
Consider a rule-heavy aggregate:
public class Order
{
private readonly List<OrderItem> _items = new();
public void AddItem(Product product, int quantity)
{
if (quantity <= 0)
throw new DomainException("Quantity must be positive");
_items.Add(new OrderItem(product, quantity));
}
}
In DDD, the repository handles aggregate persistence:
public interface IOrderRepository
{
Task<Order?> GetAsync(Guid id);
Task SaveAsync(Order order);
}
Now the repository enforces aggregate boundary semantics.
That abstraction adds clarity.
That is not overengineering.
Performance Considerations
Direct DbContext allows:
await _context.Products
.Where(p => p.Price > 100)
.ExecuteUpdateAsync(...);
Generic repositories often block access to new EF Core features.
If your repository blocks:
- Bulk operations
- Projection optimization
- Compiled queries
You may degrade performance.
Always measure abstraction cost.
Architectural Decision Matrix
| Scenario | Direct DbContext | Repository |
|---|---|---|
| Small API | ✅ | ❌ |
| Admin CRUD tool | ✅ | ❌ |
| Enterprise ERP | ⚠️ | ✅ |
| DDD Aggregates | ❌ | ✅ |
| Multi-database strategy | ❌ | ✅ |
| High-performance microservice | ✅ (carefully) | ⚠️ |
The Real Answer
There is no universal winner.
The mistake is ideological purity.
The right question is:
Does this abstraction reduce cognitive load or increase it?
Start simple.
If complexity grows, introduce boundaries deliberately.
Do not begin with abstraction.
Do not avoid abstraction when the domain demands it.
Final Thoughts
Architecture is not about patterns.
It is about tension management:
- Simplicity vs Flexibility
- Speed vs Maintainability
- Transparency vs Isolation
DbContext direct usage is honest and powerful.
Repository pattern is structured and strategic.
Both are tools.
Senior engineers choose tools based on problem shape — not blog posts.
— Written by Cristian Sifuentes
Full-stack engineer · .NET architect · Systems thinker

Top comments (1)
Nice unbiased write-up. Thanks.