DEV Community

Isaac Ojeda
Isaac Ojeda

Posted on • Updated on

 

[Parte 1] ASP.NET: Implementando CQRS con MediatR

Introducción

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

Estoy hablando de CQRS, un patrón que se ha convertido en mi forma default de diseñar sistemas en los últimos años.

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 código a mi github y podrás ver este código 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 librerías como MediatR. Aunque lo que vimos en aquel post es un tema diferente, se une totalmente porque el mediador nos permite una facilidad en muchos temas al diseñas sistemas. De igual forma, veremos un repaso de CQRS.

Command Query Responsibility Segregation es lo que CQRS dice en sus iniciales. Es un patrón de diseño que se ha vuelto muy popular en los últimos años. La idea detrás de CQRS es partir lógicamente el flujo de nuestra aplicación en dos flujos distintos:

  • Commands: Modifican el estado del dominio, no idempotente.
  • Queries: Consultan el estado del dominio, operación idempotente.

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

La siguiente imagen muestra la idea principal de cómo funciona:

Image description

Como podemos ver, la aplicación simplemente se parte en dos conceptos principales, queries y commands. La idea principal de CQRS es también partir ese datastore en dos (uno máster y otro replicado) para leer de uno y escribir en el otro, pero la idea de partirlo de una forma lógica funciona muy bien en el diseño del sistema, aunque se use una misma base de datos (que sin problema se podría implementar el uso de bases de datos físicamente separadas).

¿Qué problema se intenta resolver?

El diseño tradicional de aplicaciones en “n-capas” suelen dividir en tres capas: UI, Business Logic, Data Store.

En sus inicios esto no tiene ningún problema, pero el problema está en el mantenimiento y la falta de flexibilidad de agregar nueva funcionalidad, de depuración 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. También se suelen contar con Servicios de la misma forma, gigantes.

La segregación de responsabilidades es una cuestión importante al mantenimiento de un sistema. Modificar una funcionalidad no debería 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 aprendizaje sea muy alta. Cuando un junior quiera modificar una funcionalidad, claro que dará miedo romper algo, ya que toda esa funcionalidad está fuertemente acoplada en el servicio/repositorio.

Separar en Queries y Commands y mejor aún, en Vertical Slices (Features) permitirá tener un código bien separado, agregar funcionalidad significará agregar más Queries/Commands y no modificar Services o Repositories gigantes.

También que sea testeable de una forma más 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 más, se encuentra totalmente encapsulado de otra funcionalidad, modificar otro comando no debe afectar a otros.

Claro está, que debemos de saber cuándo refactorizar. Si tenemos un command que hace x tarea, pero en otro comando también lo hace, tal vez es tiempo de pensar sobre otros tipos de patrones (Strategy, decorators, etc) y refactorizar. También debemos de encontrar balance con DRY (Don’t Repeat Yourself) sin ignorar el Single Responsability (es un lio ¿no? con el tiempo será más fácil, te lo prometo).

Mediator Pattern

El patrón mediador simplemente es la definición de un objeto que encapsula como otros objetos interactúan entre sí. En lugar de tener dos o más 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 según 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 razón por lo que el patrón mediador es muy útil, es por la misma razón que usamos patrones como Inversion Of Control. Nos permite totalmente desacoplar componentes pero que aun así interactúen entre sí. Lo menos que tenga que considerar un componente para funcionar, es más fácil desarrollarlo, evolucionarlo y testearlo.

MediatR nos facilita implementar CQRS y el patrón mediador

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

El término *in-proces*s es una importante limitación aquí. Como .NET maneja toda interacción entre objetos en un mismo proceso, no es apropiado si queremos separar los Queries y Commands a nivel aplicación (es decir, tener sistemas separados).

Para este tipo de escenarios es mejor utilizar algún 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 (específicamente, 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 mías 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 podría 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 implementar 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>
Enter fullscreen mode Exit fullscreen mode

Podemos ignorar todo el código 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 código):

