Event Sourcing en Azure - parte 3: validación de comandos

David Guida
Computación en la nube
La tercera parte de la serie Event Sourcing on Azure, veremos cómo podemos hacer una validación sencilla en un comando antes de activar su ejecucion.

Event Sourcing en Azure - parte 3: validación de comandos




La última vez se revisó cómo se pueden utilizar CosmosDB y ServiceBus para almacenar los eventos de los Aggregates. No es una solución completa y todavía hay algunas áreas grises, pero creo que se ha cubierto la mayor parte del terreno.


Sin embargo, toda aplicación respetable necesita validar los datos antes de procesarlos y almacenarlos. No se puede crear un nuevo cliente en nuestro sistema porque alguien nos lo diga. ¿Qué pasa si se tienen datos de entrada basura o repetidos?


Ya escribí en el pasado sobre la Validación de Comandos (Command Validation) en CQRS. Bueno, eso fue hace casi 4 años. Es antiguo, pero la idea se mantiene.


Para SuperSafeBank decidí tomar un enfoque más ágil y empezar añadiendo código de validación directamente en los Command Handlers. ¿Por qué? Bueno, porque se desea mantener las cosas simples, eso es todo.


Así que, volviendo al comando CreateCustomer:


public class CreateCustomer
{   
    public Guid CustomerId { get; }
    public string FirstName { get; }
    public string LastName { get; }
    public string Email { get; }
}


Para el ejemplo, digamos que lo único que se quiere asegurar es que la dirección de correo electrónico sea única en todo el sistema. Sin embargo, no se está haciendo ninguna validación en el formato.


La validación de comandos (Command Validation) debe asegurarse de que las reglas de negocio se cumplan. Las otras preocupaciones "básicas" como rangos, formatos y demás, deben ser manejadas, antes de esto, creando los apropiados Objetos de Valor (Value Objects).


El comando incluye también un ID de cliente prellenado. No se quiere depender de la capa de Persistencia para que nos lo devuelva porque los Comandos CQRS deberían ser casi de ejecuto-y-olvido (fire-and-forget). La ejecución de los comandos no devolverá ningún resultado. O bien funcionan o bien descartan inmediatamente.


Pero se necesita un ID de vuelta, así que una opción sería generar un GUID aleatorio cuando se crea el Comando:


[HttpPost]
public async Task Create(CreateCustomerDto dto, CancellationToken cancellationToken = default)
{
    if (null == dto)
        return BadRequest();
    var command = new CreateCustomer(Guid.NewGuid(), dto.FirstName, dto.LastName, dto.Email);
    await _commandHandler.Process(command, cancellationToken);
   
    return CreatedAtAction("GetCustomer", new { id = command.Id }, command);
}


Ahora, otra cosa que se necesita es un Servicio de Correo Electrónico de Clientes. Algo básico, que se encargue simplemente de almacenar los correos electrónicos y comprobar si ya existe alguno:


public interface ICustomerEmailsService
{
    Task ExistsAsync(string email);
    Task CreateAsync(string email, Guid customerId);
}


Se codificará una implementación basada en CosmosDB, utilizando la dirección de correo electrónico como clave de partición.


El paso final es conectar todo lo anterior y añadir la validación al manejador de comandos (Command handler):


public class CreateCustomerHandler : INotificationHandler
{
    private readonly IEventsService _eventsService;
    private readonly ICustomerEmailsService _customerEmailsRepository;

    public async Task Handle(CreateCustomer command, CancellationToken cancellationToken)
    {
        if (await _customerEmailsRepository.ExistsAsync(command.Email)){
            var error = new ValidationError(nameof(CreateCustomer.Email), $"email '{command.Email}' already exists");
            throw new ValidationException("Unable to create Customer", error);
        }

        var customer = new Customer(command.Id, command.FirstName, command.LastName, command.Email);
        await _eventsService.PersistAsync(customer);
        await _customerEmailsRepository.CreateAsync(command.Email, command.Id);
    }
}



Como somos gente amable, podemos configurar nuestro sistema para capturar las ValidationExceptions y devolverlas al usuario en el formato adecuado. Andrew Lock escribió un muy buen artículo sobre los Detalles del Problema, mostrando cómo aprovechar un Middleware para manejarlos.


Ahora, en un mundo ideal esto podría ser suficiente, pero… ¿Qué sucede si se produce un error cuando se almacena el correo electrónico del cliente? Ya se han persistido los eventos, pero no guardar el correo electrónico significa que se podría pasar la validación con la misma dirección. Esto dará lugar a que se creen dos clientes con el mismo correo electrónico, lo que rompería las reglas de negocio especificadas.


Entonces, ¿cómo se puede manejar esto? Una opción es añadir el soporte de Transacción y revertir la ejecución completa del Handler si las cosas van mal. Para más detalles, se puede revisar la técnica Two-Phase-Commit o al Patrón Outbox.


La próxima entrega se revisará qué pasa con los Eventos Agregados (Aggregate Events) una vez que se ejecuta un Comando.


¡Ciao!

Traducido por Alfredo Baron

David Guida

Passionate, hard-working, and innovative software engineer with a strong desire for continuous learning and improving.
Over 15 years of experience in different domains, including Healthcare, Finance, and large-scale e-Commerce.

Microsoft MVP for Developer Technologies, confident on back-end and front-end tech stacks including .NET, NodeJs, PHP, Angular, React, as well as extensive experience on SQL, MongoDB, Azure, and GCP.

Everything is "just a tool". The secret is to know which one best suits your needs.

https://www.davidguida.net/

Related Posts

Únete a nuestra Newsletter

Lidera la Conversación en la Nube