Está en la página 1de 24

Patrón CQRS con MediatR

CQRS y el patrón mediador

La biblioteca MediatR se creó para facilitar dos patrones de arquitectura de software


principales: CQRS y el patrón Mediator. Si bien es similar, dediquemos un momento a
comprender los principios detrás de cada patrón.

CQRS

CQRS significa "Segregación de responsabilidad de consulta de comando". Como sugiere el


acrónimo, se trata de dividir la responsabilidad de los comandos (guardar) y las consultas (leer)
en diferentes modelos.

Si pensamos en el patrón CRUD de uso común (Crear-Leer-Actualizar-Eliminar), generalmente


tenemos la interfaz de usuario interactuando con un almacén de datos responsable de las
cuatro operaciones. En cambio, CQRS nos haría dividir estas operaciones en dos modelos, uno
para las consultas (también conocido como "R") y otro para los comandos (también conocido
como "CUD").

La siguiente imagen ilustra cómo funciona esto:

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 .

¿Qué problema está tratando de resolver esto?


Bueno, una razón común es que cuando diseñamos un sistema, comenzamos con el
almacenamiento de datos. Realizamos la normalización de la base de datos, agregamos claves
primarias y externas para hacer cumplir la integridad referencial, agregamos índices y, en
general, nos aseguramos de que el "sistema de escritura" esté optimizado. Esta es una
configuración común para una base de datos relacional como SQL Server o MySQL. Otras
veces, primero pensamos en los casos de uso de lectura, luego intentamos agregarlos a una
base de datos, preocupándonos menos por la duplicación u otras preocupaciones de bases de
datos relacionales (a menudo se usan "bases de datos de documentos" para estos patrones).

Ningún enfoque es incorrecto . Pero el problema es que es un acto de equilibrio constante


entre las lecturas y las escrituras, y eventualmente un lado "ganará". Todo desarrollo posterior
significa que ambos lados deben ser analizados y, a menudo, uno se ve comprometido.

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.

Algunos de estos pueden incluir:

Administrar sistemas separados (si la capa de aplicación está dividida)

Los datos se vuelven obsoletos (si la capa de la base de datos está dividida)

La complejidad de gestionar múltiples componentes

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.

En la siguiente sección, analicemos un patrón similar llamado Mediator .


Patrón mediador

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

Podemos ver en la imagen de arriba, SomeService envía un mensaje al Mediador, y el


Mediador luego invoca múltiples servicios para manejar el mensaje. No hay dependencia
directa entre ninguno de los componentes azules.

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.

Cómo MediatR facilita CQRS y patrones de mediador

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.

Configuración de una API ASP.NET Core con MediatR

Configuración del proyecto

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

Instalemos un par de paquetes a través de Package Manager Console.

Primero, el paquete MediatR:

PM> install-package MediatR

A continuación, un paquete que conecta MediatR con el contenedor ASP.NET DI:

PM> install-package MediatR.Extensions.Microsoft.DependencyInjection

Clase de inicio o programa para .NET 6 y superior

Abramos Startup.csy agreguemos una declaración de uso:

using MediatR;

A continuación, modifiquemos ConfigureServices:

services.AddMediatR(typeof(Startup));
En .NET 6, tenemos que modificar la clase Program:

builder.Services.AddMediatR(typeof(Program));

Ahora MediatR está configurado y listo para funcionar.

Justo antes de pasar a la creación del controlador, vamos a modificar el archivo


launchSettings.json:

"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.

En la carpeta Controladores, agreguemos un "Controlador API - Vacío", con el nombre


ProductsController.cs.
Entonces terminamos con la siguiente clase:

[Route("api/products")]

[ApiController]

public class ProductsController : ControllerBase

Luego agreguemos un constructor que inicialice una IMediatRinstancia:

[Route("api/products")]

[ApiController]

public class ProductsController : ControllerBase

private readonly IMediator _mediator;

public ProductsController(IMediator mediator) => _mediator = mediator;

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:

public interface ISender


{

Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken


cancellationToken = default);

Task<object?> Send(object request, CancellationToken cancellationToken = default);

public interface IPublisher

Task Publish(object notification, CancellationToken cancellationToken = default);

Task Publish<TNotification>(TNotification notification, CancellationToken cancellationToken


= default)