Controllers/
Domain/
Features/
├─ Products/
Infrastructure/
├─ Persistence/
Enter fullscreen mode Exit fullscreen mode

Siempre trato de hacerlo siguiendo los conceptos que típicamente usaríamos en una clean architecture, por ahora no importa si lo hago todo en un solo proyecto, con el tiempo decidirás como dividir tus proyectos (dos o más proyectos en una misma solución, 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; }
}
Enter fullscreen mode Exit fullscreen mode

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 prácticas que les enseño.

Infrastructure → Persistence

Como siempre, utilizaremos Entity Framework Core para la persistencia

using MediatrValidationExample.Domain;
using Microsoft.EntityFrameworkCore;

namespace MediatrValidationExample.Infrastructure.Persistence;
public class MyAppDbContext : DbContext
{
    public MyAppDbContext(DbContextOptions<MyAppDbContext> options) : base(options)
    { }

    public DbSet<Product> Products => Set<Product>();
}
Enter fullscreen mode Exit fullscreen mode

Para crear la base de datos y su migración inicial, ejecutamos el siguiente comando:

dotnet ef migrations add FirstMigration -o Infrastructure/Persistence/Migrations
Enter fullscreen mode Exit fullscreen mode
dotnet ef database update
Enter fullscreen mode Exit fullscreen mode

Features → Products → Queries

Este folder representa el Application Core, aquí irán 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 práctica que acabo de adoptar del Vertical Slice Architecture. Si quieres saber más sobre el tema, también 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; }
}
Enter fullscreen mode Exit fullscreen mode

Lo importante aquí es poner atención 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 veíamos 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 cómo se llevará acabo la tarea.

En este caso, GetProductQuery ****es un IRequest<T> que lo que representa en sí, es buscar un producto. IRequest<T> incluye un genérico 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 método GetById. En este caso, la clase representa la operación a realizar, no un método más de una clase con más métodos.

Esto es lo que me encanta de este patrón, tendremos muchos archivos y carpetas, eso sí, pero archivos pequeños y fáciles de buscar gracias a los poderosos editores de texto / IDEs.

GetProductQueryHandler es el handler del mismo Query definido arriba. Como están en el mismo archivo, podríamos decir que el Request y Handler están acoplados entre sí, pero aislados de lo demás.

Agregar funcionalidad o testearla simplemente involucra lo que está en este archivo y nada más.

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; }
}
Enter fullscreen mode Exit fullscreen mode

En este otro ejemplo, el IRequest está vacío, pero si quisiéramos buscar productos, agregar paginación, ordenamiento, etc. Se haría en esta clase GetProductsQuery, ya que representa el request que recibe la API (lo veremos en el controller).

Todos los Queries deberían de incluir el método AsNoTracking, por la razón misma que son Queries y no necesitan actualizar ningún estado de los Entities.

Features → Products → Commands

En los comandos ahora sí se actualizarán los entities, en post posteriores enseñaré como agregar validaciones, decoradores y entre otras cosas que son bien fáciles de hacer gracias a otras librerías 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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 genérico, porque los comandos generalmente no regresan información.

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);
}
Enter fullscreen mode Exit fullscreen mode

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 ningún 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 podría hacerse con un [FromQuery] sin ningún problema para recibir parámetros 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í deberías de empezar para ir probando mientras escribes tus queries gg)

En Program.cs hacemos lo siguiente (lo pongo completo porque es pequeño)

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

Aquí suceden varias cosas que hay que comentar:

  • AddEndpointsApiExplorer y AddSwaggerGen son configuración default ya incluida en la plantilla. Sabemos que esto habilita la generación 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 aplicación e ingresar a /swagger para que puedas ver su funcionamiento.

Conclusión

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

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

Utilizar CQRS tiene sus ventajas y también podría tener sus desventajas, aunque el sistema esté bien dividido en Features, Queries y Commands. Entre más crezca, cada miembro nuevo del equipo obviamente tendrá su curva de aprendizaje, y si nunca utilizó este tipo de patrones, aumentará su curva. Pero es para un bien mayor.

