CQRS con MediatR en .NET 6

En este artículo, mostraré una breve introducción sobre CQRS y Mediator. También veremos un ejemplo práctico de cómo usar estos patrones en .NET 6 usando MediatR.

Introducción a patrones CQRS y Mediator

¿Qué es CQRS?

El patrón de segregación de responsabilidad de consultas y comandos (CQRS) separa las operaciones de consulta y actualización de un almacén de datos.

Esto maximiza rendimiento, escalabilidad y seguridad de una aplicación.

En las arquitecturas tradicionales, se utiliza el mismo modelo de datos para consultar y actualizar una base de datos. Es sencillo y funciona bien para las operaciones CRUD básicas. Sin embargo, en aplicaciones más complejas, este enfoque puede resultar difícil de manejar.

CQRS separa las lecturas y las escrituras en diferentes modelos, usa «command» para actualizar los datos y «query» para leer los datos.

Queries

Son consultas que devuelven un resultado sin cambiar el estado del sistema y no tienen efectos secundarios.

  • Las consultas nunca modifican la base de datos. Una consulta devuelve un DTO que no encapsula ningún conocimiento del dominio.

Commands

Cambian el estado de un sistema.

  • Los comandos se deben basar en tareas, en lugar de centrarse en los datos.
  • Los comandos se pueden colocar en una cola para su procesamiento asincrónico, en lugar de procesarse sincrónicamente.

CQRS se puede llevar a cabo de tres formas diferentes

1. Trabajar con base de datos única. Aquí los comandos y las consultas funcionan en la misma base de datos.

2. Bases de datos separadas de comandos y consultas. Aquí la base de datos de «escritura» funciona para las tareas crear, actualizar y eliminar. Todas las cargas de consulta funcionan en la base de datos de «lectura». De esta forma, purificamos la base de datos de escritura de la carga de consultas. Podríamos tener consultas más rápidas usando una vista materializada que es un objeto de base de datos que contiene los resultados de una consulta.

3. CQRS- Event Source. La base de datos de «escritura» ahora está representada por la cola de eventos (almacén de eventos). El controlador de eventos es el componente que consume eventos del almacén de eventos y, al usar estos eventos, actualiza los datos en la base de datos de «lectura». Por lo tanto, los estados actuales de las entidades se almacenan solo en la base de datos de «lectura». Todo el historial de transformación de entidades se puede extraer utilizando una secuencia de eventos que se almacena en la base de datos de «escritura».

¿Por qué usar CQRS?

Estas son algunas ventajas de CQRS:

  • Escalado independiente. CQRS permite las cargas de trabajo de lectura y escritura que se escalen de forma independiente, lo que puede dar lugar a menos contenciones de bloqueo.
  • Esquemas de datos optimizados. El lado de lectura puede usar un esquema que está optimizado para las consultas, mientras que el lado de escritura utiliza un esquema que está optimizado para las actualizaciones.
  • Seguridad. Es más fácil asegurarse de que solo las entidades de dominio correctas realicen escrituras en los datos.
  • Separación de cuestiones. La separación de lectura y escritura puede dar lugar a modelos que sean más flexibles y fáciles de mantener. La mayor parte de la lógica de negocios compleja entra en el modelo de escritura. El modelo de lectura puede ser relativamente sencillo.
  • Consultas más sencillas. Al almacenar una vista materializada en la base de datos de lectura, la aplicación puede evitar combinaciones complejas cuando hace consultas.

¿Qué es Mediator?

Es un patrón de comportamiento que permite reducir las dependencias caóticas entre objetos. El patrón restringe las comunicaciones directas entre los objetos y los obliga a colaborar solo a través de un objeto mediador. El patrón mediador define un objeto que encapsula como un conjunto de objetos interactúan entre sí.

Los objetos no se comunican de forma directa entre ellos, en lugar de ello se comunican mediante el mediador. Esto reduce las dependencias entre los objetos en comunicación, reduciendo entonces la dependencia de código.

MediatR es una implementación del patrón Mediator en .NET. Admite solicitudes/respuestas, comandos, consultas, notificaciones y eventos, síncronos y asíncronos con envío inteligente a través generic variance de C#.

Veamos el código?

Construiremos una aplicación que ilustre cómo usar patrones CQRS y Mediator. La aplicación será simple para que podamos centrarnos más en construir el patrón CQRS utilizando MediatR. Si tu café está listo comencemos.☕️

CQRS

En primer lugar, comencemos con la implementación del patrón CQRS.

  • Primero, añadimos los modelos de las solicitudes (Requests):
