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

10

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

Quindi inizia impressionante 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 alcuni dettagli piuttosto importanti: la gestione degli errori.

Osserviamo l'azione predefinita Login da 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);
}

Conversione che ci presenta una serie di problemi del 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 è restituire CommandResult<T> anziché model e quindi gestire CommandResult in un filtro post-azione. Come discusso qui .

Un'implementazione di CommandResult potrebbe essere come questa

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

source

Tuttavia ciò non risolve il nostro problema nell'azione Login , perché ci sono più stati di errore. Potremmo aggiungere questi stati di errore extra a ICommandResult ma questo è un ottimo inizio per una classe / interfaccia molto gonfia. Si potrebbe dire che non è conforme alla Single Responsibility (SRP).

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

La convalida del modello è diventata anche più complessa insieme alla restituzione dei messaggi di errore. Prendi questo come un esempio

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

Alleghiamo un messaggio di errore insieme al modello. Questo tipo di cose non può essere fatto usando una strategia Exception (come suggerito qui ) perché abbiamo bisogno del modello. Forse puoi ottenere il modello da Request ma sarebbe un processo molto complicato.

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

Sto cercando input. Sono totalmente nel torto qui?

    
posta Snæbjørn 14.07.2017 - 15:30
fonte

3 risposte

10

Penso che ti aspetti troppo del pattern che stai utilizzando. CQRS è progettato specificamente per indirizzare la differenza di modello tra query e comandi al database e MediatR è solo una libreria di messaggistica in-process. CQRS non pretende di eliminare la necessità di una logica di business come ci si aspetta da loro. CQRS è un pattern per l'accesso ai dati, ma i tuoi problemi sono con layer di presentazione - reindirizzamenti, visualizzazioni, controller.

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

Commands: Change the state of a system but do not return a value
- Martin Fowler CommandQuerySeparation

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

Is CQRS/MediatR worth it when developing an ASP.NET application?

CQRS è un modello che ha usi molto specifici. Lo scopo è 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 richieste delle visualizzazioni sono spesso più complesse di una semplice visualizzazione di un singolo record o di una manciata di record e una query può modellare meglio le esigenze dell'applicazione. Allo stesso modo i comandi possono rappresentare le modifiche a molti record anziché CRUD, che modificano i singoli record. Martin Fowler avverte

Like any pattern, CQRS is useful in some places, but not in others. Many systems do fit a CRUD mental model, and so should be done in that style. CQRS is a significant mental leap for all concerned, so shouldn't be tackled unless the benefit is worth the jump. While I have come across successful uses of CQRS, so far the majority of cases I've run into have not been so good, with CQRS seen as a significant force for getting a software system into serious difficulties.
- 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.

Come per MediatR, è una libreria di messaggistica in-process, ha lo scopo di disaccoppiare le richieste dalla gestione delle richieste. Devi decidere nuovamente 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ì.

    
risposta data 16.07.2017 - 11:12
fonte
8

CQRS è più una cosa di gestione dei dati che non tende a sanguinare troppo pesantemente in un livello applicativo (o Domain se preferisci, poiché tende a essere più spesso usato nei sistemi DDD). L'applicazione MVC, d'altra parte, è un'applicazione a livello di presentazione e dovrebbe essere discretamente separata dal core di query / persistenza di CQRS.

Un'altra cosa che vale la pena notare (dato il confronto tra il metodo predefinito Login e il desiderio per i controller sottili): non seguire esattamente i modelli di default ASP.NET / 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 "servizio" che associa a quello che gestisce essenzialmente 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, ma deleghiamo la gestione al metodo di servizio, il che non ha proprio altro scopo se non quello di rendere le azioni del controller facili da digerire.

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

Non sono sicuro che il mediatore farebbe qualcosa di concettualmente diverso da quello: spostando alcune logiche di base del controller dal controller e in qualche altro da elaborare.

(Non avevo mai sentito parlare di questo MediatR prima, e una rapida occhiata alla pagina github non sembra indicare che sia qualcosa di rivoluzionario - certamente non qualcosa come CQRS - in effetti, sembra essere qualcosa di simile a un altro strato di astrazione che puoi inserire per complicare il codice per renderlo più semplice, ma questo è solo il mio take iniziale)

    
risposta data 16.07.2017 - 14:19
fonte
1

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

È possibile utilizzare CQRS senza MediatR o qualsiasi libreria di messaggistica in-process e si può utilizzare 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 devi nominare i tuoi modelli di input "Comandi" come sopra CreateProductCommand . E l'input delle tue query "Query". I comandi e le query sono metodi, non modelli.

Il CQRS riguarda la separazione delle responsabilità (i metodi di lettura devono essere in un posto separato dai metodi di scrittura - isolati). È un'estensione di CQS ma la differenza è in CQS è possibile inserire questi metodi in 1 classe. (nessuna segregazione di responsabilità, solo separazione tra query e comandi). Vedi separazione vs segregazione

Dal link :

At its heart is the notion that you can use a different model to update information than the model you use to read information.

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 di id

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

Tecnicamente nella descrizione originale i comandi non dovrebbero restituire alcun valore (void) che trovo stupido perché non esiste un modo semplice per ottenere l'ID generato da un oggetto appena creato: link .

quindi devi generare id ogni volta da solo invece di lasciare che sia il database a farlo.

Se vuoi saperne di più: link

    
risposta data 13.06.2018 - 14:44
fonte

Leggi altre domande sui tag