Vale la pena CQRS / MediatR quando si sviluppa un'applicazione ASP.NET?


17

Ultimamente ho esaminato CQRS / MediatR. Ma più approfondisco, meno mi piace. Forse ho frainteso qualcosa / tutto.

Quindi è fantastico affermando di ridurre il controller a questo

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

    return View(model);
}

Che si adatta perfettamente alla linea guida del controller sottile. Tuttavia lascia fuori alcuni dettagli piuttosto importanti: la gestione degli errori.

Vediamo l' Loginazione predefinita di un nuovo progetto 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 conversione ci presenta un sacco di problemi nel mondo reale. Ricorda che l'obiettivo è ridurlo a

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

    return View(model);
}

Una possibile soluzione a questo è quella di restituire un CommandResult<T>anziché un modele quindi gestire il CommandResultfiltro in un post azione. Come discusso qui .

Un'implementazione del CommandResultpotrebbe essere così

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

fonte

Tuttavia, ciò non risolve realmente il nostro problema Loginnell'azione, poiché esistono più stati di errore. Potremmo aggiungere questi stati di errore extra a, ICommandResultma questo è un ottimo inizio per una classe / interfaccia molto gonfia. Si potrebbe dire che non è conforme alla responsabilità singola (SRP).

Un altro problema è il returnUrl. Abbiamo questo return RedirectToLocal(returnUrl);pezzo di codice. In qualche modo dobbiamo gestire gli argomenti condizionali basati sullo stato di successo del comando. Mentre penso che potrebbe essere fatto (non sono sicuro se ModelBinder può mappare gli argomenti FromBody e FromQuery ( returnUrlè FromQuery) su un singolo modello). Ci si può solo chiedere che tipo di scenari folli potrebbero venire giù per la strada.

Anche la convalida del modello è diventata più complessa insieme alla restituzione di messaggi di errore. Prendi questo come esempio

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

Alleghiamo un messaggio di errore insieme al modello. Questo genere di cose non può essere fatto usando una Exceptionstrategia (come suggerito qui ) perché abbiamo bisogno del modello. Forse puoi ottenere il modello dal Requestma sarebbe un processo molto complicato.

Quindi, tutto sommato, faccio fatica a convertire questa "semplice" azione.

Sto cercando input. Sono totalmente nel torto qui?


6
Sembra che tu abbia già capito abbastanza bene le preoccupazioni rilevanti. Ci sono molti "proiettili d'argento" là fuori che hanno esempi di giocattoli che dimostrano la loro utilità, ma che inevitabilmente cadono quando vengono schiacciati dalla realtà di un'applicazione reale, nella vita reale.
Robert Harvey,

Dai un'occhiata ai comportamenti di MediatR. È fondamentalmente una pipeline che ti consente di affrontare i problemi trasversali.
fml,

Risposte:


14

Penso che ti aspetti troppo dal modello che stai usando. CQRS è progettato specificamente per affrontare la differenza di modello tra query e comandi nel database e MediatR è solo una libreria di messaggistica in-process. CQRS non pretende di eliminare la necessità di una logica aziendale come ci si aspetta da loro. CQRS è un modello per l'accesso ai dati, ma i tuoi problemi riguardano il livello di presentazione: reindirizzamenti, viste, controller.

Penso che potresti applicare erroneamente il modello CQRS all'autenticazione. Con login non può essere modellato come comando in CQRS perché

Comandi: modifica lo stato di un sistema ma non restituisce un valore
- Martin Fowler CommandQuerySeparation

A mio avviso, l'autenticazione è un dominio scadente per CQRS. Con l'autenticazione è necessario un flusso di richieste / risposte sincrono e coerente, in modo da poter 1. controllare le credenziali dell'utente 2. creare una sessione per l'utente 3. gestire una qualsiasi delle varietà di casi limite identificati 4. concedere o negare immediatamente all'utente in risposta.

Vale la pena CQRS / MediatR quando si sviluppa un'applicazione ASP.NET?

CQRS è un modello che ha usi molto specifici. Lo scopo è quello di modellare query e comandi invece di avere un modello per i record usato in CRUD. Man mano che i sistemi diventano più complessi, le esigenze delle viste sono spesso più complesse della semplice visualizzazione di un singolo record o di una manciata di record e una query può meglio modellare le esigenze dell'applicazione. Allo stesso modo i comandi possono rappresentare modifiche a molti record anziché a CRUD, che si modificano singoli record. Martin Fowler avverte