public class AddPostRequestModel
{
    public Guid UserId { get; set; }
    public Guid Id { get; set; }
    public string? Title { get; set; }
    public string? Body { get; set; }
}
public class GetPostRequestModel
{
    public Guid PostId { get; set; }
}
  • Segundo, añadimos los modelos de las respuestas (Responses):
public class AddPostResponseModel
{
    public Guid PostId { get; set; }
}
public class GetPostResponseModel
{
    public Guid Id { get; set; }
    public Guid UserId { get; set; }
    public string? Title { get; set; }
    public string? Body { get; set; }
}
  • Seguidamente, agregamos dos interfaces, que contendrán métodos que más adelante implementaremos:
public interface IAddPostCommandHandler
{
    AddPostResponseModel AddPost(AddPostRequestModel request);
}
public interface IGetPostQueryHandler
{
    GetPostResponseModel GetPost(GetPostRequestModel request);
}
  • Ahora sí, implementamos clases que heredan de estas interfaces y sus métodos (Handlers):
public class AddPostCommandHandler : IAddPostCommandHandler
{
    public AddPostResponseModel AddPost(AddPostRequestModel request)
    {
        // your logic add post...

        return new AddPostResponseModel()
        {
            PostId = Guid.NewGuid()
        };
    }
}
public class GetPostQueryHandler : IGetPostQueryHandler
{
    public GetPostResponseModel GetPost(GetPostRequestModel request)
    {
        // your logic get post...

        return new GetPostResponseModel()
        {
            Body = "It is a long established fact that a reader will...",
            Id = Guid.Parse("556b8afa-5617-425c-87af-381f278ccf90"),
            Title = "What is Lorem Ipsum?",
            UserId = Guid.Parse("40cea88f-9cdb-4509-a8bd-a307d149daf0")
        };
    }
}
  • Vamos con el controlador. Añadimos dependencias al código de inicio de la aplicación en el archivo «program.cs»:
builder.Services.AddScoped<IAddPostCommandHandler, AddPostCommandHandler>();
builder.Services.AddScoped<IGetPostQueryHandler, GetPostQueryHandler>();
  • Por último, creamos el controlador e inyectamos dependencias IAddPostCommandHandler y IGetPostCommandHandler. Finalmente ya tenemos el patrón CQRS listo para usar?
[ApiController]
[Route("[controller]")]
public class PostController : ControllerBase
{
    private readonly IAddPostCommandHandler _addPostCommandHandler;
    private readonly IGetPostQueryHandler _getPostQueryHandler;

    public PostController(IAddPostCommandHandler addPostCommandHandler, IGetPostQueryHandler getPostQueryHandler)
    {
        _addPostCommandHandler = addPostCommandHandler;
        _getPostQueryHandler = getPostQueryHandler;
    }

    [HttpGet(Name = "postDetails")]
    public IActionResult Get([FromQuery] GetPostRequestModel request)
    {
        var response = _getByIdQueryHandler.GetPost(request);
        return Ok(response);
    }

    [HttpPost(Name = "addPost")]
    public IActionResult Post([FromBody] AddPostRequestModel request)
    {
        var response = _addPostCommandHandler.AddPost(request);
        return Ok(response);
    }
}

MediatR

Seguidamente, continuaremos con el patrón Mediator, que nos ayuda a resolver los siguientes problemas:

  • Reducir el número de conexiones entre clases.
  • Encapsulación de objetos utilizando la interfaz del mediador.
  • Proporcionar una interfaz unificada para gestionar las dependencias entre clases.

Apliquemos MediatR al ejemplo anterior.

Al comienzo, instalamos la biblioteca «MediatR»:

Install-Package MediatR.Extensions.Microsoft.DependencyInjection

Refactorizamos el código fuente de la siguiente manera:

  • En primer lugar, cuando comienza a usar la biblioteca MediatR, lo primero que debe definir es «request». Las solicitudes describen el comportamiento de sus comandos y consultas. IRequest<T> es la solicitud o mensaje que indica la tarea a realizar, solicitada por algún servicio y dirigida a n Handlers. Es decir, el mediador va a tomar el IRequest<T> y se lo mandará a los handlers registrados. Estos handlers saben del mensaje que pueden recibir y ellos saben cómo se llevará a cabo la tarea.
public class AddPostRequestModel : IRequest<AddPostResponseModel>
{
    public Guid UserId { get; set; }
    public Guid Id { get; set; }
    public string? Title { get; set; }
    public string? Body { get; set; }
}
public class GetPostRequestModel : IRequest<GetPostResponseModel>
{
    public Guid PostId { get; set; }
}
  • En segundo lugar, cuando se crea una solicitud, necesitaremos un controlador para resolver la solicitud, para esto actualizamos los controladores con IRequestHandler<T>. Todos los controladores deben implementar la interfaz IRequestHandler. Esta interfaz depende de dos parámetros. Primero — solicitud, segundo — respuesta.