where TNotification : INotification;

public interface IMediator : ISender, IPublisher

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 class Product

public int Id { get; set; }

public string Name { get; set; }

Tan simple como eso.


Ahora, agreguemos una nueva FakeDataStoreclase y modifiquemos:

public class FakeDataStore

private static List<Product> _products;

public FakeDataStore()

_products = new List<Product>

new Product { Id = 1, Name = "Test Product 1" },

new Product { Id = 2, Name = "Test Product 2" },

new Product { Id = 3, Name = "Test Product 3" }

};

public async Task AddProduct(Product product)

_products.Add(product);

await Task.CompletedTask;

public async Task<IEnumerable<Product>> GetAllProducts() => await


Task.FromResult(_products);

Aquí simplemente estamos interactuando con una lista estática de productos, que es
suficiente para nuestros propósitos.

ConfigureServicesActualicemos para Startup.csconfigurar nuestro DataStore como singleton :

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.

Separación de los comandos y las consultas

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".

Solicitudes con MediatR

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).

Usaremos el FakeDataStoreque creamos anteriormente para implementar algunas solicitudes


de MediatR.

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:

public record GetProductsQuery() : IRequest<IEnumerable<Product>>;

Aquí, creamos un registro llamado GetProductsQuery, que implementa


IRequest<IEnumerable<Product>>. Esto simplemente significa que nuestra solicitud devolverá
una lista de productos.

Luego, en la carpeta Controladores, vamos a crear una nueva clase de controlador para
manejar nuestra consulta:

public class GetProductsHandler : IRequestHandler<GetProductsQuery,


IEnumerable<Product>>

private readonly FakeDataStore _fakeDataStore;

public GetProductsHandler(FakeDataStore fakeDataStore) => _fakeDataStore =


fakeDataStore;

public async Task<IEnumerable<Product>> Handle(GetProductsQuery request,

CancellationToken cancellationToken) => await _fakeDataStore.GetAllProducts();

Algo está pasando aquí, así que vamos a desglosarlo un poco.

Creamos una clase llamada GetProductsHandler, que hereda de


IRequestHandler<GetProductsQuery, IEnumerable<Product>>. Esto significa que esta clase se
encargará GetProductsQuery, en este caso, de devolver la lista de productos.

En nuestra clase, implementamos un solo método llamado , que devuelve los valores de
nuestro .GetProductsHandler HandleFakeDataStore
Llamando y probando nuestra solicitud

Para llamar a nuestra solicitud, solo necesitamos agregar la GetProducts()acción en nuestro


ProductsController:

[HttpGet]

public async Task<ActionResult> GetProducts()

var products = await _mediator.Send(new GetProductsQuery());

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.

Ahora asegurémonos de que todo funcione como se esperaba.

Primero, presionemos CTRL+F5 para compilar y ejecutar nuestra aplicación.

Entonces encendamos Postman y creemos una nueva solicitud:

Solicitud de cartero para GetProducts

¡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.

Dentro de nuestra carpeta "Comandos", agreguemos un registro llamado


AddProductCommand:

public record AddProductCommand(Product Product) : IRequest;

Entonces, nuestro registro tiene una sola Productpropiedad y hereda de la IRequestinterfaz.


Observe que esta vez la IRequestfirma no tiene un parámetro de tipo. Esto se debe a que no
estamos devolviendo un valor.

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 .

Luego, en la Handlerscarpeta, vamos a agregar nuestro controlador:

public class AddProductHandler : IRequestHandler<AddProductCommand, Unit>

private readonly FakeDataStore _fakeDataStore;

public AddProductHandler(FakeDataStore fakeDataStore) => _fakeDataStore =


fakeDataStore;

public async Task<Unit> Handle(AddProductCommand request, CancellationToken


cancellationToken)

await _fakeDataStore.AddProduct(request.Product);
return Unit.Value;

Creamos la Handlerclase, que hereda de la IRequestHandler<AddProductCommand,


Unit>interfaz. Esta es una implementación similar a nuestra anterior GetValuesQuery.
Simplemente estamos diciendo que esta clase manejará la solicitud y devolverá un vacío.
Cuando usamos , en lugar de void, usamos la estructura que representa un tipo
void.AddProductCommand MediatRUnit