Diseñar sistemas mantenibles debe de ser también 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 división 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

Top comments (13)

Collapse
 
mrdave1999 profile image
Dave Roman • Edited

Hola Isaac, gracias por escribir el tutorial, me ha servido para mantener un proyecto.

De igual manera me gustaría dar mis observaciones sobre su artículo, que en realidad no es para generar una discusión o algo por el estilo.

Citaré algunos párrafos:

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

(2) 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 aprendizaje sea muy alta.

(3) También que sea testeable de una forma más 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.

Los inconvenientes que mencionan los párrafos no es un problema en sí del patrón de Repositorio o de agente de servicio, sino de las malas prácticas que se aplican al proyecto. Sí sigue los principios de ingeniería como el de dependencias explícitas y de responsabilidad única, se puede lograr tener una clase altamente cohesiva y de bajo acoplamiento.

Un ejemplo simple obtenido del código de nopCommerce:
CustomerRegistrationService.cs

La clase CustomerRegistrationService está saturada, tiene 20 dependencias! Solo mirando el constructor es una señal que la clase debe ser dividida por partes, para así cumplir con el principio de responsabilidad única, sin embargo, para lograrlo no es necesario crear un método de servicio por clase: ValidateCustomerHandler.Handle, RegisterCustomerHandler.Handle, etc.

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

CQRS tiene sus ventajas. El problema es cuando se aplica de manera tan estricta y rígida, por lo que esto conduce a tener un montón de archivos de clases por cada acción que realice un punto final (endpoint): GetProductsQueryHandler, EditProductsQueryHandler, etc. La desventaja de hacerlo así es que cuando se necesite realizar alguna modificación (o estudio) a una funcionalidad, se debe de acceder a muchas clases (es como moverte entre clases). Para Dave es una desventaja, tal vez para otros no.

Me gusto mucho esta implementación de CQRS: github.com/Odonno/cqrs-dotnet-core... (no es tan estricta).

Este comentario es solo mi punto de vista. Saludos!

Collapse
 
isaacojeda profile image
Isaac Ojeda

Revisando el repositorio de Odonno realmente yo creo ya son opiniones de cada quien, pero aquí vuelve a ser lo mismo que busco evitar, aunque sigas el single responsability, la clase ParkingCommandHandler la vas a tener que partir para no tener una clase gigante como la CustomerRegistrationService.

Tener varios métodos Handle en una clase también me causa conflicto, ya que la forma en la que navego el código, no se me haría práctico, aunque estoy seguro que es algo trivial a lo que te puedes acostumbrar.

Te recomiendo esta charla de Jimmy Bogard, esa es una razón por la que decido hacer una clase por feature, a veces hay features muy pesados que tienen mucha lógica, tener todo junto en ese feature me resulta práctico y fácil de entender. Entiendo que puedes aplicar mil principios y formas para limpiar el CommandHandler y que no se convierta en un "big ball of mud" al final como dices, son opiniones jeje.

Saludos ya por última vez!

Collapse
 
mrdave1999 profile image
Dave Roman

Sí, utilizar una clase por feature cuando la lógica es demasiada compleja, me parece bien. Sin embargo, he visto proyectos que por cada feature "simple" crean una clase y apenas tiene cinco líneas de código (o peor aún, una sola línea), entonces, al menos para mí, me parece tedioso estar moviéndome entre tantos archivos que al final tienen una lógica demasiada simple.

Y otro detalle que he visto en estos proyectos, es que usan nombres de clases que representan acciones, por ejemplo GetProductsByIdHandler, al menos para mí lo veo confuso, ya que estoy acostumbrado a nombrar clases como sustantivos (tal vez documentando las clases puede ser de ayuda para saber cuál es su intención).

De igual manera, gracias por responder Issac.
Saludos!

Collapse
 
isaacojeda profile image
Isaac Ojeda

Volviendo a leer tu comentario:

La desventaja de hacerlo así es que cuando se necesite realizar alguna modificación (o estudio) a una funcionalidad, se debe de acceder a muchas clases (es como moverte entre clases)

