Skip to content

AggregateRoot

La clase abstracta AggregateRoot sirve como base para implementar agregados en un sistema de Event Sourcing. Proporciona mecanismos para gestionar eventos de dominio, aplicar cambios y reconstruir el estado de un agregado a partir de su historial de eventos. AggregateRoot es parte del núcleo de la arquitectura de Event Sourcing, asegurando que los cambios de estado se realicen de manera consistente y que el historial sea la fuente de la verdad para el estado del agregado.

¿Cómo Funciona?

AggregateRoot funciona como un contenedor de eventos de dominio. Cuando se realiza una acción que modifica el estado del agregado, un evento de dominio se genera y se agrega a la lista de eventos no confirmados del agregado. Estos eventos se utilizan para dos propósitos principales:

  1. Aplicación de Cambios: Los eventos se aplican a la entidad de manera inmediata, modificando su estado actual.
  2. Persistencia: Los eventos se almacenan como registros inmutables del historial del agregado.

La clase también gestiona la rehidratación del agregado, reconstruyendo su estado a partir de los eventos almacenados. Esto garantiza que la entidad siempre se pueda reconstruir a su último estado conocido.

Métodos


La clase AggregateRoot proporciona los siguientes métodos para gestionar eventos de dominio y cambios de estado en un agregado:

AddEvent

Type: protected override void AddEvent(IDomainEvent @event)

Agrega un evento de dominio a la lista de eventos no confirmados del agregado. Además, actualiza la versión del agregado e incluye metadatos como la versión, la categoría, la fecha de ocurrencia y el Id del evento, antes de invocar al método ApplyEvent que es donde se aplica el cambio al estado de la entidad.

ApplyEvent

Type: public virtual void ApplyEvent(IDomainEvent @event)

Aplica un evento de dominio específico al agregado. Determina dinámicamente el método Apply apropiado según el tipo de evento y lo invoca.

Rehydrate

Type: public static TAggregate Rehydrate<TAggregate>(Guid id, IEnumerable<IDomainEvent> events)

El método Rehydrate<TAggregate>(Guid id, IEnumerable<IDomainEvent> events) reconstruye un agregado a partir de su historial de eventos. Crea una nueva instancia del agregado y aplica cada evento en orden para restaurar su estado.

Implementación