Come ogni modello, CQRS è utile in alcuni punti, ma non in altri. Molti sistemi si adattano a un modello mentale CRUD, e quindi dovrebbero essere fatti in quello stile. Il CQRS è un significativo salto mentale per tutti gli interessati, quindi non dovrebbe essere affrontato a meno che il beneficio non valga la pena. Mentre mi sono imbattuto in usi di successo di CQRS, finora la maggior parte dei casi in cui mi sono imbattuto non è stata così buona, con CQRS visto come una forza significativa per mettere in difficoltà un sistema software.
- Martin Fowler CQRS

Quindi, per rispondere alla tua domanda, CQRS non dovrebbe essere la prima risorsa quando si progetta un'applicazione quando CRUD è adatto. Niente nella tua domanda mi ha dato l'indicazione che hai un motivo per usare CQRS.

Per quanto riguarda MediatR, è una libreria di messaggistica in-process, che mira a disaccoppiare le richieste dalla gestione delle richieste. Devi decidere di nuovo se migliorerà il tuo design per utilizzare questa libreria. Personalmente non sono un sostenitore della messaggistica in-process. L'accoppiamento lento può essere ottenuto in modi più semplici rispetto alla messaggistica e ti consiglio di iniziare da lì.


1
Sono d'accordo al 100%. CQRS è solo un po 'pubblicizzato, quindi ho pensato che "loro" vedessero qualcosa che io non avevo. Perché non riesco a vedere i vantaggi di CQRS nelle app Web CRUD. Finora l'unico scenario è CQRS + ES che ha senso per me.
Snæbjørn,

Qualcuno del mio nuovo lavoro ha deciso di mettere MediatR sul nuovo sistema ASP.Net rivendicandolo come un'architettura. L'implementazione che ha fatto non è DDD, né SOLID, né DRY, né KISS. È un piccolo sistema pieno di YAGNI. Ed è iniziato molto tempo dopo alcuni commenti come il tuo, incluso il tuo. Sto cercando di capire come potrei refactment il codice per adattare gradualmente la sua architettura. Ho avuto la stessa opinione su CQRS al di fuori di un livello aziendale e sono contento che ci siano molti sviluppatori esperti che la pensano in questo modo.
MFedatto,

È un po 'ironico affermare che l'idea di incorporare CQRS / MediatR potrebbe essere associata a un sacco di YAGNI e alla mancanza di KISS, quando in realtà alcune delle alternative popolari, come il modello di Repository, promuovono YAGNI gonfiando la classe del repository e forzando interfacce per specificare molte operazioni CRUD su tutti gli aggregati di root che vogliono implementare tali interfacce, lasciando spesso quei metodi inutilizzati o riempiti con eccezioni "non implementate". Poiché CQRS non utilizza queste generalizzazioni, può implementare solo ciò che è necessario.
Lesair Valmont,

@LesairValmont Repository dovrebbe essere solo CRUD. "specifica molte operazioni CRUD" dovrebbe essere solo 4 (o 5 con "elenco"). Se si dispone di schemi di accesso alle query più specifici, questi non dovrebbero trovarsi nell'interfaccia del repository. Non ho mai incontrato un problema con metodi di repository inutilizzati. Puoi fare un esempio?
Samuel,

@ Samuel: Penso che il modello di repository sia perfetto per alcuni scenari, proprio come lo è CQRS. In realtà, su un'applicazione di grandi dimensioni, ci saranno alcune parti il ​​cui adattamento migliore sarà il modello di archivio e altre che sarebbero maggiormente avvantaggiate dal CQRS. Dipende da molti fattori diversi, come la filosofia seguita da quella parte dell'applicazione (ad esempio task-based (CQRS) vs. CRUD (repo)), l'ORM utilizzato (se presente), la modellazione del dominio ( ad es. DDD). Per i semplici cataloghi CRUD, CQRS è decisamente eccessivo e alcune funzionalità di collaborazione in tempo reale (come una chat) non ne utilizzano nessuna.
Lesair Valmont,

10

CQRS è più una questione di gestione dei dati piuttosto che e non tende a sanguinare troppo in un livello di applicazione (o dominio se preferisci, poiché tende ad essere usato più spesso nei sistemi DDD). L'applicazione MVC, d'altra parte, è un'applicazione a livello di presentazione e dovrebbe essere abbastanza ben separata dal nucleo di query / persistenza del CQRS.

Un'altra cosa degna di nota (dato il confronto tra il Loginmetodo predefinito e il desiderio di controller sottili): non seguirei esattamente i modelli ASP.NET predefiniti / codice boilerplate come qualsiasi cosa di cui dovremmo preoccuparci per le migliori pratiche.

Mi piacciono anche i controller sottili, perché sono molto facili da leggere. Ogni controller che ho di solito ha un oggetto "di servizio" che si accoppia con che essenzialmente gestisce la logica richiesta dal controller:

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