Realmente es lo contrario, un archivo tiene de responsabilidad un feature y para cambiar ese feature hay que irnos solamente a ese archivo, no a uno gigante como un repositorio (como lo menciona el post)

Tal vez dependiendo del equipo y el tipo de proyecto, se deben de evaluar la estructura del proyecto, pero al final siempre se deben de seguir los principios de ingeniería como lo mencionas.

Saludos nuevamente!

Collapse
 
isaacojeda profile image
Isaac Ojeda • Edited

Muchas gracias por tus comentarios, son totalmente válidos.

Tal vez, la razón por la que a mi me funciona tener cientos de archivos como lo mencionas, es por mi uso de navegación en el editor.

Ejem. En VS Code CTRL + P y luego mi búsqueda nada explícita, ejem. GetProdQH haciendo referencia a GetProductsQueryHandler. El buscador lo hace muy bien y no es necesario escribir toda la palabra, tal vez esa es la razón por la que a mi o en mi equipo no nos resulta un problema tener muchos archivos.

Gracias por compartir nuevamente. Saludos!

Collapse
 
pontiacgtx profile image
PontiacGTX

reddit.com/r/csharp/comments/rxxg5.... Según esto usar mediatr es 52x más ineficiente que usar un servicio directamente,y creo que para un web API está métrica es importante,a veces escribir menos código no significa más eficiencia a coste de rendimiento

Collapse
 
isaacojeda profile image
Isaac Ojeda • Edited

Mira este vídeo de Nick y los comentarios de zshazz (en el hilo de reddit) son interesantes y están acorde al vídeo.

Es mentira que sea 52 veces más lento.

Lo que sí es real, sí existe una degradación del performance, pero en ciertos escenarios es donde vas a "sentirlo". Por los beneficios que agrega MediatR y el tipo de aplicación, para mi sigue siendo una opción 99% recomendada. No todos programamos el siguiente Netflix que requiere 1,000 microservicios, pero sí programamos para estar listos a adaptarnos a eso.

Gracias por tu comentario, siempre es bueno indagar y cuestionar, por que eso nos lleva a investigar más. Saludos.

UPDATE. Importante mencionar, que si solo usas el .Send() del mediador y nada de Decorators, Publish y > cosas similares, no vale la pena usar MediatR. Siempre hay que analizar cada proyecto para saber que le conviene usar.

Collapse
 
zedrikk profile image
zedrikk

Excelente aporte. Esperare con paciencia la parte 2. Gracias por compartir

Collapse
 
isaacojeda profile image
Isaac Ojeda

Gracias, ya está arriba la parte 2. Saludos

Collapse
 
jpmontoya182 profile image
Juan Pablo Montoya Cardona • Edited

Excelente articulo, tuve que agregar en el constructor de MyAppDbContext esta instruccion :

public MyAppDbContext(DbContextOptions options) : base(options)
{
Database.EnsureCreated();
}

Saludos

Collapse
 
isaacojeda profile image
Isaac Ojeda

Tienes razón, no expliqué la parte donde hice la migración y actualización de la BD...

Gracias!

Collapse
 
edd profile image
NSysX • Edited

En el CreateProductCommandHandler , como seria la modificacion si necesito agregar una lista de objectos al CreateProductCommand , en lugar de un solo objeto.
Gracias

Collapse
 
isaacojeda profile image
Isaac Ojeda

Pues tendrías que incluirlo en el IRequest como un listado, una idea es:

public class CreateProductCommand : IRequest
{

     public List<CreateProductItem> Products { get;set;} = new();

     public class CreateProductItem
     {
          public string Description { get; set; } = default!;
          public double Price { get; set; }
     }
}
Enter fullscreen mode Exit fullscreen mode

Y en el Handler, iteras la creación del producto.

Pueden haber más formas, pero algo así lo haría yo

Saludos!!

Regex for lazy developers

regex for lazy devs

You know who you are. Sorry for the callout 😆