CQRS / MediatR en vaut-il la peine lors du développement d'une application ASP.NET?

17

Je me suis penché récemment sur CQRS / MediatR. Mais plus j'explore moins je l'aime. J'ai peut-être mal compris quelque chose / tout.

Cela commence donc génial en prétendant réduire votre contrôleur à ce

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

    return View(model);
}

Ce qui correspond parfaitement à la ligne directrice du contrôleur mince. Cependant, il omet certains détails assez importants - la gestion des erreurs.

Regardons l' Loginaction par défaut d'un nouveau projet 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);
}

La conversion qui nous présente un tas de problèmes du monde réel. N'oubliez pas que l'objectif est de le réduire à

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

    return View(model);
}

Une solution possible à cela consiste à renvoyer un CommandResult<T>au lieu d'un model, puis à gérer le CommandResultfiltre dans un post-action. Comme discuté ici .

Une implémentation du CommandResultpourrait être comme ceci

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

la source

Cependant, cela ne résout pas vraiment notre problème dans l' Loginaction, car il existe plusieurs états de défaillance. Nous pourrions ajouter ces états d'échec supplémentaires, ICommandResultmais c'est un bon début pour une classe / interface très gonflée. On pourrait dire qu'il n'est pas conforme à la responsabilité unique (SRP).

Un autre problème est le returnUrl. Nous avons ce return RedirectToLocal(returnUrl);morceau de code. D'une manière ou d'une autre, nous devons gérer les arguments conditionnels basés sur l'état de réussite de la commande. Bien que je pense que cela pourrait être fait (je ne sais pas si le ModelBinder peut mapper les arguments FromBody et FromQuery ( returnUrlest FromQuery) à un seul modèle). On ne peut que se demander quel genre de scénarios fous pourraient se produire sur la route.

La validation des modèles est également devenue plus complexe avec le renvoi de messages d'erreur. Prenez cela comme exemple

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

Nous joignons un message d'erreur avec le modèle. Ce genre de chose ne peut pas être fait en utilisant une Exceptionstratégie (comme suggéré ici ) parce que nous avons besoin du modèle. Vous pouvez peut-être obtenir le modèle de la Requestmais ce serait un processus très complexe.

Donc, dans l'ensemble, j'ai du mal à convertir cette action "simple".

Je cherche des entrées. Suis-je totalement dans l'erreur ici?

Snæbjørn
la source
6
On dirait que vous comprenez déjà assez bien les préoccupations pertinentes. Il y a beaucoup de "balles d'argent" qui ont des exemples de jouets qui prouvent leur utilité, mais qui tombent inévitablement lorsqu'elles sont pressées par la réalité d'une application réelle et réelle.
Robert Harvey
Découvrez les comportements MediatR. Il s'agit essentiellement d'un pipeline qui vous permet de répondre aux préoccupations transversales.
fml

Réponses:

14

Je pense que vous attendez trop du modèle que vous utilisez. CQRS est spécialement conçu pour traiter la différence de modèle entre les requêtes et les commandes de la base de données , et MediatR est juste une bibliothèque de messagerie en cours. Le CQRS ne prétend pas éliminer le besoin d'une logique métier comme vous vous y attendez. CQRS est un modèle d'accès aux données, mais vos problèmes sont liés à la couche de présentation - redirection, vues, contrôleurs.

Je pense que vous pouvez mal appliquer le modèle CQRS à l'authentification. Avec la connexion, il ne peut pas être modélisé comme une commande dans CQRS car

Commandes: modifiez l'état d'un système mais ne renvoyez pas de valeur
- Martin Fowler CommandQuerySeparation

À mon avis, l'authentification est un domaine médiocre pour CQRS. Avec l'authentification, vous avez besoin d'un flux de demande-réponse synchrone fortement cohérent afin que vous puissiez 1. vérifier les informations d'identification de l'utilisateur 2. créer une session pour l'utilisateur 3. gérer n'importe quelle variété de cas marginaux que vous avez identifiés 4. accorder ou refuser immédiatement l'utilisateur en réponse.

CQRS / MediatR en vaut-il la peine lors du développement d'une application ASP.NET?

CQRS est un modèle qui a des utilisations très spécifiques. Son but est de modéliser les requêtes et les commandes au lieu d'avoir un modèle pour les enregistrements tel qu'utilisé dans CRUD. À mesure que les systèmes deviennent plus complexes, les demandes de vues sont souvent plus complexes que de simplement montrer un seul enregistrement ou une poignée d'enregistrements, et une requête peut mieux modéliser les besoins de l'application. De même, les commandes peuvent représenter des modifications apportées à de nombreux enregistrements au lieu de CRUD dont vous modifiez des enregistrements uniques. Martin Fowler met en garde