namespace CodeDesignPlus.Net.Event.Sourcing.Abstractions;
/// <summary>
/// Represents the contract to be implemented by the aggregate root.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="AggregateRoot"/> class.
/// </remarks>
public abstract class AggregateRoot(Guid id) : Core.Abstractions.AggregateRoot(id), IAggregateRoot
{
/// <summary>
/// The cache of the delegates to create instances of the aggregate root.
/// </summary>
private static readonly Dictionary<Type, Delegate> instanceDelegatesCache = [];
/// <summary>
/// The cache of the methods to apply the changes that occur in the aggregate root.
/// </summary>
private static readonly Dictionary<Type, MethodInfo> applyMethodsCache = [];
/// <summary>
/// The category of the aggregate root.
/// </summary>
public abstract string Category { get; protected set; }
/// <summary>
/// The version of the aggregate root.
/// </summary>
public long Version { get; private set; } = -1;
/// <summary>
/// Adds a domain event to the list of events that have occurred in the aggregate root.
/// </summary>
/// <param name="event">The domain event to add to the list of events that have occurred in the aggregate root.</param>
protected override void AddEvent(IDomainEvent @event)
{
@event.Metadata.Add("Version", ++this.Version);
@event.Metadata.Add("Category", this.Category);
@event.Metadata.Add("OccurredAt", @event.OccurredAt);
@event.Metadata.Add("EventId", @event.EventId);
base.AddEvent(@event);
ApplyEvent(@event);
}
/// <summary>
/// Applies the changes that occur in the aggregate root.
/// </summary>
/// <param name="event">The domain event to apply the changes.</param>
public virtual void ApplyEvent(IDomainEvent @event)
{
var eventType = @event.GetType();
if (!applyMethodsCache.TryGetValue(eventType, out var applyMethod))
{
applyMethod = GetType().GetMethod("Apply", BindingFlags.NonPublic | BindingFlags.Instance, null, [eventType], null);
if (applyMethod != null)
applyMethodsCache[eventType] = applyMethod;
}
applyMethod?.Invoke(this, [@event]);
}
/// <summary>
/// Rehydrates the aggregate root from the events that have occurred.
/// </summary>
/// <typeparam name="TAggregate">The type of the aggregate root.</typeparam>
/// <param name="id">The identifier of the aggregate root.</param>
/// <param name="events">The events that have occurred in the aggregate root.</param>
/// <returns>The aggregate root rehydrated from the events that have occurred.</returns>
public static TAggregate Rehydrate<TAggregate>(Guid id, IEnumerable<IDomainEvent> events)
where TAggregate : AggregateRoot
{
var aggregate = CreateOrGetDelegate<TAggregate>()(id);
foreach (var @event in events)
{
aggregate.ApplyEvent(@event);
aggregate.Version++;
}
return aggregate;
}
/// <summary>
/// Creates an instance of the aggregate root.
/// </summary>
/// <typeparam name="T">The type of the aggregate root.</typeparam>
/// <returns>An instance of the aggregate root.</returns>
private static Func<Guid, T> CreateOrGetDelegate<T>()
{
if (!instanceDelegatesCache.TryGetValue(typeof(T), out var instanceDelegate))
{
var parameter = Expression.Parameter(typeof(Guid), nameof(IAggregateRoot.Id).ToLower());
var constructor = typeof(T).GetConstructor([typeof(Guid)]);
var instante = Expression.New(constructor, parameter);
var lamda = Expression.Lambda<Func<Guid, T>>(instante, parameter);
instanceDelegate = lamda.Compile();
instanceDelegatesCache[typeof(T)] = instanceDelegate;
}
return (Func<Guid, T>)instanceDelegate;
}
}

La clase AggregateRoot es abstracta y debe ser heredada por clases concretas que representen agregados específicos de un dominio. La implementación se basa en los siguientes principios:

  • Gestión de Versiones: Cada evento está asociado con una versión del agregado, asegurando que los eventos se apliquen en el orden correcto y que el estado del agregado esté siempre sincronizado.
  • Categorización de Agregados: La propiedad Category permite categorizar los agregados, facilitando su identificación y manejo en la base de datos de eventos.
  • Aplicación Dinámica de Eventos: La búsqueda dinámica del método Apply por reflexión permite a cada agregado definir cómo responde a cada tipo de evento, sin necesidad de una lógica centralizada de aplicación de eventos.
  • Cache de Delegados: La creación de delegados para la creación del agregado se cachea, optimizando la performance al rehidratar los agregados.
  • Cache de Métodos: La búsqueda de los métodos Apply por tipo de evento se cachea también, evitando reflexión cada vez que se aplica un evento.

Ejemplo de Uso


El ejemplo de OrderAggregate muestra cómo implementar un agregado en el contexto de Event Sourcing en C#. Un agregado es una colección de objetos relacionados que se tratan como una unidad para fines de cambios de datos. En este caso, OrderAggregate representa un pedido que puede tener un nombre, un identificador de usuario y una lista de productos. El agregado también maneja eventos de dominio que representan cambios en el estado del agregado.

