¿Vale la pena CQRS / MediatR al desarrollar una aplicación ASP.NET?

17

He estado buscando en CQRS / MediatR últimamente. Pero cuanto más profundizo, menos me gusta. Quizás he entendido mal algo / todo.

Por lo tanto, comienza increíble al afirmar que reduce su controlador a esto

public async Task<ActionResult> Edit(Edit.Query query)
{
    var model = await _mediator.SendAsync(query);

    return View(model);
}

Que encaja perfectamente con la guía del controlador delgado. Sin embargo, deja de lado algunos detalles bastante importantes: manejo de errores.

Veamos la Loginacción predeterminada de un nuevo proyecto MVC

public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
    ViewData["ReturnUrl"] = returnUrl;
    if (ModelState.IsValid)
    {
        // This doesn't count login failures towards account lockout
        // To enable password failures to trigger account lockout, set lockoutOnFailure: true
        var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
        if (result.Succeeded)
        {
            _logger.LogInformation(1, "User logged in.");
            return RedirectToLocal(returnUrl);
        }
        if (result.RequiresTwoFactor)
        {
            return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
        }
        if (result.IsLockedOut)
        {
            _logger.LogWarning(2, "User account locked out.");
            return View("Lockout");
        }
        else
        {
            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            return View(model);
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

Conversión que nos presenta un montón de problemas del mundo real. Recuerde que el objetivo es reducirlo a

public async Task<IActionResult> Login(Login.Command command, string returnUrl = null)
{
    var model = await _mediator.SendAsync(command);

    return View(model);
}

Una posible solución a esto es devolver un en CommandResult<T>lugar de a modely luego manejarlo CommandResulten un filtro posterior a la acción. Como se discutió aquí .

Una implementación de la CommandResultpodría ser así

public interface ICommandResult  
{
    bool IsSuccess { get; }
    bool IsFailure { get; }
    object Result { get; set; }
}

fuente

Sin embargo, eso realmente no resuelve nuestro problema en la Loginacción, porque hay múltiples estados de falla. Podríamos agregar estos estados de falla adicionales, ICommandResultpero ese es un gran comienzo para una clase / interfaz muy hinchada. Se podría decir que no cumple con la responsabilidad única (SRP).

Otro problema es el returnUrl. Tenemos este return RedirectToLocal(returnUrl);pedazo de código. De alguna manera, necesitamos manejar argumentos condicionales basados ​​en el estado de éxito del comando. Si bien creo que podría hacerse (no estoy seguro de si ModelBinder puede asignar argumentos FromBody y FromQuery ( returnUrles FromQuery) a un solo modelo). Uno solo puede preguntarse qué tipo de escenarios locos podrían venir en el futuro.

La validación del modelo también se ha vuelto más compleja junto con la devolución de mensajes de error. Toma esto como un ejemplo

else
{
    ModelState.AddModelError(string.Empty, "Invalid login attempt.");
    return View(model);
}

Adjuntamos un mensaje de error junto con el modelo. Este tipo de cosas no se pueden hacer usando una Exceptionestrategia (como se sugiere aquí ) porque necesitamos el modelo. Quizás pueda obtener el modelo del, Requestpero sería un proceso muy complicado.

Así que, en general, me está costando convertir esta acción "simple".

Estoy buscando entradas. ¿Estoy totalmente equivocado aquí?

Snæbjørn
fuente
66
Parece que ya entiendes bastante bien las preocupaciones relevantes. Hay muchas "balas de plata" que tienen ejemplos de juguetes que demuestran su utilidad, pero que inevitablemente se caen cuando se ven presionados por la realidad de una aplicación real de la vida real.
Robert Harvey
Echa un vistazo a los comportamientos de MediatR. Básicamente es una tubería que le permite abordar preocupaciones transversales.
fml

Respuestas:

14

Creo que esperas demasiado del patrón que estás usando. CQRS está diseñado específicamente para abordar la diferencia de modelo entre la consulta y los comandos de la base de datos , y MediatR es solo una biblioteca de mensajería en proceso. CQRS no pretende eliminar la necesidad de una lógica empresarial como espera que lo hagan. CQRS es un patrón para el acceso a datos, pero sus problemas son con la capa de presentación: redireccionamientos, vistas, controladores.

Creo que puede estar aplicando mal el patrón CQRS a la autenticación. Con el inicio de sesión, no se puede modelar como un comando en CQRS porque

Comandos: cambiar el estado de un sistema pero no devolver un valor
- Martin Fowler CommandQuerySeparation

En mi opinión, la autenticación es un dominio deficiente para CQRS. Con la autenticación, necesita un flujo de solicitud-respuesta síncrono muy consistente para que pueda 1. verificar las credenciales del usuario 2. crear una sesión para el usuario 3. manejar cualquiera de la variedad de casos extremos que ha identificado 4. otorgar o denegar inmediatamente al usuario en respuesta.

¿Vale la pena CQRS / MediatR al desarrollar una aplicación ASP.NET?

CQRS es un patrón que tiene usos muy específicos. Su propósito es modelar consultas y comandos en lugar de tener un modelo para registros como se usa en CRUD. A medida que los sistemas se vuelven más complejos, las demandas de vistas a menudo son más complejas que solo mostrar un solo registro o un puñado de registros, y una consulta puede modelar mejor las necesidades de la aplicación. Del mismo modo, los comandos pueden representar cambios en muchos registros en lugar de CRUD, que cambia los registros individuales. Martin Fowler advierte

Como cualquier patrón, CQRS es útil en algunos lugares, pero no en otros. Muchos sistemas se ajustan a un modelo mental CRUD, por lo que deberían hacerse en ese estilo. CQRS es un salto mental significativo para todos los interesados, por lo que no debe abordarse a menos que el beneficio valga la pena. Si bien he encontrado usos exitosos de CQRS, hasta ahora la mayoría de los casos con los que me he encontrado no han sido tan buenos, con CQRS visto como una fuerza significativa para hacer que un sistema de software tenga serias dificultades.
- Martin Fowler CQRS

Entonces, para responder a su pregunta, CQRS no debería ser el primer recurso al diseñar una aplicación cuando CRUD es adecuado. Nada en su pregunta me dio la indicación de que tiene una razón para usar CQRS.

En cuanto a MediatR, es una biblioteca de mensajería en proceso, tiene como objetivo desacoplar las solicitudes del manejo de solicitudes. Debe decidir nuevamente si mejorará su diseño para usar esta biblioteca. Personalmente no soy un defensor de la mensajería en proceso. El acoplamiento suelto se puede lograr de formas más simples que la mensajería, y le recomendaría que comience allí.

Samuel
fuente
1
Estoy 100% de acuerdo. CQRS es un poco exagerado, así que pensé que "ellos" vieron algo que yo no. Porque me cuesta ver los beneficios de CQRS en las aplicaciones web CRUD. Hasta ahora, el único escenario es CQRS + ES que tiene sentido para mí.
Snæbjørn
Un tipo en mi nuevo trabajo decidió poner MediatR en el nuevo sistema ASP.Net alegando que era una arquitectura. La implementación que realizó no es DDD, ni SÓLIDA, ni SECA, ni BESO. Es un pequeño sistema lleno de YAGNI. Y ha comenzado mucho después de algunos comentarios como el suyo, incluido el suyo. Estoy tratando de imaginar cómo puedo modificar el código para adaptar su arquitectura gradualmente. Tenía la misma opinión sobre CQRS fuera de una capa empresarial y me alegra que haya varios desarrolladores experimentados que piensen de esa manera.
MFedatto
Es un poco irónico afirmar que la idea de incorporar CQRS / MediatR podría estar asociada con una gran cantidad de YAGNI y una falta de KISS, cuando en realidad algunas de las alternativas populares, como el patrón Repository, promueven YAGNI al hinchar la clase de repositorio y forzar interfaces para especificar muchas operaciones CRUD en todos los agregados raíz que desean implementar tales interfaces, dejando a menudo esos métodos sin usar o llenos de excepciones "no implementadas". Debido a que CQRS no usa estas generalizaciones, solo puede implementar lo que se necesita.
Lesair Valmont
@LesairValmont Repository solo se supone que es CRUDO. "especificar muchas operaciones CRUD" solo debe ser 4 (o 5 con "lista"). Si tiene patrones de acceso a consultas más específicos, no deberían estar en su interfaz de repositorio. Nunca me he encontrado con un problema de métodos de repositorio no utilizados. ¿Puede dar un ejemplo?
Samuel
@Samuel: Creo que el patrón de repositorio está perfectamente bien para ciertos escenarios, al igual que CQRS. En realidad, en una aplicación grande, habrá algunas partes cuyo mejor ajuste será el patrón de repositorio y otras que serían más beneficiadas por CQRS. Depende de muchos factores diferentes, como la filosofía seguida en esa parte de la aplicación (por ejemplo, basada en tareas (CQRS) frente a CRUD (repo)), el ORM que se usa (si lo hay), el modelado del dominio ( por ejemplo, DDD). Para catálogos CRUD simples, CQRS es definitivamente excesivo, y algunas funciones de colaboración en tiempo real (como un chat) tampoco lo usarían.
Lesair Valmont
10

CQRS es más una cuestión de gestión de datos y no tiende a sangrar demasiado en una capa de aplicación (o Dominio si lo prefiere, ya que tiende a usarse con mayor frecuencia en los sistemas DDD). Su aplicación MVC, por otro lado, es una aplicación de capa de presentación y debe estar bastante bien separada del núcleo de consulta / persistencia del CQRS.

Otra cosa que vale la pena señalar (dada su comparación del Loginmétodo predeterminado y el deseo de controladores delgados): no seguiría exactamente las plantillas / código estándar repetitivo de ASP.NET como algo por lo que deberíamos preocuparnos por las mejores prácticas.

También me gustan los controladores delgados, porque son muy fáciles de leer. Cada controlador que tengo generalmente tiene un objeto de "servicio" con el que se empareja y que esencialmente maneja la lógica requerida por el controlador:

public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null) {

    var result = _service.Login(model);
    switch (result) {
        case result.lockout: return View("Lockout");
        case result.ok: return RedirectToLocal(returnUrl);
        default: return View("GeneralError");
    }
}

Todavía lo suficientemente delgado, pero no hemos cambiado realmente cómo funciona el código, solo delegue el manejo al método de servicio, que realmente no tiene otro propósito que hacer que las acciones del controlador sean fáciles de digerir.

Tenga en cuenta que esta clase de servicio sigue siendo responsable de delegar la lógica al modelo / aplicación según sea necesario, en realidad es solo una ligera extensión del controlador para mantener el código ordenado. Los métodos de servicio son generalmente bastante cortos también.

No estoy seguro de que el mediador esté haciendo algo conceptualmente diferente a eso: mover alguna lógica básica del controlador fuera del controlador y en otro lugar para ser procesado.

(No había oído hablar de este MediatR antes, y un vistazo rápido a la página de github no parece indicar que sea algo innovador, ciertamente no algo como CQRS, de hecho, parece ser algo más que otra capa de abstracción que usted puede complicar el código haciendo que parezca más simple, pero esa es solo mi primera toma)

jleach
fuente
5

Le recomiendo que vea la presentación NDC de Jimmy Bogard sobre su enfoque para modelar solicitudes http https://www.youtube.com/watch?v=SUiWfhAhgQw

Entonces obtendrá una idea clara de para qué se utiliza Mediatr.

Jimmy no tiene una adhesión ciega a los patrones y abstracciones. El es muy pragmático. Mediatr limpia las acciones del controlador. En cuanto al manejo de excepciones, inserto eso en una clase principal llamada algo como Ejecutar. Entonces terminas con una acción de controlador muy limpia.

Algo como:

public bool Execute<T>(Func<T> messageFunction)
{
    try
    {
        messageFunction();

        return true;
    }
    catch (ValidationException exception)
    {
        Errors = string.Join(Environment.NewLine, exception.Errors.Select(e => e.ErrorMessage));
        Logger.LogException(exception, "ValidationException caught in SiteController");
    }
    catch (SiteException exception)
    {
        Errors = exception.Message;
        Logger.LogException(exception);
    }
    catch (DbEntityValidationException dbEntityValidationException)
    {
        // Retrieve the error messages as a list of strings.
        var errorMessages = dbEntityValidationException.EntityValidationErrors
                .SelectMany(x => x.ValidationErrors)
                .Select(x => x.ErrorMessage);

        // Join the list to a single string.
        var fullErrorMessage = string.Join("; ", errorMessages);

        // Combine the original exception message with the new one.
        var exceptionMessage = string.Concat(dbEntityValidationException.Message, " The validation errors are: ", fullErrorMessage);

        Logger.LogError(exceptionMessage);

        // Throw a new DbEntityValidationException with the improved exception message.
        throw new DbEntityValidationException(exceptionMessage, dbEntityValidationException.EntityValidationErrors);                
    }
    catch (Exception exception)
    {
        Errors = "An error has occurred.";
        Logger.LogException(exception, "Exception caught in SiteController.");
    }

    // used to indicate that any transaction which may be in progress needs to be rolled back for this request.
    HttpContext.Items[UiConstants.Error] = true;

    Response.StatusCode = (int)HttpStatusCode.InternalServerError; // fail

    return false;
}

El uso se parece un poco a esto:

[Route("api/licence")]
public IHttpActionResult Post(LicenceEditModel licenceEditModel)
{
    var updateLicenceCommand = new UpdateLicenceCommand { LicenceEditModel = licenceEditModel };
    int licenceId = -1;

    if (Execute(() => _mediator.Send(updateLicenceCommand)))
    {
        return JsonSuccess(licenceEditModel);
    }

    return JsonError(Errors);
}

Espero que ayude.

DavidRogersDev
fuente
4

Mucha gente (yo también lo hice) confunde el patrón con una biblioteca. CQRS es un patrón, pero MediatR es una biblioteca que puede usar para implementar ese patrón

Puede usar CQRS sin MediatR o cualquier biblioteca de mensajería en proceso y puede usar MediatR sin CQRS:

public interface IProductsWriteService
{
    void CreateProduct(CreateProductCommand createProductCommand);
}

public interface IProductsReadService
{
    ProductDto QueryProduct(Guid guid);
}

CQS se vería así:

public interface IProductsService
{
    void CreateProduct(CreateProductCommand createProductCommand);
    ProductDto QueryProduct(Guid guid);
}

De hecho, no tiene que nombrar a sus modelos de entrada "Comandos" como se indica arriba CreateProductCommand. Y entrada de sus consultas "Consultas". Los comandos y las consultas son métodos, no modelos.

CQRS trata sobre la segregación de responsabilidad (los métodos de lectura deben estar en un lugar separado de los métodos de escritura, aislados). Es una extensión de CQS, pero la diferencia está en CQS, puede colocar estos métodos en 1 clase. (sin segregación de responsabilidad, solo separación de comando-consulta). Ver separación vs segregación

Desde https://martinfowler.com/bliki/CQRS.html :

En el fondo está la noción de que puede usar un modelo diferente para actualizar la información que el modelo que usa para leer la información.

Hay confusión en lo que dice, no se trata de tener un modelo separado para entrada y salida, se trata de la separación de responsabilidades.

CQRS y limitación de generación de identificación

Hay una limitación que enfrentará al usar CQRS o CQS

Técnicamente, en la descripción original, los comandos no deberían devolver ningún valor (nulo) que me parece estúpido porque no hay una manera fácil de obtener la identificación generada de un objeto recién creado: /programming/4361889/how-to- get-id-in-create-when-apply-cqrs .

por lo que debe generar una identificación cada vez, en lugar de dejar que la base de datos lo haga.


Si desea obtener más información: https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf

Konrad
fuente
1
Desafío su afirmación de que un comando de CQRS para persistir nuevos datos en una base de datos que no puede devolver un nuevo ID generado por la base de datos es "estúpido". Prefiero pensar que este es un asunto filosófico. Recuerde que gran parte de DDD y CQRS se trata de la inmutabilidad de datos. Cuando lo piensa dos veces, comienza a darse cuenta de que el simple acto de persistir los datos es una operación de mutación de datos. Y no se trata solo de nuevas ID, sino que también podría ser campos llenos de datos predeterminados, disparadores y procesos almacenados que también podrían alterar sus datos.
Lesair Valmont
Claro que puede enviar algún tipo de evento como "ItemCreated" con un nuevo elemento como argumento. Si se trata simplemente de un protocolo de solicitud-respuesta y está usando CQRS "verdadero", entonces la identificación debe conocerse por adelantado para que pueda pasarla a una función de consulta separada, absolutamente nada de malo en eso. En muchos casos, CQRS es simplemente exagerado. Puedes vivir sin eso. No es más que una forma de estructurar su código y eso depende principalmente de los protocolos que use también.
Konrad
Y puede lograr la inmutabilidad de datos sin CQRS
Konrad