Arquitectura de software

¿Repository Pattern sigue teniendo sentido con EF Core?

DbContext ya es un repositorio y una unidad de trabajo. Cuándo un repositorio sobre EF Core aporta valor, cuándo es una capa vacía y qué alternativas hay.

Por Leyton Manuel Espitia Diaz 5 min de lectura

Fragmento de código C# de una interfaz IRepository genérica sobre EF Core

Casi todas las plantillas de arquitectura en .NET incluyen un IRepository<T> genérico con GetByIdAsync, GetAllAsync, AddAsync, Update y Delete, implementado sobre EF Core. Y casi todos los proyectos que lo usan terminan agregándole métodos hasta que deja de ser genérico.

Un repositorio sobre EF Core puede ser una abstracción innecesaria. Pero no siempre: hay casos donde aporta valor real. La diferencia está en qué problema estás intentando resolver.

EF Core ya implementa el patrón

Martin Fowler describe el repositorio como una colección en memoria de objetos de dominio que oculta el acceso a datos. La unidad de trabajo agrupa los cambios y los confirma juntos.

// DbSet<T> es el repositorio
var order = await db.Orders.FirstAsync(o => o.Id == id, ct);
db.Orders.Add(newOrder);

// DbContext es la unidad de trabajo
await db.SaveChangesAsync(ct);

Un IRepository<T> encima de DbSet<T>, y un IUnitOfWork encima de DbContext, son en muchos casos una abstracción sobre otra abstracción que hace lo mismo.

Cómo suele degradarse un repositorio genérico

Empieza limpio:

public interface IRepository<T> where T : class
{
    Task<T?> GetByIdAsync(int id, CancellationToken ct);
    Task<IReadOnlyList<T>> GetAllAsync(CancellationToken ct);
    Task AddAsync(T entity, CancellationToken ct);
    void Remove(T entity);
}

Luego aparecen las necesidades reales:

  • El listado necesita paginación y filtros → GetPagedAsync(int page, int size, Expression<Func<T, bool>>? filter).
  • El detalle necesita las líneas del pedido → GetByIdWithIncludesAsync(int id, params Expression<Func<T, object>>[] includes).
  • Un reporte necesita solo tres columnas → no hay forma de proyectar, así que se traen entidades completas.

O la salida rápida: exponer IQueryable<T>. En ese momento el repositorio dejó de ocultar EF Core: quien lo usa escribe LINQ de EF Core igual que antes, solo que con un intermediario más.

El costo más grave no es la cantidad de código. Es que el repositorio genérico esconde justo lo que importa para el rendimiento: proyecciones, tracking, AsSplitQuery, etiquetas para el monitoreo. Lo vimos en Tracking vs NoTracking en EF Core.

El argumento de las pruebas

La razón más común para el repositorio es poder simularlo en pruebas unitarias. Es un argumento válido, pero tiene un problema: una prueba con un repositorio simulado verifica la lógica, pero no que la consulta funcione. Y con EF Core, muchos errores están justamente en la consulta: una expresión que no se traduce a SQL, un Include que falta, un filtro mal escrito.

Hoy es práctico probar contra una base de datos real:

// Testcontainers levanta un SQL Server real en Docker para las pruebas
var sql = new MsSqlBuilder().Build();
await sql.StartAsync();

var options = new DbContextOptionsBuilder<AppDbContext>()
    .UseSqlServer(sql.GetConnectionString())
    .Options;

Estas pruebas son más lentas que las unitarias, pero detectan errores que un mock nunca vería. Un punto medio común: lógica de negocio pura con pruebas unitarias (sin base de datos) y casos de uso con pruebas de integración contra una base real.

Cuándo un repositorio sí aporta valor

1. Repositorios específicos por agregado, no genéricos

public interface IOrderRepository
{
    Task<Order?> GetForCheckoutAsync(int orderId, CancellationToken ct);
    Task AddAsync(Order order, CancellationToken ct);
}

Este repositorio no es una envoltura de DbSet. Expresa las formas en que el dominio necesita cargar un pedido: con sus líneas, su cliente y sus pagos, porque el checkout necesita todo eso para validar sus reglas. La consulta compleja queda en un solo lugar con un nombre que dice para qué existe.

2. Cuando conviven varias tecnologías de acceso a datos

Si algunas consultas son con EF Core, otras con Dapper y otras contra una API externa, una interfaz por caso de uso evita que la lógica de negocio sepa cuál es cuál.

3. Cuando el dominio es rico y las reglas importan más que las consultas

En sistemas con Domain-Driven Design, el repositorio que carga y guarda agregados completos protege las invariantes. Ahí tiene un propósito claro.

Alternativas cuando no hace falta

  • DbContext directo en el caso de uso. Es lo más simple y transparente para APIs mayormente CRUD.
  • Métodos de extensión para consultas reutilizables:
public static class OrderQueries
{
    public static IQueryable<Order> PendingFor(this IQueryable<Order> orders, int customerId) =>
        orders.Where(o => o.CustomerId == customerId && o.Status == OrderStatus.Pending);
}

var pending = await db.Orders.PendingFor(customerId).CountAsync(ct);

Reutilizas el filtro sin esconder nada de EF Core.

El trade-off

Repositorio genérico Repositorio por agregado DbContext directo
Código adicional Alto Medio Ninguno
Control de la consulta Bajo Alto Total
Pruebas unitarias con mocks Fáciles Fáciles Difíciles
Acoplamiento a EF Core Oculto, pero real Aislado Explícito

Cuándo evitarlo

  • Si tu repositorio solo reenvía llamadas a DbSet.
  • Si expone IQueryable<T>: ya no está ocultando nada.
  • Si la única razón es "cambiar de ORM algún día". Casi nunca pasa, y cuando pasa, el repositorio no es lo que más trabajo da.

Conclusión

El patrón Repository no está muerto, pero el repositorio genérico sobre EF Core casi siempre es una capa que cuesta más de lo que aporta. Si vas a tener repositorios, que expresen cómo tu dominio necesita los datos. Si no tienes esa necesidad, DbContext ya hace el trabajo. Y esto encaja con la pregunta más amplia de si realmente necesitas Clean Architecture.

Leyton Manuel Espitia Diaz

Escrito por

Leyton Manuel Espitia Diaz

CEO y fundador de Nakanaba

Desarrollador de software y fundador de Nakanaba. Lidera la arquitectura de Vitrin: backend en .NET, aplicaciones en Angular y .NET MAUI, SQL Server y servicios en Cloudflare.

¿Tu sistema necesita una revisión de arquitectura?

Te ayudamos a decidir qué estructura necesita tu software hoy y cómo prepararlo para crecer, sin sobreingeniería.

Ver desarrollo a medida