Introducción a la clase
Hemos aprendido cómo desacoplar la infraestructura con Puertos y Adaptadores en Arquitectura Hexagonal. Ahora profundizaremos en la implementación de Agregados y Repositorios, dos conceptos clave de Domain-Driven Design (DDD) que encajan perfectamente en este enfoque.
¿Qué obtendrás de esta clase?
- Aprenderás qué son los Agregados y cómo funcionan en Arquitectura Hexagonal.
- Implementarás Agregados y Repositorios en C# utilizando buenas prácticas de DDD.
- Entenderás cómo los Repositorios actúan como Adaptadores en la arquitectura.
- Verás cómo persistir y recuperar Agregados con Entity Framework Core (EF Core).
¿Qué es un Agregado en DDD?
Un Agregado es un grupo de Entidades y Value Objects que forman una unidad lógica en el dominio.
- Tiene una Entidad Raíz que controla su estado.
- Protege la consistencia de los datos dentro del agregado.
- Las demás Entidades dentro del Agregado no pueden ser modificadas directamente desde fuera.
Ejemplo de un Agregado «Pedido» con «Productos»
(Entidad Raíz)
│── Producto (Entidad)
│── Producto (Entidad)
Reglas de un Agregado en Arquitectura Hexagonal:
- Solo se accede a las Entidades a través de la Entidad Raíz.
- La persistencia se maneja a nivel del Agregado completo, no de cada Entidad individualmente.
- Los Repositorios actúan como Adaptadores para persistir y recuperar Agregados.
Paso 1: Implementar el Agregado «Pedido»
public class Pedido
{
public int Id { get; private set; }
public string Cliente { get; private set; }
private readonly List<Producto> _productos = new List<Producto>();
public IReadOnlyCollection<Producto> Productos => _productos.AsReadOnly();
public Pedido(int id, string cliente)
{
Id = id;
Cliente = cliente;
}
public void AgregarProducto(Producto producto)
{
_productos.Add(producto);
}
public decimal CalcularTotal()
{
return _productos.Sum(p => p.Precio);
}
}
Explicación:
Pedido
es la Entidad Raíz del Agregado._productos
es una lista privada, evitando modificaciones externas directas.- Se expone
IReadOnlyCollection<Producto>
para evitar modificaciones externas.
Paso 2: Implementar la Entidad «Producto» dentro del Agregado
public class Producto
{
public int Id { get; private set; }
public string Nombre { get; private set; }
public decimal Precio { get; private set; }
public Producto(int id, string nombre, decimal precio)
{
Id = id;
Nombre = nombre;
Precio = precio;
}
}
Reglas de Producto:
- No tiene métodos para cambiar su estado después de crearse.
- Solo puede ser modificado por el Agregado «Pedido».
Paso 3: Definir un Puerto para el Repositorio del Agregado
El Puerto define un contrato para persistir y recuperar Agregados.
public interface IPedidoRepository
{
void Guardar(Pedido pedido);
Pedido ObtenerPorId(int id);
}
Ventaja: Permite cambiar la base de datos sin afectar la lógica del negocio.
Paso 4: Implementar el Adaptador (Repositorio con EF Core)
El Adaptador implementa el puerto (IPedidoRepository
) y usa Entity Framework Core para la persistencia.
public class PedidoRepository : IPedidoRepository
{
private readonly AppDbContext _context;
public PedidoRepository(AppDbContext context)
{
_context = context;
}
public void Guardar(Pedido pedido)
{
_context.Pedidos.Add(pedido);
_context.SaveChanges();
}
public Pedido ObtenerPorId(int id)
{
return _context.Pedidos
.Include(p => p.Productos) // Cargar los Productos relacionados
.FirstOrDefault(p => p.Id == id);
}
}
Explicación:
Guardar()
almacena el Agregado completo (Pedido y sus Productos).ObtenerPorId()
usa.Include(p => p.Productos)
para cargar el Agregado completo.
Paso 5: Crear un Caso de Uso para Manejar el Agregado
Los Casos de Uso manejan la lógica de aplicación, sin conocer la infraestructura.
public class CrearPedidoUseCase
{
private readonly IPedidoRepository _repository;
public CrearPedidoUseCase(IPedidoRepository repository)
{
_repository = repository;
}
public void Ejecutar(Pedido pedido)
{
_repository.Guardar(pedido);
}
}
Ventaja:
- Totalmente desacoplado de la persistencia.
- Facilita pruebas unitarias.
Paso 6: Implementar un Controlador REST en ASP.NET Core
El Controlador actúa como un Adaptador, permitiendo que la API REST use los Casos de Uso.
[ApiController]
[Route("api/pedidos")]
public class PedidoController : ControllerBase
{
private readonly CrearPedidoUseCase _crearPedidoUseCase;
private readonly IPedidoRepository _pedidoRepository;
public PedidoController(CrearPedidoUseCase crearPedidoUseCase, IPedidoRepository pedidoRepository)
{
_crearPedidoUseCase = crearPedidoUseCase;
_pedidoRepository = pedidoRepository;
}
[HttpPost]
public IActionResult CrearPedido([FromBody] Pedido pedido)
{
_crearPedidoUseCase.Ejecutar(pedido);
return Ok("Pedido creado exitosamente");
}
[HttpGet("{id}")]
public IActionResult ObtenerPedido(int id)
{
var pedido = _pedidoRepository.ObtenerPorId(id);
if (pedido == null)
return NotFound();
return Ok(pedido);
}
}
Ventaja: Permite que cualquier interfaz de usuario (API REST, CLI, UI web) use la aplicación sin afectar el dominio.
Errores comunes al trabajar con Agregados y Repositorios
- Acceder a Entidades del Agregado fuera de la Entidad Raíz.
- Solución: Modificar los Productos solo desde
Pedido
.
- Solución: Modificar los Productos solo desde
- Guardar Entidades individualmente en lugar de guardar el Agregado completo.
- Solución: Usar el Repositorio del Agregado para persistir todo junto.
- No usar
Include()
en EF Core al recuperar el Agregado.- Solución: Usar
.Include(p => p.Productos)
en las consultas.
- Solución: Usar
Cuestionario de Autoevaluación
- ¿Por qué es importante encapsular todas las modificaciones dentro de la Entidad Raíz?
- ¿Cómo se diferencia un Agregado de una simple relación entre Entidades?
- ¿Por qué los Repositorios deben manejar el Agregado completo en lugar de Entidades individuales?
- ¿Cómo implementarías una prueba unitaria para el Caso de Uso
CrearPedidoUseCase
? - ¿Cuál es la ventaja de usar
.Include()
en Entity Framework Core al recuperar un Agregado?
Resumen de la Clase
- Un Agregado agrupa Entidades y Value Objects en una unidad lógica con una Entidad Raíz.
- Los Repositorios manejan la persistencia de Agregados completos, no de Entidades individuales.
- Los Puertos (Interfaces) permiten desacoplar la lógica del negocio de la infraestructura.
- Los Adaptadores (Repositorios y Controladores) implementan los Puertos y conectan la aplicación con bases de datos y API REST.
Próximo paso
En la siguiente clase veremos Casos de Uso en Arquitectura Hexagonal y cómo manejarlos correctamente.