Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
March 31, 2022 12:02 am GMT

[Parte 1] CQRS y MediatR: Implementando CQRS en ASP.NET.

Introduccin

En este post veremos de un tema que ya he hablado en otras ocasiones, pero en este caso quisiera profundizar ms y empezar una serie de posts que nos permitan conocer distintos patrones de diseo al momento de desarrollar servicios web.

Estoy hablando de CQRS, un patrn que se ha convertido en mi forma default de disear sistemas en los ltimos aos.

CQRS tiene sus ventajas y seguro sus desventajas, que hasta ahora no me ha dolido ninguna.

Espero que este post te sea de utilidad. Te recuerdo que siempre subo el cdigo a mi github y podrs ver este cdigo aqu.

Qu es CQRS?

En posts anteriores mencion un par de razones de por qu es muy buena idea hacer uso de CQRS, especialmente si estamos usando libreras como MediatR. Aunque lo que vimos en aqul post es un tema diferente, se une totalmente porque el mediador nos permite una fcilidad en muchos temas al diseas sistemas. De igual forma, veremos un repaso de CQRS.

Command Query Responsibility Segregation es lo que CQRS dice en sus iniciales. Es un patrn de diseo que se ha vuelto muy popular en los ltimos aos. La idea detrs de CQRS es partir lgicamente el flujo de nuestra aplicacin en dos flujos distintos:

  • Commands: Modifican el estado del dominio, no idemponente.
  • Queries: Consultan el estado del dominio, operacin idemponente.

Si pensamos en un CRUD, los comandos (los que cambian el estado) sern Create, Update y Delete. Los Quieries, pues la lectura Read.

La siguiente imagen muestra la idea principal de cmo funciona:

Image description

Como podemos ver, la aplicacin simplemente se parte en dos conceptos principales, queries y commands. La idea principal de CQRS es tambin partir ese datastore en dos (uno master y otro replicado) para leer de uno y escribir en el otro, pero la idea de partirlo de una forma lgica funciona muy bien en el diseo del sistema aunque se use una misma base de datos (que sin problema se podra implementar el uso de bases de datos fsicamente separadas).

Qu problema se intenta resolver?

El diseo tradicional de aplicaciones en n-capas suelen dividir en tres capas: UI, Business Logic, Data Store.

En sus inicios esto no tiene ningn problema, pero el problema est en el mantenimiento y la falta de flexibilidad de agregar nueva funcionalidad, de depuracin y entre otras cosas.

En sistemas n-capas se cuenta con Repositorios enormes, donde se encuentran todas las operaciones que puedes hacer en un entity. Tambin se suelen contar con Servicios de la misma forma, gigantes.

La segregacin de responsabilidades es una cuestin importante al mantenimiento de un sistema. Modificar una funcionalidad no debera de afectar a cosas totalmente externas. Tener una clase ProductsService donde se encuentre todo lo que hace el sistema sobre los productos, se convertir en un problema s o s cuando este sistema no pare de crecer, ingresen nuevos miembros al equipo y la curva de aprendisaje sea muy alta. Cuando un junior quiera modificar una funcionalidad, claro que dar miedo romper algo, ya que toda esa funcionalidad est fuertemente acomplada en el servicio/repositorio.

Separar en Queries y Commands y mejor an, en Vertical Slices (Features) permitir tener un cdigo bien separado, agregar funcionalidad significar agregar ms Queries/Commands y no modificar Services o Repositories gigantes.

Tambin que sea testeable de una forma ms sencilla, un servicio puede tener dependencias para las distintas operaciones que hace sobre un Entity. Ese servicio necesitar todos esos mocks para probar x o y. Un Command solo tendr lo que necesita para funcionar, y nada ms, se encuentra totalmente encapsulado de otra funcionalidad, modificar otro comando no debe afectar a otros.

Claro est, que debemos de saber cuando refactorizar. Si tenemos un command que hace x tarea, pero en otro comando tambin lo hace, tal vez es tiempo de pensar sobre otro tipos de patrones (Strategy, decorators, etc) y refactorizar. Tambin debemos de encontrar balance con DRY (Dont Repeat Yourself) sin ignorar el Single Responsability (es un lio no? con el tiempo ser ms fcil, te lo prometo).

Mediator Pattern

El patrn mediador simplemente es la definicin de un objeto que encapsula como otros objetos interactuan entre si. En lugar de tener dos o ms objetos que dependen directamente de otros objetos, solo toman dependencia directa de un mediador y este se encarga de gestionar las interacciones entre objetos:

Image description

Como podemos ver SomeService manda un mensaje al mediador, y el mediador manda a llamar otros servicios para que hagan algo segn el mensaje recibido. SomeService no conoce nada sobre los otros servicios que hacen algo con su solicitud, solo le dice al mediador que necesita que se haga algo.

