Una Unit of Work es un objeto que registra cambios y coordina su escritura como una unidad. Varios repositorios pueden compartirla para confirmar juntos las modificaciones de una operación de negocio.
En el artículo anterior vimos cómo el Repository Pattern nos permitía tratar la base de datos como si fuera una colección en memoria.
Pero en el mundo real, las operaciones de negocio rara vez afectan a una sola tabla o entidad. Imagina que estamos procesando un pedido en una tienda online. La operación “Realizar Pedido” implica:
Insertar el registro en la tabla Pedidos.
Restar cantidad en la tabla Inventario.
Crear una Factura.
¿Qué pasa si usamos repositorios sueltos?
El Problema: Datos Inconsistentes
Si manejamos los repositorios de forma aislada, cada uno podría estar guardando sus cambios en el momento en que se le llama.
// ❌ El peligro de no usar transacciones
public void ProcesarPedido(Pedido pedido)
{
// 1. Guardamos el pedido (Éxito)
_pedidoRepository.Add(pedido);
// 2. Restamos stock (ERROR: Se va la luz, falla la red, o excepción)
_inventarioRepository.RestarStock(pedido.Items);
// 3. Generamos factura
_facturaRepository.Add(new Factura(pedido));
}Si el paso 2 falla, tenemos un problema grave: Tenemos un pedido creado, pero no hemos restado el stock. Nuestro inventario miente. Los datos son inconsistentes.
Necesitamos que estas tres operaciones sean Atómicas: o suceden todas, o no sucede ninguna.
Implementación en C#
Vamos a implementar el patrón manualmente.
Nota: si usas Entity Framework Core, DbContext ya cumple el papel de Unit of Work. Aun así, conviene entender qué coordina.
La Interfaz
public interface IUnitOfWork : IDisposable
{
// Accesos a los repositorios
IUsuarioRepositorio Usuarios { get; }
IPedidoRepositorio Pedidos { get; }
// El botón rojo: Guardar todo
int Commit();
}La Implementación
Aquí está la clave. La Unit of Work crea el contexto (la conexión a BBDD) y se lo pasa a todos los repositorios. Así, todos trabajan sobre la misma transacción en memoria.
public class UnitOfWork : IUnitOfWork
{
private readonly MiDbContext _context;
// Backing fields para los repositorios
private IUsuarioRepositorio _usuarios;
private IPedidoRepositorio _pedidos;
public UnitOfWork(MiDbContext context)
{
_context = context;
}
// Lazy Loading de repositorios: Solo se crean si se piden
public IUsuarioRepositorio Usuarios
{
get
{
return _usuarios ??= new UsuarioRepositorio(_context);
}
}
public IPedidoRepositorio Pedidos
{
get
{
return _pedidos ??= new PedidoRepositorio(_context);
}
}
public int Commit()
{
// Aquí es donde realmente se impacta en la Base de Datos
return _context.SaveChanges();
}
public void Dispose()
{
_context.Dispose();
}
}Los Repositorios Adaptados
Los repositorios ya no crean su propia conexión, la reciben.
public class PedidoRepositorio : IPedidoRepositorio
{
private readonly MiDbContext _context;
public PedidoRepositorio(MiDbContext context)
{
_context = context;
}
public void Add(Pedido pedido)
{
// Solo añadimos al contexto en memoria, NO guardamos todavía
_context.Pedidos.Add(pedido);
}
}Uso en el Servicio (Transaccional)
Ahora nuestra lógica de negocio es segura.
public class ProcesadorPedidos
{
private readonly IUnitOfWork _uow;
public ProcesadorPedidos(IUnitOfWork uow)
{
_uow = uow;
}
public void RealizarPedido(Pedido nuevoPedido)
{
try
{
// 1. Operación con repositorio de Pedidos
_uow.Pedidos.Add(nuevoPedido);
// 2. Operación con repositorio de Usuarios
var usuario = _uow.Usuarios.GetById(nuevoPedido.UsuarioId);
usuario.PuntosFidelidad += 10; // Modificamos entidad
// ... más operaciones ...
// 3. EL MOMENTO DE LA VERDAD
// Si algo falla antes de esta línea, no se guarda NADA.
_uow.Commit();
Console.WriteLine("Pedido procesado y guardado con éxito.");
}
catch (Exception ex)
{
// Si ocurre un error, no llamamos a Commit.
// Este scope de Unit of Work debe descartarse tras el fallo.
// No reutilices un DbContext que conserva cambios pendientes.
Console.WriteLine($"Error procesando pedido: {ex.Message}");
}
}
}Unit of Work y Entity Framework Core
En EF Core, DbContext cumple el papel de Unit of Work y cada DbSet<T> ofrece capacidades similares a un repositorio.
Cuando haces:
context.Users.Add(user); // Repository (en memoria)
context.Orders.Add(order); // Repository (en memoria)
context.SaveChanges(); // Unit of Work (Commit de la transacción)SaveChanges aplica por defecto una transacción cuando el proveedor la admite. Para coordinar varias llamadas a SaveChanges o recursos externos necesitas una estrategia transaccional explícita.
Sin embargo, crear una capa de abstracción IUnitOfWork por encima de EF Core sigue siendo útil para:
- Desacoplar de EF Core: Si quieres cambiar EF por Dapper o ADO.NET puro en el futuro.
- Límite de aplicación: Exponer operaciones del dominio puede evitar que capas superiores dependan directamente de EF Core. Las pruebas de persistencia deben seguir usando una base de datos adecuada.
Ventajas y Desventajas
| Ventajas | Desventajas |
|---|---|
| Coordinación: Agrupa cambios que deben confirmarse juntos cuando el almacén admite transacciones. | Abstracción sobre abstracción: Con un ORM moderno puede ser redundante. |
| Centralización: Gestiona la conexión a la BBDD en un solo punto. | Complejidad: Añade una capa más de indirección. |
| Control del commit: Evita que cada repositorio confirme por su cuenta. | Alcance: No vuelve atómicas por sí sola operaciones sobre varias bases de datos o APIs. |
El patrón Unit of Work es el director de orquesta de vuestra capa de datos. Asegura que todos los instrumentos (repositorios) toquen al unísono y que la obra (la transacción) termine bien o no termine en absoluto.