public class AddPostCommandHandler : IRequestHandler<AddPostRequestModel, AddPostResponseModel>
{
    public async Task<AddPostResponseModel> Handle(AddPostRequestModel request, CancellationToken cancellationToken)
    {
        // your logic add post...

        return new AddPostResponseModel()
        {
            PostId = Guid.NewGuid()
        };
    }
}
public class GetPostQueryHandler : IRequestHandler<GetPostRequestModel, GetPostResponseModel>
{
    public async Task<GetPostResponseModel> Handle(GetPostRequestModel request, CancellationToken cancellationToken)
    {
        // your logic get post...

        return new GetPostResponseModel()
        {
            Body = "It is a long established fact that a reader will...",
            Id = Guid.Parse("556b8afa-5617-425c-87af-381f278ccf90"),
            Title = "What is Lorem Ipsum?",
            UserId = Guid.Parse("40cea88f-9cdb-4509-a8bd-a307d149daf0")
        };
    }
}
  • En tercer lugar, registramos MediatR en «program.cs»:
//builder.Services.AddScoped<IAddPostCommandHandler, AddPostCommandHandler>();
//builder.Services.AddScoped<IGetPostQueryHandler, GetPostQueryHandler>();
builder.Services.AddMediatR(Assembly.GetExecutingAssembly());
  • Entonces, tenemos nuestra «request» y nuestro «handler», pero ¿cómo usarlos? Todo lo que necesitamos es definir el controlador, inyectar el mediador y enviar la consulta. Eso es todo.
[ApiController]
[Route("[controller]")]
public class PostController : ControllerBase
{
    private readonly IMediator _mediator;

    public PostController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpGet(Name = "postDetails")]
    public IActionResult Get([FromQuery] GetPostRequestModel request)
    {
        var response = _mediator.Send(request);
        return Ok(response);
    }

    [HttpPost(Name = "addPost")]
    public IActionResult Post([FromBody] AddPostRequestModel request)
    {
        var response = _mediator.Send(request);
        return Ok(response);
    }
}
  • Por último, refactorizamos eliminando las interfaces IAddPostCommandHandler y IGetPostQueryHandler, porque MediatR proporciona una interfaz unificada para gestionar las dependencias entre clases.

Validaciones con FluentValidation

Podemos mejorar lo anterior, añadiendo validaciones usando FluentValidation, es una biblioteca de validación de .NET de uso gratuito que nos ayuda a que las validaciones sean limpias, fáciles de crear y mantener. Veamos el código siguiente:

  • Instalamos FluentValidation desde la consola de paquetes:
Install-Package FluentValidation.DependencyInjectionExtensions
Install-Package FluentValidation.AspNetCore
  • Ahora, sigamos adelante y agreguemos un nuevo validador con nuestra regla directamente en las clases AddPostRequestModel y GetPostRequestModel.

Creamos una clase llamada AddPostCommandValidator que hereda de la clase AbstractValidator<T>, especificando el tipo AddPostRequestModel. Esto le permite a FluentValidation saber que esta validación es para la clase AddPostRequestModel.

public class AddPostCommandValidator : AbstractValidator<AddPostRequestModel>
{
    public AddPostCommandValidator()
    {
        RuleFor(v => v.Title)
            .MaximumLength(200)
            .NotEmpty()
            .WithMessage("Title is empty.");

        /*...*/
    }
}
public class GetPostQueryValidator : AbstractValidator<GetPostRequestModel>
{
    public GetPostQueryValidator()
    {
        /*...*/

        RuleFor(x => x.PostId)
            .Must(ValidateGuid)
            .WithErrorCode("Not a guid");
    }

    private bool ValidateGuid(Guid arg)
    {
        return Guid.TryParse(arg.ToString(), out var result);
    }
}

Podemos agregar tantas reglas como queramos, encadenar validadores e incluso usar validadores personalizados.

  • Finalmente, añadimos FluentValidation al código de inicio de la aplicación:
builder.Services.AddFluentValidation(options =>
{
    options.RegisterValidatorsFromAssembly(Assembly.GetExecutingAssembly());
});

Todas las implementaciones de estos ejemplos están disponibles en el código de GitHub, que podéis descargar.

En este post presenté la arquitectura básica de CQRS y cómo implementarla con MediatR. Pero en este enlace, Clean Architecture .NET 6, te mostraré cómo implementar un ejemplo CQRS del mundo real.

Eso es todo, por el momento, seguiré actualizando y agregando contenido en este post. Espero que te haya resultado interesante😉

Descargas

El código fuente de este artículo se puede encontrar en GitHub

Enlaces de interés

MediatR

Clean Architecture .NET 6