La razn por lo que el patrn mediador es muy til, es por la misma razn que usamos patrones como Inversion Of Control. Nos permite totalmente desacoplar componentes pero que aun as interactuen entre si. Lo menos que tenga que considerar un componente para funcionar, es ms fcil desarrollarlo, evolucionarlo y testearlo.

MediatR nos facilita implementar CQRS y el patrn mediador

MediatR es una implementacin del mediador que ocurre todo in-process (en la misma aplicacin), y totalmente, nos ayuda a crear sistemas con CQRS. Toda la comunicacin entre el usuario y la persistencia ocurre por medio de MediatR.

El trmino *in-proces*s es una importante limitacin aqu. Como .NET maneja toda interaccin entre objetos en un mismo proceso, no es apropiado si queremos separar los Queries y Commands a nivel aplicacin (es decir, tener sistemas separados).

Para este tipo de escenarios es mejor utilizar algn Message Broker, como ya lo vimos en este post que escrib.

Implementando CQRS en ASP.NET Core

La idea de utilizar CQRS en ASP.NET Core (especificamente, una Web API) es delegar la responsabilidad de procesar cada Request a un Handler y no al Controller (y aparte todo lo que vimos anteriormente arriba).

Por qu? Podemos tener varias razones, las mias son, que todo el procesamiento de los requests de la API no dependan de los Controllers y lo delega a alguien en el Application Core (pensando en Clean architecture o Vertical Slice).

Sin problema alguno, en .NET 7, por performance podra empezar a utilizar Minimal APIs. Ya que los controllers no realizan tarea alguna (solo recibir la solicitud) y podemos hacer ese tipo de cambios sin problemas.

Para implemenar CQRS en asp.net core utilizando MediatR (y de ejemplo, una base de datos SQLite) utilizaremos los siguientes paquetes en un proyecto Web API (dotnet new webapi):

<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /><PackageReference Include="FluentValidation.AspNetCore" Version="10.4.0" /><PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" /><PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.3" /><PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.3">   <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>   <PrivateAssets>all</PrivateAssets></PackageReference>

Podemos ignorar todo el cdigo ejemplo que viene en la plantilla (las clases Weather y eso) y trabajaremos con la siguiente estructura en un mismo proyecto (de igual forma, te recomiendo revisar el cdigo):

Controllers/Domain/Features/ Products/Infrastructure/ Persistence/

Siempre trato de hacerlo siguiendo los conceptos que tipicamente usaramos en una clean architecture, por ahora no importa si lo hago todo en un solo proyecto, con el tiempo decidirs como dividir tus proyectos (dos o ms proyectos en una misma solucin, etc).

Domain

Aqu realmente no hay nada que explicar, simplemente usaremos una clase Product para hacer este ejemplo.

namespace MediatrValidationExample.Domain;public class Product{    public int ProductId { get; set; }    public string Description { get; set; } = default!;    public double Price { get; set; }}

