DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

Repository Pattern vs Direct DbContext Usage in .NET (2026 Edition)

Repository Pattern vs Direct DbContext Usage in .NET (2026 Edition)

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 DbContext directly, 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 DbContext usage 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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

You move fast.


2. Full EF Core Power

You keep full access to:

  • Include
  • AsNoTracking
  • ExecuteUpdateAsync
  • ExecuteDeleteAsync
  • 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();
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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(...)
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Service Layer

public class ProductService
{
    private readonly IProductRepository _repository;

    public ProductService(IProductRepository repository)
    {
        _repository = repository;
    }
}
Enter fullscreen mode Exit fullscreen mode

We have introduced a boundary.

But what did we actually gain?


What Repository Actually Changes

The service now depends on:

IProductRepository
Enter fullscreen mode Exit fullscreen mode

instead of:

AppDbContext
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

This is usually a mistake.

Why?

Because EF already provides:

_context.Set<T>()
Enter fullscreen mode Exit fullscreen mode

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

In DDD, the repository handles aggregate persistence:

public interface IOrderRepository
{
    Task<Order?> GetAsync(Guid id);
    Task SaveAsync(Order order);
}
Enter fullscreen mode Exit fullscreen mode

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(...);
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
jancg profile image
Jan C. de Graaf

Nice unbiased write-up. Thanks.