Luego, implementamos el Handle(AddProductCommand request, CancellationToken


cancellationToken) método, agregando nuestro valor a nuestro FakeDataStore.

Llamando y probando nuestra solicitud

Ahora llamemos a nuestro AddProductCommandagregando el Postmétodo en


ProductsController:

[HttpPost]

public async Task<ActionResult> AddProduct([FromBody]Product product)

{ {

await _mediator.Send(new AddProductCommand(product));

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:

Solicitud de cartero para AddProduct


Para probar que realmente funcionó, ejecutemos nuestra GetAllProductssolicitud
nuevamente:

Solicitud de cartero para GetAllProducts

Esto prueba que nuestro AddProductCommandestá funcionando correctamente, enviando un


mensaje a MediatR con nuestro nuevo valor y actualizando el estado. Entonces podemos ver
que nuestro Querymodelo (por ejemplo, GetAllProducts) ha sido actualizado con nuestro
nuevo valor.

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. 🙂

Trabajar con comandos que devuelven un valor

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.

Pero para hacer eso, tenemos que crear la acción GetProductById.

Por supuesto, antes de hacer eso, debemos crear un nuevo registro de consulta:

public record GetProductByIdQuery(int Id) : IRequest<Product>;

Modifique la FakeDataStoreclase agregando un nuevo método:


public async Task<Product> GetProductById(int id) =>

await Task.FromResult(_products.Single(p => p.Id == id));

Y crea un nuevo controlador:

public class GetProductByIdHandler : IRequestHandler<GetProductByIdQuery, Product>

private readonly FakeDataStore _fakeDataStore;

public GetProductByIdHandler(FakeDataStore fakeDataStore) => _fakeDataStore =


fakeDataStore;

public async Task<Product> Handle(GetProductByIdQuery request, CancellationToken


cancellationToken) =>

await _fakeDataStore.GetProductById(request.Id);

Excelente.

Ahora podemos agregar una nueva acción en el controlador:

[HttpGet("{id:int}", Name = "GetProductById")]

public async Task<ActionResult> GetProductById(int id)

var product = await _mediator.Send(new GetProductByIdQuery(id));

return Ok(product);

You can test this with a new Postman query if you want:

Obtener producto por id cqrs consulta


Comando de modificación

Con esto en su lugar, podemos modificar nuestro AddProductCommandregistro:

public record AddProductCommand(Product Product) : IRequest<Product>;

Entonces un controlador:

public class AddProductHandler : IRequestHandler<AddProductCommand, Product>

private readonly FakeDataStore _fakeDataStore;

public AddProductHandler(FakeDataStore fakeDataStore) => _fakeDataStore =


fakeDataStore;

public async Task<Product> Handle(AddProductCommand request, CancellationToken


cancellationToken)

await _fakeDataStore.AddProduct(request.Product);

return request.Product;

Por supuesto, esta es una implementación muy simplificada, pero entiendes el punto.

Y finalmente, podemos modificar nuestra acción:

[HttpPost]

public async Task<ActionResult> AddProduct([FromBody]Product product)

var productToReturn = await _mediator.Send(new AddProductCommand(product));

return CreatedAtRoute("GetProductById", new { id = productToReturn.Id },


productToReturn);
}

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:

Crear producto con mejor respuesta

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.

Los ejemplos pueden ser:

Enviando un correo electrónico

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.

Actualizando nuestro FakeDataStore


Abramos nuestro FakeDataStorey agreguemos un nuevo método:

public async Task EventOccured(Product product, string evt)

_products.Single(p => p.Id == product.Id).Name = $"{product.Name} evt: {evt}";

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.

Crear la notificación y los controladores

Definamos un mensaje de notificación que encapsule el evento que nos gustaría definir.

Primero, agreguemos una nueva carpeta llamada Notificaciones.

Dentro de esa carpeta, agreguemos un registro llamado ProductAddedNotification:

public record ProductAddedNotification(Product Product) : INotification;

Aquí, creamos una clase llamada ProductAddedNotificationwhich implements INotification,


con una sola propiedad Product. Este es el equivalente a IRequestlo que vimos antes, pero
para Notificaciones.

Ahora, podemos crear nuestros dos controladores:

public class EmailHandler : INotificationHandler<ProductAddedNotification>

{
private readonly FakeDataStore _fakeDataStore;

public EmailHandler(FakeDataStore fakeDataStore) => _fakeDataStore = fakeDataStore;

public async Task Handle(ProductAddedNotification notification, CancellationToken


cancellationToken)

await _fakeDataStore.EventOccured(notification.Product, "Email sent");

await Task.CompletedTask;

public class CacheInvalidationHandler : INotificationHandler<ProductAddedNotification>

private readonly FakeDataStore _fakeDataStore;

public CacheInvalidationHandler(FakeDataStore fakeDataStore) => _fakeDataStore =


fakeDataStore;

public async Task Handle(ProductAddedNotification notification, CancellationToken


cancellationToken)

await _fakeDataStore.EventOccured(notification.Product, "Cache Invalidated");

await Task.CompletedTask;

Con estas dos clases, creamos dos controladores llamados EmailHandlery


CacheInvalidationHandlerque esencialmente hacen lo mismo:

Implemente INotificationHandler<ProductAddedNotification>, lo que significa que puede


manejar ese evento

Llame al método EventOccured en FakeDataStore, especificando el evento que ocurrió

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

A continuación, debemos activar nuestra notificación.

Abramos ProductsControllery modifiquemos el Postmétodo:

[HttpPost]

public async Task<ActionResult> AddProduct([FromBody]Product product)

var productToReturn = await _mediator.Send(new AddProductCommand(product));

await _mediator.Publish(new ProductAddedNotification(productToReturn));

return CreatedAtRoute("GetProductById", new { id = productToReturn.Id },


productToReturn);

Además de enviar la AddProductCommandsolicitud a MediatR, ahora enviamos a MediatR


nuestro ProductAddedNotification, esta vez usando el Publish método.

Si quisiéramos, podríamos haberlo hecho directamente en el controlador de


AddProductCommand, pero ubiquémoslo aquí para simplificar.

Probando nuestras Notificaciones

Para probar que las cosas funcionan, ejecutemos nuestra aplicación y ejecutemos nuevamente
la solicitud para GetProducts:

MediatR - Notificaciones - Obtener valores

Como esperábamos, tenemos los tres valores que inicializamos en el


constructor.FakeDataStore
A continuación, ejecutemos la otra solicitud de Postman para agregar un nuevo producto.

Ahora, ejecutemos la GetProductssolicitud de nuevo:

Notificaciones de MediatR - Visualización de nuestros eventos

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.

Construyendo comportamientos de MediatR

A menudo, cuando creamos aplicaciones, tenemos muchas preocupaciones transversales.


Estos incluyen autorización, validación y registro.

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.

Veamos cómo implementar un comportamiento de MediatR que registra por nosotros.

Creando nuestro Comportamiento

Primero, agreguemos otra carpeta de solución llamada "Comportamientos":


Carpeta de comportamientos para la funcionalidad de comportamiento de MediatR

A continuación, agreguemos una clase dentro de la carpeta llamada LoggingBehavior:

public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest,


TResponse>

private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)

=> _logger = logger;

public async Task<TResponse> Handle(TRequest request, CancellationToken


cancellationToken,

RequestHandlerDelegate<TResponse> next)

_logger.LogInformation($"Handling {typeof(TRequest).Name}");

var response = await next();

_logger.LogInformation($"Handled {typeof(TResponse).Name}");

return response;

Expliquemos nuestro código:

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>

Luego implementamos el Handlemétodo, iniciando sesión antes y después de llamar al


next()delegado.
Este controlador de registro se puede aplicar a cualquier solicitud y registrará la salida antes y
después de que se maneje.

Registrando nuestro Comportamiento

Para registrar nuestro comportamiento, agreguemos una línea a ConfigureServicesin Startup:

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

Probando nuestro comportamiento

Ejecutemos nuestra aplicación, esta vez usando el atajo F5 para ejecutar en modo de
depuración.

Luego abramos Postman y ejecutemos la solicitud GetAllProducts.

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:

Comportamientos - Ver nuestro comportamiento

¡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

Dividir nuestras lecturas/escrituras en aplicaciones separadas (modificando para publicar en


Kafka/Service Bus, luego haciendo que una segunda aplicación lea desde el bus de
mensajes)ProductAddedNotification

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!

También podría gustarte