Nota : Aqu usamos el operador default! simplemente para tener un string inicializado y le decimos al compilador que nunca ser null (lo cual es una reverenda mentira, el default de un string es null . Son las malas prcticas que les enseo.

Infrastructure Persistence

Como siempre, utilizaremos Entity Framework Core para la persistencia

usingMediatrValidationExample.Domain;usingMicrosoft.EntityFrameworkCore;namespaceMediatrValidationExample.Infrastructure.Persistence;publicclassMyAppDbContext:DbContext{publicMyAppDbContext(DbContextOptions<MyAppDbContext>options):base(options){ }publicDbSet<Product>Products=>Set<Product>();}

Features Products Queries

Este folder representa el Application Core, aqu irn los Queries y Commands que requiera la Web Api. Podemos empezar con el ejemplo simple de consultar Producto(s).

La forma en que les mostrar como hago los Queries y Commands es una practica que acabo de adoptar del Vertical Slice Architecture. Si quieres saber ms sobre el tema, tambin he escrito sobre ello.

En resumen, la idea es poner todo lo que se necesite en un solo archivo (El request, handler, validators, mappers, models, etc) y como comento en el post, refactorizar si es necesario (igual es otro tema, pero ya queda a tu criterio como hacerlo).

using MediatR;using MediatrValidationExample.Infrastructure.Persistence;namespace MediatrValidationExample.Features.Products.Queries;public class GetProductQuery : IRequest<GetProductQueryResponse>{    public int ProductId { get; set; }}public class GetProductQueryHandler : IRequestHandler<GetProductQuery, GetProductQueryResponse>{    private readonly MyAppDbContext _context;    public GetProductQueryHandler(MyAppDbContext context)    {        _context = context;    }    public async Task<GetProductQueryResponse> Handle(GetProductQuery request, CancellationToken cancellationToken)    {        var product = await _context.Products.FindAsync(request.ProductId);        return new GetProductQueryResponse        {            Description = product.Description,            ProductId = product.ProductId,            Price = product.Price        };    }}public class GetProductQueryResponse{    public int ProductId { get; set; }    public string Description { get; set; } = default!;    public double Price { get; set; }}

Lo importante aqu es poner atencin en la interfaz IRequest<T> y IRequestHandler<T>.

IRequest<T> es la solicitud o mensaje que indica la tarea a realizar, solicitada por SomeService y dirigida a n Handlers (como lo veamos en la imagen arriba).

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 como se llevar acabo la tarea.

En este caso, GetProductQuery ****es un IRequest<T> que lo que representa en si, es buscar un producto. IRequest<T> incluye un genrico para poder especificar el tipo de objeto que va a regresar (ya que pues, es un query, estamos consultando el estado del dominio).

En otros tiempos, lo que se hubiera hecho es un ProductsService o ProductsRepository con un mtodo GetById. En este caso, la clase representa la operacin a realizar, no un mtodo ms de una clase con ms mtodos.

Esto es lo que me encanta de este patrn, tendremos muchos archivos y carpetas, eso s, pero archivos pequeos y fciles de buscar gracias a los poderosos editores de texto / IDEs.

GetProductQueryHandler es el handler del mismo Query definido arriba. Como estn en el mismo archivo, podramos decir que el Request y Handler estn acoplados entre s, pero aislados de lo dems.

Agregar funcionalidad o testearla simplemente involucra lo que est en este archivo y nada ms.

using MediatR;using MediatrValidationExample.Infrastructure.Persistence;using Microsoft.EntityFrameworkCore;namespace MediatrValidationExample.Features.Products.Queries;public class GetProductsQuery : IRequest<List<GetProductsQueryResponse>>{}public class GetProductsQueryHandler : IRequestHandler<GetProductsQuery, List<GetProductsQueryResponse>>{    private readonly MyAppDbContext _context;    public GetProductsQueryHandler(MyAppDbContext context)    {        _context = context;    }    public Task<List<GetProductsQueryResponse>> Handle(GetProductsQuery request, CancellationToken cancellationToken) =>        _context.Products            .AsNoTracking()            .Select(s => new GetProductsQueryResponse            {                ProductId = s.ProductId,                Description = s.Description,                Price = s.Price            })            .ToListAsync();}public class GetProductsQueryResponse{    public int ProductId { get; set; }    public string Description { get; set; } = default!;    public double Price { get; set; }}

En este otro ejemplo, el IRequest est vaco, pero si quisieramos buscar productos, agregar paginacin, ordenamiento, etc. Se hara en esta clase GetProductsQuery, ya que representa el request que recibe la API (lo veremos en el controller).

Todos los Queries deberan de incluir el mtodo AsNoTracking, por la razn misma que son Queries y no necesitan actualizar ningn estado de los Entities.

Features Products Commands

En los comandos ahora s se actualizarn los entities, en post posteriores ensear como agregar validaciones, decoradores y entre otras cosas que son bien fciles de hacer gracias a otras libreras como FluentValidation y la misma ya usada MediatR.

using MediatR;using MediatrValidationExample.Domain;using MediatrValidationExample.Infrastructure.Persistence;namespace MediatrValidationExample.Features.Products.Commands;public class CreateProductCommand : IRequest{    public string Description { get; set; } = default!;    public double Price { get; set; }}public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand>{    private readonly MyAppDbContext _context;    public CreateProductCommandHandler(MyAppDbContext context)    {        _context = context;    }    public async Task<Unit> Handle(CreateProductCommand request, CancellationToken cancellationToken)    {        var newProduct = new Product        {            Description = request.Description,            Price = request.Price        };        _context.Products.Add(newProduct);        await _context.SaveChangesAsync();        return Unit.Value;    }}

Aqu lo nico que necesitamos del request, es el nombre del producto que queremos registrar y su precio. Se sigue usando la interfaz de MediatR IRequest, solo que ahora sin un tipo genrico, porque los comandos generalmente no regresan informacin.

Controllers

Dentro de controllers, por fin haremos uso del mediador. Ser de la siguiente manera:

using MediatR;using MediatrValidationExample.Features.Products.Commands;using MediatrValidationExample.Features.Products.Queries;using Microsoft.AspNetCore.Mvc;namespace MediatrValidationExample.Controllers;[ApiController][Route("api/products")]public class ProductsController : ControllerBase{    private readonly IMediator _mediator;    public ProductsController(IMediator mediator)    {        _mediator = mediator;    }    /// <summary>    /// Consulta los productos    /// </summary>    /// <returns></returns>    [HttpGet]    public Task<List<GetProductsQueryResponse>> GetProducts() => _mediator.Send(new GetProductsQuery());    /// <summary>    /// Crea un producto nuevo    /// </summary>    /// <param name="command"></param>    /// <returns></returns>    [HttpPost]    public async Task<IActionResult> CreateProduct([FromBody] CreateProductCommand command)    {        await _mediator.Send(command);        return Ok();    }    /// <summary>    /// Consulta un producto por su ID    /// </summary>    /// <param name="query"></param>    /// <returns></returns>    [HttpGet("{ProductId}")]    public Task<GetProductQueryResponse> GetProductById([FromRoute] GetProductQuery query) =>        _mediator.Send(query);}

Por Dependency Injection se solicita el mediador con la interfaz IMediator. Una vez teniendo el IRequest correspondiente inicializado, simplemente se lo mandamos al mediador y el determinar el handler(s) que deben de ejecutar la solicitud.

CreateProduct el IRequest (aka command) se recibe desde el Body del request (ya que es una clase POCO, se puede recibir y serializar sin ningn problema).

En GetProductyById el IRequest (aka Query) se obtiene del Path del URL. Aqu s es importante que en el segmento del Path se llame igual que la propiedad para que haga match.

En GetProducts se inicializa manualmente, ya que no estamos recibiendo nada desde la solicitud, pero podra hacerse con un [FromQuery] sin ningn problema para recibir parmetros adicionales.

Wrapping Up

Para poder correr todo esto, tenemos que configurar dependencias y todo lo necesario para que todo lo que acabamos de hacer funcione (tal vez, por aqu deberas de empezar para ir probando mientras escribes tus queries gg)

En Program.cs hacemos lo siguiente (lo pongo completo porque es pequeo)

using MediatR;using MediatrValidationExample.Domain;using MediatrValidationExample.Infrastructure.Persistence;using System.Reflection;var builder = WebApplication.CreateBuilder(args);builder.Services.AddEndpointsApiExplorer();builder.Services.AddSwaggerGen();builder.Services.AddControllers();builder.Services.AddMediatR(Assembly.GetExecutingAssembly());builder.Services.AddSqlite<MyAppDbContext>(builder.Configuration.GetConnectionString("Default"));var app = builder.Build();if (app.Environment.IsDevelopment()){    app.UseSwagger();    app.UseSwaggerUI();}app.UseHttpsRedirection();app.MapControllers();await SeedProducts();app.Run();async Task SeedProducts(){    using var scope = app.Services.CreateScope();    var context = scope.ServiceProvider.GetRequiredService<MyAppDbContext>();    if (!context.Products.Any())    {        context.Products.AddRange(new List<Product>        {            new Product            {                Description = "Product 01",                Price = 16000            },            new Product            {                Description = "Product 02",                Price = 52200            }        });        await context.SaveChangesAsync();    }}

Aqu suceden varias cosas que hay que comentar:

  • AddEndpointsApiExplorer y AddSwaggerGen son configuracin default ya incluida en la plantilla. Sabemos que esto habilita la generacin de documentos que describen la API usando OpenAPI.
  • AddControllers agrega lo necesario para poder usar ControllerBase en una API (no incluye razor ni nada que tenga que ver con Views)
  • AddMediatR agrega el mediador y busca todos los IRequest y IRequestHandlers que nuestro assembly tenga (o sea, en nuestro proyecto).
  • AddSqlite pues agrega el DbContext utilizando el proveedor SQLite
  • SeedProducts crea dos productos de ejemplos para que podamos jugar con la SwaggerUI y hacer pruebas.

En este punto ya puedes correr la aplicacin e ingresar a /swagger para que puedas ver su funcionamiento.

Conclusin

Hemos aprendido como configurar CQRS utilizando MediatR en un proyecto en ASP.NET Core Web API.

Vimos como podemos encapsular cada funcionalidad de nuestra API en archivos individuales, cada uno representando un Query o Command.

Utilizar CQRS tiene sus ventajas y tambin podra tener sus desventajas, aunque el sistema est bien dividido en Features, Queries y Commands. Entre ms crezca, cada miembro nuevo del equipo obviamente tendr su curva de aprendisaje, y si nunca utiliz este tipo de patrones, aumentar su curva. Pero es para un bien mayor.

Disear sistemas mantenibles debe de ser tambin una meta de cada Developer / Solution Architect, ya que haces un sistema y probablemente alguien en el futuro tendr que mantenerlo. Hacer ese proceso menos doloroso es lo mejor que se puede hacer.

Esta divisin de conceptos nos ha ayudado mucho en los ltimos proyectos desarrollados en mi equipo, agregar funcionalidad o modificarla no debe de ser un dolor de cabeza.

Referencias


Original Link: https://dev.to/isaacojeda/parte-1-cqrs-y-mediatr-implementando-cqrs-en-aspnet-56oe

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To