Comme tout modèle, le CQRS est utile à certains endroits, mais pas à d'autres. De nombreux systèmes correspondent à un modèle mental CRUD, et devraient donc être effectués dans ce style. Le CQRS est un saut mental important pour toutes les parties concernées, donc ne devrait pas être abordé à moins que l'avantage en vaille la peine. Bien que j'aie rencontré des utilisations réussies du CQRS, jusqu'à présent, la majorité des cas que j'ai rencontrés n'étaient pas aussi bons, le CQRS étant considéré comme une force importante pour mettre un système logiciel en difficulté.
- Martin Fowler CQRS

Donc, pour répondre à votre question, le CQRS ne devrait pas être le premier recours lors de la conception d'une application lorsque CRUD convient. Rien dans votre question ne m'a donné d'indication que vous avez une raison d'utiliser le CQRS.

Quant à MediatR, c'est une bibliothèque de messagerie en cours, elle vise à dissocier les requêtes du traitement des requêtes. Vous devez à nouveau décider si cela améliorera votre conception pour utiliser cette bibliothèque. Personnellement, je ne suis pas un partisan de la messagerie en cours. Le couplage lâche peut être réalisé de manière plus simple que la messagerie, et je vous recommande de commencer par là.

Samuel
la source
1
Je suis d'accord à 100%. Le CQRS est juste un peu excité, alors j'ai pensé qu'ils "ont" vu quelque chose que je n'ai pas vu. Parce que j'ai du mal à voir les avantages du CQRS dans les applications Web CRUD. Jusqu'à présent, le seul scénario est CQRS + ES qui a du sens pour moi.
Snæbjørn
Un gars de mon nouveau travail a décidé de mettre MediatR sur un nouveau système ASP.Net en le revendiquant comme une architecture. L'implémentation qu'il a faite n'est ni DDD, ni SOLID, ni DRY, ni KISS. C'est un petit système plein de YAGNI. Et cela a commencé longtemps après quelques commentaires comme le vôtre, le vôtre inclus. J'essaie de comprendre comment je peux refacturer le code pour adapter progressivement son architecture. J'avais la même opinion sur le CQRS en dehors d'une couche métier et je suis heureux que plusieurs développeurs expérimentés pensent de cette façon.
MFedatto
Il est un peu ironique d'affirmer que l'idée d'incorporer CQRS / MediatR pourrait être associée à beaucoup de YAGNI et à un manque de KISS, alors qu'en fait certaines des alternatives populaires, comme le modèle de référentiel, favorisent YAGNI en gonflant la classe de référentiel et en forçant interfaces pour spécifier un grand nombre d'opérations CRUD sur tous les agrégats racine qui souhaitent implémenter de telles interfaces, laissant souvent ces méthodes inutilisées ou remplies d'exceptions "non implémentées". Le CQRS n'utilisant pas ces généralisations, il ne peut implémenter que ce qui est nécessaire.
Lesair Valmont
@LesairValmont Repository est uniquement censé être CRUD. "spécifier un grand nombre d'opérations CRUD" ne devrait être que 4 (ou 5 avec "liste"). Si vous avez des modèles d'accès aux requêtes plus spécifiques, ils ne devraient pas se trouver dans votre interface de référentiel. Je n'ai jamais rencontré de problème de méthodes de référentiel inutilisées. Pouvez-vous donner un exemple?
Samuel
@Samuel: Je pense que le modèle de référentiel est parfaitement adapté à certains scénarios, tout comme le CQRS. En fait, sur une grande application, il y aura certaines parties dont le meilleur ajustement sera le modèle de référentiel et d'autres qui bénéficieraient davantage du CQRS. Cela dépend de nombreux facteurs différents, comme la philosophie suivie sur cette partie de l'application (par exemple, basée sur les tâches (CQRS) vs CRUD (repo)), l'ORM utilisé (le cas échéant), la modélisation du domaine ( par exemple DDD). Pour les catalogues CRUD simples, CQRS est définitivement exagéré, et certaines fonctionnalités collaboratives en temps réel (comme un chat) n'utiliseraient pas non plus.
Lesair Valmont
10

Le CQRS est plus une chose de gestion des données plutôt que de ne pas avoir tendance à saigner trop fortement dans une couche d'application (ou domaine si vous préférez, car il a tendance à être le plus souvent utilisé dans les systèmes DDD). Votre application MVC, d'autre part, est une application de couche de présentation et doit être assez bien séparée du noyau de requête / persistance du CQRS.

Une autre chose à noter (compte tenu de votre comparaison de la Loginméthode par défaut et du désir de contrôleurs légers): je ne suivrais pas exactement les modèles / code passe-partout ASP.NET par défaut comme étant quelque chose dont nous devrions nous soucier pour les meilleures pratiques.

J'aime aussi les contrôleurs minces, car ils sont très faciles à lire. Chaque contrôleur que j'ai possède généralement un objet "service" qu'il associe et qui gère essentiellement la logique requise par le contrôleur:

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");
    }
}

Encore assez mince, mais nous n'avons pas vraiment changé le fonctionnement du code, déléguez simplement la gestion à la méthode de service, qui ne sert vraiment à rien d'autre que de rendre les actions du contrôleur faciles à digérer.

Gardez à l'esprit que cette classe de service est toujours responsable de la délégation de la logique au modèle / à l'application selon les besoins, c'est vraiment juste une légère extension du contrôleur pour garder le code propre. Les méthodes de service sont généralement assez courtes également.

Je ne suis pas sûr que le médiateur ferait quelque chose de conceptuellement différent de cela: déplacer une logique de contrôleur de base hors du contrôleur et vers un autre endroit à traiter.

(Je n'avais jamais entendu parler de ce MediatR auparavant, et un rapide coup d'œil à la page github ne semble pas indiquer que c'est quelque chose de révolutionnaire - certainement pas quelque chose comme CQRS - en fait, il semble être quelque chose comme juste une autre couche d'abstraction que vous peut mettre pour compliquer le code en le rendant plus simple, mais ce n'est que ma première prise)

jleach
la source
5

Je vous recommande fortement de consulter la présentation NDC de Jimmy Bogard sur son approche de la modélisation des requêtes http https://www.youtube.com/watch?v=SUiWfhAhgQw

Vous aurez alors une idée claire de l'utilisation de Mediatr.

Jimmy n'a pas une adhésion aveugle aux motifs et aux abstractions. Il est très pragmatique. Mediatr nettoie les actions du contrôleur. En ce qui concerne la gestion des exceptions, je pousse cela dans une classe parent appelée quelque chose comme Execute. Vous vous retrouvez donc avec une action de contrôleur très propre.

Quelque chose comme:

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;
}

L'utilisation ressemble un peu à ceci:

[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);
}

J'espère que cela pourra aider.

DavidRogersDev
la source
4

Beaucoup de gens (je l'ai fait aussi) confondent le motif avec une bibliothèque. CQRS est un modèle, mais MediatR est une bibliothèque que vous pouvez utiliser pour implémenter ce modèle

Vous pouvez utiliser CQRS sans MediatR ou toute bibliothèque de messagerie en cours et vous pouvez utiliser MediatR sans CQRS:

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

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

CQS ressemblerait à ceci:

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

En fait, vous n'avez pas à nommer vos modèles d'entrée "Commandes" comme ci-dessus CreateProductCommand. Et saisie de vos requêtes "Requêtes". Les commandes et les requêtes sont des méthodes, pas des modèles.

Le CQRS concerne la séparation des responsabilités (les méthodes de lecture doivent être dans un endroit séparé des méthodes d'écriture - isolées). C'est une extension de CQS mais la différence est qu'en CQS vous pouvez mettre ces méthodes dans 1 classe. (pas de séparation des responsabilités, juste une séparation commande-requête). Voir séparation vs ségrégation

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

Au cœur se trouve la notion que vous pouvez utiliser un modèle différent pour mettre à jour les informations que le modèle que vous utilisez pour lire les informations.

Il y a de la confusion dans ce qu'il dit, il ne s'agit pas d'avoir un modèle distinct pour les entrées et les sorties, il s'agit de la séparation des responsabilités.

CQRS et limitation de génération d'ID

Il y a une limitation à laquelle vous serez confronté lors de l'utilisation de CQRS ou CQS

Techniquement, dans les descriptions originales, les commandes ne devraient renvoyer aucune valeur (void) que je trouve stupide car il n'y a pas de moyen facile d'obtenir l'ID généré à partir d'un objet nouvellement créé: /programming/4361889/how-to- get-id-in-create-when-apply-cqrs .

vous devez donc générer un identifiant à chaque fois au lieu de laisser la base de données le faire.


Si vous voulez en savoir plus: https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf

Konrad
la source
1
Je conteste votre affirmation selon laquelle une commande CQRS pour la persistance de nouvelles données dans une base de données ne pouvant pas renvoyer un identifiant nouvellement généré est "stupide". Je pense plutôt que c'est une question philosophique. Rappelez-vous qu'une grande partie de DDD et CQRS concerne l'immuabilité des données. Lorsque vous y réfléchissez à deux fois, vous commencez à réaliser que le simple fait de conserver des données est une opération de mutation des données. Et il ne s'agit pas seulement de nouveaux identifiants, mais il peut également s'agir de champs remplis de données par défaut, de déclencheurs et de processus stockés qui pourraient également modifier vos données.
Lesair Valmont
Bien sûr, vous pouvez envoyer une sorte d'événement comme "ItemCreated" avec un nouvel élément comme argument. Si vous traitez simplement avec le protocole de demande-réponse et que vous utilisez le "vrai" CQRS, l'identifiant doit être connu à l'avance pour que vous puissiez le transmettre à une fonction de requête distincte - absolument rien de mal à cela. Dans de nombreux cas, le CQRS est simplement exagéré. Vous pouvez vous en passer. Ce n'est rien d'autre qu'un moyen de structurer votre code et cela dépend principalement des protocoles que vous utilisez également.
Konrad
Et vous pouvez atteindre l'immuabilité des données sans CQRS
Konrad