DEV Community

Desarrollando una API en ASP.NET con CQRS y MediatR: Parte 1

Isaac Ojeda on March 31, 2022

Introducción En esta publicación, exploraremos un tema del que ya hemos hablado en ocasiones anteriores, pero esta vez queremos profundi...
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
 
arasagui profile image
Antonio Aguila

Antes que nada quiero agradecer estos posts, es valiosa la información que nos compartes se aprende mucho!

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!!