0

Arquitectura de Software con C# 17: Implementación de Agregados y Repositorios en Arquitectura Hexagonal

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.
  • 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.

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.

Fernando Sonego

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *