web-dev-qa-db-fra.com

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

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 laisse de côté des détails assez importants - la gestion des erreurs.

Regardons l'action Login 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 CommandResult dans un filtre de post-action. Comme discuté ici .

Une implémentation de CommandResult pourrait ressembler à ceci

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

source

Cependant, cela ne résout pas vraiment notre problème dans l'action Login, car il existe plusieurs états d'échec. Nous pourrions ajouter ces états d'échec supplémentaires à ICommandResult mais c'est un excellent 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 cette return RedirectToLocal(returnUrl); morceau de code. D'une manière ou d'une autre, nous devons gérer les arguments conditionnels en fonction de 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 (returnUrl est 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 stratégie Exception (comme suggéré ici ) parce que nous avons besoin du modèle. Vous pouvez peut-être obtenir le modèle à partir de Request mais 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?

19
Snæbjørn

Je pense que vous attendez trop du modèle que vous utilisez. CQRS est spécifiquement conçu pour adresser la différence de modèle entre la requête et les commandes à 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 avec couche de présentation - redirections, vues, contrôleurs.

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

Commandes: modifiez l'état d'un système mais ne retournez 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 Edge 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à.

15
Samuel

CQRS est plus une chose de gestion de données plutôt que de ne pas tendre 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 méthode par défaut Login et de votre 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 dans 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)

10
jleach

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.

5
DavidRogersDev

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 mettre en œuvre 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

De 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 devrez faire face 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éé: https://stackoverflow.com/questions/4361889/ comment-obtenir-id-en-créer-lors-application-cqrs .

vous devez donc générer un identifiant à chaque fois vous-même 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

4
Konrad