Cómo implementar CQRS en ASP.NET usando MediatR
En este post te ensenaremos a usar Entity Framework Core para comandos y Dapper para consultas.
Cuando la gente piensa en CQRS, a menudo piensa en arquitecturas distribuidas, complejas y basadas en eventos con bases de datos de lectura y escritura separadas.
Usar diferentes bases de datos para lectura y escritura significa que podemos usar una arquitectura políglota, donde elegimos una base de datos que se adapta perfectamente al problema en cada lado.
Si no va a desarrollar una aplicación a gran escala, generalmente está bien ejecutar su configuración CQRS usando una sola base de datos. Esto simplifica mucho las cosas porque no tenemos que lidiar con la propagación de cambios desde el lado de escritura al lado de lectura o escenarios en los que cada lado no está sincronizado.
CQRS tiene dos procesos importantes y separados para gestionar las operaciones y consultas en un proyecto .net.
Proceso de escritura: crear/actualizar/eliminar datos
Para este proceso de escritura utilizararemos Entity Framework Core.
Entity Framework es un ORM potente que ayuda a encapsular la lógica empresarial y la validación para escribir datos. También utilizaremos el diseño basado en dominio para el lado de escritura para que la lógica comercial y de validación se pueda encapsular en nuestros modelos/entidades de escritura.
Proceso de lectura: leer datos
En el caso de realizar consultas a la base de datos, utilizaremos la herramienta Dapper.
Dapper es un mapeador de objetos liviano que permite leer datos de manera extremadamente eficiente.
Segregación de responsabilidades de consultas de comandos (CQRS)
CQRS divide el acceso a los datos en comandos y consultas. Cada operación de escritura o lectura tiene un comando o consulta dedicado, en comparación con los patrones más tradicionales donde se utilizan clases de "servicio" o "negocios" más grandes con muchos métodos diferentes.
Commands: escribir datos: crear/actualizar/eliminar
Queries: Leer datos
A continuación, se muestra cómo se puede crear y leer un pronóstico del tiempo utilizando CQRS en comparación con las implementaciones basadas en servicios.
Cada clase de comando y consulta tiene una clase de controlador correspondiente. Los comandos y consultas se envían a su controlador mediante una implementación de mediador en proceso sincrónica mediante el paquete MediatR.
Dividir nuestro código en Commands/Queries/Handlers granulares garantiza que se cumpla el principio de responsabilidad única (SRP), lo que hace que nuestras soluciones sean flexibles para los cambios y fáciles de probar.
Cuando utilizamos patrones basados en servicios, nuestras clases pueden volverse rápidamente grandes y difíciles de manejar.
Por esta razón, incluso si no utiliza diferentes modelos de lectura/escritura, seguir CQRS puede hacer que su código sea mucho más limpio y más fácil de mantener.
Otro beneficio de usar CQRS es que la única dependencia que su código ahora necesita es la instancia de Mediator. El Mediador es responsable de enviar sus Comandos y Consultas a su Manejador correspondiente.
Antes de entrar en detalles, veamos la diferencia entre los dos enfoques al acceder a datos a través de un controlador API.
Implementación de CQRS
Primero veremos cómo se implementa el lado de escritura usando comandos a través de Entity Framework Core y luego veremos cómo se implementa el lado de lectura usando consultas a través de Dapper.
Commands
Los comandos se utilizan para crear/actualizar/eliminar datos utilizando nuestros modelos de escritura. En este ejemplo, se sigue el diseño basado en dominio para que podamos encapsular tanta lógica comercial y de validación como sea posible dentro de nuestros agregados/entidades. Cuando me refiero a escribir modelos aquí, me refiero a nuestras entidades que se almacenan en la base de datos a través de Entity Framework.
Todos los comandos implementan la clase base Command y sus controladores implementan la clase base CommandHandler. Estas clases base heredan de las interfaces MediatR IRequest e IRequestHandler, lo que permite enviarlas a través de un ISender.
Command y Command Handler Base Classes
El uso de records para crear los comandos funciona bien porque deberían ser estructuras de datos simples e inmutables. Los controladores de comandos no devuelven nada en absoluto; este debería ser generalmente el caso cuando se sigue CQRS.
Ejemplo de Comand
El uso de Mediator para enviar Comands y Queries rompe las dependencias al navegar por el código. Eso puede dificultar la búsqueda rápida de implementaciones de Handler al observar el código que consume. Por esta razón suelo poner las clases Handler en el mismo archivo que el Comando o la Consulta.
A continuación, se muestra un ejemplo de comando para crear un nuevo pronóstico del tiempo. En este caso, la clase WeatherForecast es nuestro modelo de escritura.
El modelo de escritura de WeatherForecast utiliza Domain-Driven Design basado en dominio para encapsular la lógica de validación/actualización mediante configuradores privados y guardias.
El lado de escritura utiliza una implementación genérica de Entity Framework Core Repository. EF se encargará de toda la complejidad del almacenamiento de nuestros datos. Dado que nuestras escrituras son menos frecuentes, no nos preocupa tanto el rendimiento.
Devolver datos de comandos
Si sigue CQRS "puro", sus comandos no deben devolver datos.
Hay casos en los que se pueden devolver validaciones o metadatos; sin embargo, las respuestas no deben incluir ninguna información sobre el modelo de escritura. Una de las razones de esto es para que sus Comandos puedan manejarse de forma asincrónica en el futuro, de manera que pueda activarse y olvidarse si es necesario; en nuestro ejemplo, estamos implementando un Mediador en proceso, por lo que este no será el caso.
En el ejemplo anterior, se pasa un nuevo ID de Guid al comando para que no sea necesario devolverlo nuevamente. Si está utilizando un identificador generado por una base de datos, este principio puede resultar difícil de cumplir. Recomiendo ser pragmático aquí: si realmente crees que necesitas devolver datos de tus Comandos y funciona para tu equipo, ¡hazlo!
Consultas
Las consultas se utilizan para leer datos. Las clases base Query y QueryHandler también heredan de las interfaces MediatR para que puedan enviarse a través de un ISender. La diferencia clave es que las consultas deben especificar un tipo de respuesta: este es el modelo de lectura que sabe cómo consultar.
Ejemplo de consulta
Aquí hay un ejemplo de consulta para leer un pronóstico del tiempo. En este caso, la clase WeatherForecastReadModel es nuestro modelo de lectura.
Nuestro modelo Query Handler y Read son súper simples y livianos. No debe haber ninguna lógica o mapeo complejos para que los modelos de lectura puedan leerse y serializarse de la manera más eficiente posible
.La principal diferencia en el lado de lectura es la implementación del repositorio.
Se utiliza un WeatherForecastsReadModelRepository personalizado para leer datos usando Dapper.
Esto significa que podemos diseñar nuestras consultas SQL exactamente como queremos optimizar el rendimiento, sin que Entity Framework se interponga en el camino.
Aquí estamos leyendo la misma tabla que Entity Framework usó para los modelos de escritura; sin embargo, podríamos usar otras opciones si quisiéramos, por ejemplo: SQL Views.