Documentos de Académico
Documentos de Profesional
Documentos de Cultura
CQRS
Diagrama CQRS
Como podemos ver, la aplicación simplemente separa los modelos de consulta y comando. El
patrón CQRS no establece requisitos formales sobre cómo se produce esta separación . Podría
ser tan simple como una clase separada en la misma aplicación (como veremos en breve con
MediatR), hasta aplicaciones físicas separadas en diferentes servidores. Esa decisión se basaría
en factores como los requisitos de escala y la infraestructura, por lo que no tomaremos esa
decisión hoy.
El punto clave es que para crear un sistema CQRS, solo necesitamos dividir las lecturas de las
escrituras .
CQRS nos permite "liberarnos" de estas consideraciones y darle a cada sistema el mismo
diseño y consideración que merece , sin preocuparnos por el impacto del otro sistema. Esto
tiene enormes beneficios tanto en el rendimiento como en la agilidad, especialmente si
equipos separados están trabajando en estos sistemas.
compensaciones
CQRS suena muy bien en principio, pero como con cualquier cosa en el software, siempre hay
compensaciones.
Los datos se vuelven obsoletos (si la capa de la base de datos está dividida)
En última instancia, depende de nuestros casos de uso específicos. Las buenas prácticas de
desarrollo nos alentarían a "mantenerlo simple" (KISS) y, por lo tanto, solo emplear estos
patrones cuando surja una necesidad. De lo contrario, es simplemente una optimización
prematura.
El patrón Mediator simplemente define un objeto que encapsula cómo los objetos interactúan
entre sí. En lugar de que dos o más objetos tengan una dependencia directa entre sí,
interactúan con un "mediador", que está a cargo de enviar esas interacciones a la otra parte:
Diagrama de mediador
La razón por la que el patrón Mediator es útil es la misma razón por la que los patrones como
Inversion of Control son útiles. Permite el "acoplamiento flexible", ya que el gráfico de
dependencia se minimiza y, por lo tanto, el código es más simple y más fácil de probar. En
otras palabras, cuantas menos consideraciones tenga un componente, más fácil será su
desarrollo y evolución.
Vimos en la imagen anterior como los servicios no tienen dependencia directa, y el productor
de los mensajes no sabe quién o cuántas cosas lo van a manejar. Esto es muy similar a cómo
funciona un intermediario de mensajes en el patrón "publicar/suscribir". Si quisiéramos
agregar otro controlador, podríamos, y el productor no tendría que modificarse.
Ahora que hemos repasado algo de teoría, hablemos de cómo MediatR hace que todas estas
cosas sean posibles.
Puede pensar en MediatR como una implementación de Mediator "en proceso", que nos
ayuda a construir sistemas CQRS. Toda la comunicación entre la interfaz de usuario y el
almacén de datos se realiza a través de MediatR.
El término “en proceso” es una limitación importante aquí. Dado que es una biblioteca .NET
que administra las interacciones dentro de las clases en el mismo proceso, no es una biblioteca
apropiada para usar si quisiéramos separar los comandos y las consultas en dos sistemas. En
esas circunstancias, un mejor enfoque sería un intermediario de mensajes como Kafka o Azure
Service Bus.
Sin embargo, para este artículo, nos quedaremos con un sistema CQRS simple de un solo
proceso, por lo que MediatR se ajusta perfectamente.
En primer lugar, abramos Visual Studio y creemos una nueva aplicación web ASP.NET Core,
seleccionando API como tipo de proyecto. Lo llamaremos CqrsMediatrExample.
dependencias
using MediatR;
services.AddMediatR(typeof(Startup));
En .NET 6, tenemos que modificar la clase Program:
builder.Services.AddMediatR(typeof(Program));
"profiles": {
"CqrsMediatrExample": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"launchUrl": "weatherforecast",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
Controlador
Ahora que tenemos todo instalado, configuremos un nuevo controlador que enviará mensajes
a MediatR.
[Route("api/products")]
[ApiController]
[Route("api/products")]
[ApiController]
La IMediatRinterfaz nos permite enviar mensajes a MediatR, que luego los envía a los
controladores relevantes. Debido a que ya instalamos el paquete de inyección de
dependencia, la instancia se resolverá automáticamente.
Una nota adicional. A partir de la versión 9.0 de MediatR, la interfaz de IMediator se divide en
dos interfaces: ISender e IPublisher. Entonces, aunque todavía podemos usar la interfaz
IMediator para enviar solicitudes a un controlador, si queremos ser más estrictos al respecto,
podemos usar la interfaz ISender en su lugar. No tienes que cambiar nada más. Esta interfaz
contiene el método Enviar para enviar solicitudes a los controladores. Por supuesto, para las
notificaciones, debe usar la interfaz de IPublisher que contiene el método Publish:
Data Store
Por lo general, nos gustaría interactuar con una base de datos real. Pero para este artículo,
vamos a crear una clase falsa que encapsule esta responsabilidad y simplemente interactúe
con algunos valores del Producto.
Pero antes de hacer eso, tenemos que crear una Productclase simple:
public FakeDataStore()
};
_products.Add(product);
await Task.CompletedTask;
Aquí simplemente estamos interactuando con una lista estática de productos, que es
suficiente para nuestros propósitos.
services.AddSingleton<FakeDataStore>();
O en .NET 6, tenemos que actualizar la clase Program:
builder.Services.AddSingleton<FakeDataStore>();
Ahora que nuestro almacén de datos está implementado, configuremos nuestra aplicación
para CQRS.
Después de todo, este artículo trata sobre CQRS, así que vamos a crear tres nuevas carpetas
para este propósito: "Comandos", "Consultas" y "Manejadores".
Usaremos estas carpetas a lo largo del ejercicio para separar nuestros modelos. Como se
mencionó anteriormente, estamos haciendo CQRS "en proceso", por lo que esta es una forma
sencilla de organizarlo.
En la siguiente sección, vamos a hablar sobre el uso más común de MediatR, "Solicitudes".
Las solicitudes de MediatR son mensajes de estilo de solicitud-respuesta muy simples, en los
que un solo controlador maneja de manera síncrona una sola solicitud (síncrona desde el
punto de vista de la solicitud, no asíncrono/espera interno de C#). Los buenos casos de uso
aquí serían devolver algo de una base de datos o actualizar una base de datos.
Hay dos tipos de solicitudes en MediatR. Uno que devuelve un valor y otro que no. A menudo,
esto corresponde a lecturas/consultas (que devuelven un valor) y escrituras/comandos
(normalmente no devuelven un valor).
Primero, creemos una solicitud que devuelva todos los productos de nuestro FakeDataStore.
GetProductsQuery
Como se trata de una consulta, agreguemos una clase llamada GetValuesQuerya la carpeta
"Consultas" e implementémosla:
Luego, en la carpeta Controladores, vamos a crear una nueva clase de controlador para
manejar nuestra consulta:
En nuestra clase, implementamos un solo método llamado , que devuelve los valores de
nuestro .GetProductsHandler HandleFakeDataStore
Llamando y probando nuestra solicitud
[HttpGet]
return Ok(products);
Así de sencillo es enviar una solicitud a MediatR. Tenga en cuenta que no dependemos de
FakeDataStore, ni tenemos idea de cómo se maneja la consulta. Este es uno de los principios
del patrón Mediator, y podemos verlo implementado de primera mano aquí con MediatR.
¡Fantástico! Esto prueba que MediatR está funcionando correctamente, ya que los valores que
vemos son los que inicializó nuestro FakeDataStore. Acabamos de implementar nuestra
primera "Consulta" en CQRS 🙂
En la siguiente sección, hablemos del otro tipo de solicitud de MediatR, siendo la que no
devuelve un valor, es decir, un “Comando”.
Comandos MediatR
Para crear nuestro primer "Comando", agreguemos una solicitud que tome un solo producto y
actualice nuestro FakeDataStore.
Tenga en cuenta que, debido a la simplicidad de este ejemplo, estamos utilizando la entidad de
dominio (Producto) como tipo de retorno para nuestra consulta y como parámetro para el
comando. En las aplicaciones del mundo real, no haríamos eso, usaríamos DTO para ocultar
una entidad de dominio de la API pública. Si desea ver cómo usar DTO con acciones de API
web, puede leer los artículos de la parte 5 y la parte 6 de nuestra serie .NET Core Web API .
await _fakeDataStore.AddProduct(request.Product);
return Unit.Value;
[HttpPost]
{ {
return StatusCode(201);
Nuevamente muy similar a nuestro método. Pero esta vez, estamos configurando un valor en
nuestro , y no devolvemos ningún valor.Get AddProductCommand
Para probar nuestro comando, ejecutemos nuestra aplicación nuevamente y agreguemos una
nueva solicitud a Postman:
Si bien esto puede parecer simple en teoría, intentemos pensar más allá del hecho de que
simplemente estamos actualizando una lista de cadenas en memoria. Nos comunicamos con
un almacén de datos a través de construcciones de mensajes simples, sin tener idea de cómo
se está implementando. Los comandos y consultas podrían apuntar a diferentes almacenes de
datos. No saben cómo se manejará su solicitud y no les importa .
En este punto, vamos a darnos una palmadita en la espalda, ya que ahora tenemos una API
ASP.NET Core completamente funcional que implementa los patrones CQRS + Mediator con
MediatR. 🙂
Como puede ver, nuestra acción POST solo devuelve un código de estado 201. Pero eso no es
suficiente. Hay una manera mucho mejor de informar a nuestro cliente que esta acción tuvo
éxito.
Por supuesto, antes de hacer eso, debemos crear un nuevo registro de consulta:
await _fakeDataStore.GetProductById(request.Id);
Excelente.
return Ok(product);
You can test this with a new Postman query if you want:
Entonces un controlador:
await _fakeDataStore.AddProduct(request.Product);
return request.Product;
Por supuesto, esta es una implementación muy simplificada, pero entiendes el punto.
[HttpPost]
Después de todos estos cambios, podemos enviar la solicitud de publicación, pero esta vez,
encontraremos un producto recién creado en el cuerpo de la respuesta y también, en la
pestaña del encabezado, Locationpara obtener ese nuevo producto:
Con todo esto en mente, puede implementar fácilmente las acciones Actualizar y Eliminar.
Ahora vayamos aún más lejos y en la siguiente sección exploremos otro tema de MediatR
llamado "Notificaciones".
Notificaciones MediatR
Entonces, solo hemos visto una sola solicitud manejada por un solo controlador. Sin embargo,
¿qué pasa si queremos manejar una sola solicitud de varios controladores?
Ahí es donde entran las notificaciones . En estas situaciones, generalmente tenemos múltiples
operaciones independientes que deben ocurrir después de algún evento.
Invalidar un caché
Para demostrar esto, actualizaremos el flujo que creamos anteriormente para publicar una
notificación y que dos controladores la manejen.AddProductCommand
Enviar un correo electrónico e invalidar un caché está fuera del alcance de este artículo, pero
para demostrar el comportamiento de las notificaciones, simplemente actualicemos nuestra
lista de valores falsos para indicar que se manejó algo.
await Task.CompletedTask;
Muy simple, buscamos un producto en particular y lo actualizamos para indicar un evento que
ocurrió en él.
Ahora que hemos modificado nuestra tienda, vamos a crear la notificación y los controladores
en la siguiente sección.
Definamos un mensaje de notificación que encapsule el evento que nos gustaría definir.
{
private readonly FakeDataStore _fakeDataStore;
await Task.CompletedTask;
await Task.CompletedTask;
En los casos de uso del mundo real, estos se implementarían de manera diferente,
probablemente tomando dependencias externas y haciendo algo significativo, pero aquí solo
estamos tratando de demostrar el comportamiento de las notificaciones.
Activación de la notificación
[HttpPost]
Para probar que las cosas funcionan, ejecutemos nuestra aplicación y ejecutemos nuevamente
la solicitud para GetProducts:
Como era de esperar, cuando agregamos un nuevo producto, ambos eventos se dispararon y
editaron el nombre. Si bien es un ejemplo artificial, la conclusión clave aquí es que podemos
disparar un evento y manejarlo muchas veces, sin que el productor sepa nada diferente.
Si quisiéramos ampliar nuestro flujo de trabajo para realizar una tarea adicional, simplemente
podríamos agregar un nuevo controlador. No necesitaríamos modificar la notificación en sí o la
publicación de dicha notificación, que nuevamente toca los puntos anteriores de extensibilidad
y separación de preocupaciones.
En la sección final, hablaremos sobre algo nuevo en MediatR 3.0, llamado Comportamientos.
En lugar de repetir esta lógica a través de nuestros controladores, podemos hacer uso de
Comportamientos. Los comportamientos son muy similares al middleware de ASP.NET Core, ya
que aceptan una solicitud, realizan alguna acción y luego (opcionalmente) transmiten la
solicitud.
RequestHandlerDelegate<TResponse> next)
_logger.LogInformation($"Handling {typeof(TRequest).Name}");
_logger.LogInformation($"Handled {typeof(TResponse).Name}");
return response;
Primero definimos una LoggingBehavior clase, tomando dos parámetros de tipo TRequesty
TResponse, e implementando la interfaz. En pocas palabras, este comportamiento puede
operar en cualquier solicitud.IPipelineBehavior<TRequest, TResponse>
services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
En .NET 6:
builder.Services.AddSingleton(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
Tenga en cuenta que estamos usando la notación <,> para especificar el comportamiento que
se puede usar para cualquier parámetro de tipo genérico
Ejecutemos nuestra aplicación, esta vez usando el atajo F5 para ejecutar en modo de
depuración.
Si luego abrimos la ventana " Salida " en Visual Studio y seleccionamos " Mostrar salida de:
Aplicación web - Servidor web ASP.NET Core ", vemos algunos mensajes interesantes:
¡Excelente! Esta es la salida de registro antes y después de que GetProductsse invoque nuestro
controlador de consultas.
Para obtener más información sobre el comportamiento de MediatR y cómo podemos usarlo
con FluentValidation para aplicar la validación en nuestro proyecto, puede leer nuestro
artículo CQRS Validation Pipeline with MediatR and FluentValidation .
Lo importante aquí es que no necesitábamos modificar nuestras solicitudes o controladores
existentes. Simplemente agregamos un nuevo comportamiento y lo conectamos.
Con la misma facilidad, podríamos agregar autorización y validación a toda nuestra aplicación,
de la misma manera, hacer que los comportamientos sean una excelente manera de manejar
las preocupaciones transversales de manera simple y concisa.
Conclusión
En este artículo, hemos repasado cómo se puede usar MediatR para implementar los patrones
CQRS y Mediator en ASP.NET Core . Hemos revisado las solicitudes y notificaciones, y cómo
manejar las preocupaciones transversales con los comportamientos.
MediatR proporciona un gran punto de partida para una aplicación que necesita evolucionar
de un simple monolito a una aplicación más madura, permitiéndonos separar las
preocupaciones de lectura y escritura y minimizando las dependencias entre el código.
Esto nos coloca en una excelente posición para tomar varios pasos adicionales posibles:
Use una base de datos diferente para las lecturas (tal vez extendiendo nuestra para agregar un
segundo controlador para escribir en una nueva base de datos, luego modificándola para leer
desde esta base de datos)ProductAddedNotification GetProductsQuery
Ahora tenemos nuestra aplicación en una excelente posición para realizar los pasos anteriores
si surge la necesidad, sin complicar demasiado las cosas a corto plazo.
Esperamos que haya disfrutado este artículo y tenga una buena base sobre CQRS y MediatR.
¡Feliz codificación!