DbContext vs. Repository + Unit of Work: ¿necesitás los dos?

DbSet<T> ya es un Repository genérico, y DbContext ya es un Unit of Work. Por qué envolver EF Core en tus propias capas muchas veces no cumple lo que promete — y los casos donde sí vale la pena.

Este debate vuelve cada tanto — en LinkedIn, en una call con un compañero, en un code review. Alguien abre un PR con IRepository<T> e IUnitOfWork arriba de EF Core. Otro pregunta "¿para qué, si DbContext ya hace eso?". Y de ahí el hilo se divide en dos bandos que casi nunca se terminan de convencer.

Fui a buscar una respuesta concreta en vez de elegir bando por intuición. Esto es a lo que llegué.


#Lo que EF Core ya te da

Dos cosas, de fábrica.

DbSet<T> ya es un Repository genérico. Add, Remove, Find, y filtrado vía IQueryable — es la misma superficie que expone un IRepository<T> hecho a mano, menos el archivo de más.

CSHARP
public class AppDbContext : DbContext
{
    public DbSet<Order> Orders => Set<Order>();
    public DbSet<Customer> Customers => Set<Customer>();
}

// no hace falta repository para esto
var activeOrders = await context.Orders
    .Where(o => o.Status == OrderStatus.Active)
    .ToListAsync();

DbContext ya es un Unit of Work. Trackea cada cambio hecho a través de sus DbSet y los confirma juntos con un solo SaveChangesAsync(), dentro de una única transacción.

CSHARP
context.Orders.Add(order);
context.Inventory.Update(stock);
await context.SaveChangesAsync(); // los dos cambios, una sola transacción

Entonces, antes de agregar IRepository<T> e IUnitOfWork encima, vale la pena preguntarse: ¿qué agrega esta capa nueva que no esté ya ahí?


#Los dos argumentos que no se sostienen

"Repository hace más fácil testear." En la práctica, mockear IRepository<T> significa simular a mano el comportamiento de LINQ — Where, Include, OrderBy — que es exactamente lo que ya hacen los providers de EF Core por vos. El provider InMemory, o SQLite en modo in-memory, te permiten testear contra un DbContext real, con traducción de queries real, sin tocar un servidor de base de datos. Una interfaz de repository hecha a mano muchas veces termina siendo menos fiel al comportamiento de producción que testear directamente contra EF Core.

"Repository te permite cambiar de base de datos o de ORM más adelante." En la práctica, nunca vi un equipo reemplazar EF Core por otro ORM en un proyecto ya en marcha. Y cuando la interfaz del repository expone IQueryable<T> — que es lo que pasa en la mayoría de los casos, porque en algún momento alguien necesitó filtrar o paginar — la abstracción ya estaba filtrando detalles de EF Core igual. No te desacoplaste del ORM; solo agregaste una capa entre vos y él.

Ninguno de los dos argumentos es falso. Es que EF Core ya los responde por su cuenta, lo que vuelve a la capa extra redundante, no equivocada.


#Dónde Repository + Unit of Work siguen teniendo sentido

El patrón no está roto — generalmente se aplica mal. Deja de ser ruido en el momento en que la interfaz deja de ser una copia de DbSet<T>.

Cuando el contrato es específico del dominio, no un CRUD genérico.

CSHARP
// No agrega nada sobre llamar a context.Customers directamente
public interface ICustomerRepository
{
    Task<Customer?> GetByIdAsync(Guid id);
    Task AddAsync(Customer customer);
}

// Agrega algo: una query que el dominio realmente necesita, nombrada por lo que significa
public interface ICustomerRepository
{
    Task<Customer?> FindActiveByEmailAsync(string email);
    Task<IReadOnlyList<Customer>> FindOverdueAccountsAsync();
}

La segunda versión no está ocultando EF Core — está expresando un concepto de dominio. Eso sí justifica una interfaz real.

Cuando hay un límite arquitectónico real que proteger. En Clean o Hexagonal architecture, ocultar EF Core detrás de una interfaz normalmente no es "por si lo cambiamos después" — es que la capa de dominio, por diseño, no debe referenciar tipos de infraestructura en absoluto. Esa es una regla arquitectónica, no un argumento de testeabilidad o portabilidad, y es una razón legítima por sí sola.

Cuando un Unit of Work coordina más de un DbContext o storage. Si una operación de negocio necesita confirmar cambios contra dos fuentes de datos distintas de forma conjunta, DbContext.SaveChangesAsync() solo no alcanza para eso. Ese es el único caso donde un Unit of Work hace coordinación real, en vez de duplicar lo que EF Core ya hace.


#La pregunta que realmente lo decide

No es "Repository y Unit of Work: ¿sí o no?". La pregunta es: ¿esta interfaz dice algo más de lo que ya dice DbSet<T>?

Si ICustomerRepository tiene los mismos cinco métodos que cualquier otro I*Repository<T> del codebase, es ceremonia — bórralo e inyectá AppDbContext directamente. Si expresa algo específico del dominio, o protege un límite que decidiste imponer a propósito, mantenelo.

Por default, usá DbContext directamente. Agregá un repository cuando tengas una razón puntual, no por costumbre aplicada igual a todos los proyectos.

#ef-core #dotnet #repository-pattern #unit-of-work