Ancora abbastanza sottile, ma non abbiamo davvero cambiato il modo in cui funziona il codice, basta delegare la gestione al metodo di servizio, che in realtà non ha altro scopo se non quello di rendere le azioni del controller facili da digerire.

Tenete presente che questa classe di servizio è ancora responsabile della delega della logica al modello / all'applicazione come richiesto, in realtà è solo una leggera estensione del controller per mantenere pulito il codice. I metodi di servizio sono generalmente piuttosto brevi.

Non sono sicuro che il mediatore farebbe qualcosa di concettualmente diverso da quello: spostare una logica di base del controller fuori dal controller e in un altro posto per essere elaborato.

(Non avevo mai sentito parlare di questo MediatR prima, e una rapida occhiata alla pagina di github non sembra indicare che sia qualcosa di rivoluzionario - certamente non qualcosa come CQRS - in effetti, sembra essere qualcosa come solo un altro livello di astrazione che tu posso complicare il codice facendolo sembrare più semplice, ma è solo il mio approccio iniziale)


5

Consiglio vivamente di visualizzare la presentazione NDC di Jimmy Bogard sul suo approccio alla modellazione delle richieste http https://www.youtube.com/watch?v=SUiWfhAhgQw

Avrai quindi un'idea chiara di ciò per cui Mediatr è utilizzato.

Jimmy non ha una cieca aderenza a schemi e astrazioni. È molto pragmatico. Mediatr ripulisce le azioni del controller. Per quanto riguarda la gestione delle eccezioni, la inserisco in una classe genitore chiamata qualcosa come Execute. Quindi finisci con un'azione del controller molto pulita.

Qualcosa di simile a:

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'utilizzo è un po 'così:

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

Spero possa aiutare.


4

Molte persone (l'ho fatto anch'io) confondono il modello con una biblioteca. CQRS è un modello ma MediatR è una libreria che è possibile utilizzare per implementare quel modello

Puoi usare CQRS senza MediatR o qualsiasi libreria di messaggistica in-process e puoi usare MediatR senza CQRS:

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

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

CQS sarebbe simile a questo:

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

In effetti, non è necessario nominare i modelli di input "Comandi" come sopra CreateProductCommand. E input delle tue query "Query". Comando e query sono metodi, non modelli.

CQRS riguarda la separazione delle responsabilità (i metodi di lettura devono trovarsi in un posto separato dai metodi di scrittura - isolati). È un'estensione di CQS ma la differenza è in CQS che puoi mettere questi metodi in 1 classe. (nessuna separazione di responsabilità, solo separazione comando-query). Vedi separazione vs segregazione

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

Alla base c'è l'idea che è possibile utilizzare un modello diverso per aggiornare le informazioni rispetto al modello utilizzato per leggere le informazioni.

C'è confusione in ciò che dice, non si tratta di avere un modello separato per input e output, si tratta di separazione delle responsabilità.

CQRS e limitazione della generazione dell'ID

C'è una limitazione che dovrai affrontare quando usi CQRS o CQS

Tecnicamente nella descrizione originale i comandi non dovrebbero restituire alcun valore (nullo) che trovo stupido perché non esiste un modo semplice per ottenere l'id generato da un oggetto appena creato: /programming/4361889/how-to- get-id-in-create-when-apply-cqrs .

quindi devi generare id ogni volta tu stesso invece di lasciarlo fare al database.


Se vuoi saperne di più: https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf


1
Sfido la tua affermazione che un comando del CQRS per il persistere di nuovi dati in un database che non è in grado di restituire un ID appena generato dal database è "stupido". Penso piuttosto che questa sia una questione filosofica. Ricorda che gran parte di DDD e CQRS riguarda l'immutabilità dei dati. Quando ci pensi due volte, inizi a capire che il semplice atto di perseverare nei dati è un'operazione di mutazione dei dati. E non si tratta solo di nuovi ID, ma potrebbe anche essere campi pieni di dati predefiniti, trigger e processi memorizzati che potrebbero modificare anche i tuoi dati.
Lesair Valmont,

Sicuro che puoi inviare un tipo di evento come "ItemCreated" con un nuovo oggetto come argomento. Se hai a che fare semplicemente con il protocollo richiesta-risposta e usi CQRS "vero", allora id deve essere conosciuto in anticipo in modo da poterlo passare a una funzione di query separata - assolutamente nulla di sbagliato in questo. In molti casi, CQRS è semplicemente eccessivo. Puoi vivere senza di essa. Non è altro che un modo di strutturare il tuo codice e dipende principalmente da quali protocolli usi anche tu.
Konrad,

E puoi ottenere l'immutabilità dei dati senza CQRS
Konrad
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.