using CodeDesignPlus.Net.Event.Sourcing.Abstractions;
using CodeDesignPlus.Net.Event.Sourcing.Sample.Events;
namespace CodeDesignPlus.Net.Event.Sourcing.Sample.Aggregates;
public class OrderAggregate : AggregateRoot
{
public string? Name { get; private set; }
public Guid IdUser { get; private set; }
public List<string> Products { get; private set; } = [];
public override string Category { get; protected set; } = "Order";
public OrderAggregate(Guid id) : base(id) { }
private OrderAggregate(Guid id, string name, Guid idUser) : base(id)
{
this.Name = name;
this.IdUser = idUser;
}
public static OrderAggregate Create(Guid id, string name, Guid idUser)
{
var aggregate = new OrderAggregate(id, name, idUser);
aggregate.AddEvent(new OrderCreatedDomainEvent(id, name, idUser));
return aggregate;
}
public void UpdateName(string name)
{
this.AddEvent(new NameUpdatedDomainEvent(this.Id, name, this.IdUser));
}
public void AddProduct(string product)
{
this.AddEvent(new ProductAddedDomainEvent(this.Id, product));
}
private void Apply(OrderCreatedDomainEvent @event)
{
this.Name = @event.Name;
this.IdUser = @event.IdUser;
}
private void Apply(NameUpdatedDomainEvent @event)
{
this.Name = @event.Name;
}
private void Apply(ProductAddedDomainEvent @event)
{
this.Products.Add(@event.Product);
}
}
  1. Definición de la clase y propiedades

    Definimos la clase OrderAggregate que hereda de AggregateRoot y declaramos las propiedades Name, IdUser y Products que representan el nombre del pedido, el identificador del usuario y la lista de productos, respectivamente. También definimos la propiedad Category que representa la categoría del agregado que será utilizada por la implementación del servicio IEventSourcingService cuando se almacene el evento en la base de datos.

    public class OrderAggregate : AggregateRoot
    {
    public string? Name { get; private set; }
    public Guid IdUser { get; private set; }
    public List<string> Products { get; private set; } = new List<string>();
    public override string Category { get; protected set; } = "Order";
    }
  2. Constructores

    Definimos dos constructores, uno sin parámetros que es usado por el proceso de rehidratación y otro con parámetros que es usado para crear una nueva instancia del agregado a partir del patrón name constructor.

    public OrderAggregate(Guid id) : base(id) { }
    private OrderAggregate(Guid id, string name, Guid idUser) : base(id)
    {
    this.Name = name;
    this.IdUser = idUser;
    }
  3. Name Constructor

    Definimos un método estático Create que se encarga de crear una nueva instancia del agregado a partir de los parámetros proporcionados. En este caso, el método crea una nueva instancia del agregado y agrega un evento de dominio OrderCreatedDomainEvent que representa la creación del pedido.

    public static OrderAggregate Create(Guid id, string name, Guid idUser)
    {
    var aggregate = new OrderAggregate(id, name, idUser);
    aggregate.AddEvent(new OrderCreatedDomainEvent(id, name, idUser));
    return aggregate;
    }
  4. Métodos

    Definimos métodos para actualizar el nombre del pedido y agregar un producto al pedido. En ambos métodos, agregamos eventos de dominio que representan los cambios en el estado del agregado.

    Al invocar estos métodos, se agregan eventos de dominio a la lista de eventos del agregado. Estos eventos se aplican al estado del agregado invocando el método Apply con su respectivo evento.

    public void UpdateName(string name)
    {
    this.AddEvent(new NameUpdatedDomainEvent(this.Id, name, this.IdUser));
    }
    public void AddProduct(string product)
    {
    this.AddEvent(new ProductAddedDomainEvent(this.Id, product));
    }
  5. Métodos para Rehidratación

    Implementamos los métodos Apply para aplicar los cambios que ocurren en el agregado o cuando se rehidrata a partir de eventos de dominio. En este caso, implementamos los métodos Apply para los eventos OrderCreatedDomainEvent, NameUpdatedDomainEvent y ProductAddedDomainEvent.

    private void Apply(OrderCreatedDomainEvent @event)
    {
    this.Name = @event.Name;
    this.IdUser = @event.IdUser;
    }
    private void Apply(NameUpdatedDomainEvent @event)
    {
    this.Name = @event.Name;
    }
    private void Apply(ProductAddedDomainEvent @event)
    {
    this.Products.Add(@event.Product);
    }

Conclusiones


La clase AggregateRoot proporciona una base sólida para implementar agregados en sistemas basados en Event Sourcing. Su diseño modular y flexible permite a los desarrolladores construir modelos de dominio robustos y rastreables, centralizando la gestión del historial de eventos y la aplicación de cambios de estado.

Referencias Adicionales