Lohnt sich CQRS / MediatR bei der Entwicklung einer ASP.NET-Anwendung?

16

Ich habe mich in letzter Zeit mit CQRS / MediatR befasst. Aber je mehr ich einen Drilldown durchführe, desto weniger mag ich es. Vielleicht habe ich etwas / alles falsch verstanden.

Es fängt also großartig an, wenn Sie behaupten, Ihren Controller darauf zu reduzieren

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

    return View(model);
}

Das passt perfekt zur Richtlinie für dünne Controller. Es werden jedoch einige ziemlich wichtige Details ausgelassen - die Fehlerbehandlung.

Sehen wir uns die Standardaktion Loginaus einem neuen MVC-Projekt an

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

Das Konvertieren stellt uns vor eine Reihe von Problemen in der realen Welt. Denken Sie daran, das Ziel ist es, es zu reduzieren

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

    return View(model);
}

Eine mögliche Lösung besteht darin, ein CommandResult<T>anstelle von a zurückzugeben modelund dann den CommandResultFilter in einer Nachaktion zu behandeln. Wie hier besprochen .

Eine Implementierung von CommandResultkönnte so sein

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

Quelle

Dies löst jedoch unser Problem in der LoginAktion nicht wirklich , da es mehrere Fehlerzustände gibt. Wir könnten diese zusätzlichen Fehlerzustände hinzufügen, ICommandResultaber das ist ein guter Anfang für eine sehr aufgeblähte Klasse / Schnittstelle. Man könnte sagen, es entspricht nicht der Single Responsibility (SRP).

Ein weiteres Problem ist das returnUrl. Wir haben diesen return RedirectToLocal(returnUrl);Code. Irgendwie müssen wir bedingte Argumente behandeln, die auf dem Erfolgsstatus des Befehls basieren. Ich glaube zwar, dass dies getan werden könnte (ich bin nicht sicher, ob der ModelBinder FromBody- und FromQuery- returnUrlArgumente ( is FromQuery) einem einzelnen Modell zuordnen kann ). Man kann sich nur fragen, welche verrückten Szenarien die Straße runterkommen könnten.

Die Modellvalidierung ist zusammen mit der Rückgabe von Fehlermeldungen auch komplexer geworden. Nehmen Sie dies als Beispiel

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

Wir fügen dem Modell eine Fehlermeldung bei. Mit einer ExceptionStrategie (wie hier vorgeschlagen ) kann so etwas nicht gemacht werden, weil wir das Modell brauchen. Vielleicht können Sie das Modell aus dem Requestherunterladen, aber es wäre ein sehr komplizierter Prozess.

Alles in allem fällt es mir schwer, diese "einfache" Aktion umzusetzen.

Ich suche nach Eingaben. Bin ich hier total im Unrecht?

Snæbjørn
quelle
6
Klingt so, als würdest du die relevanten Anliegen schon ziemlich gut verstehen. Es gibt viele "Silberkugeln" mit Spielzeugbeispielen, die sich als nützlich erweisen, die aber unweigerlich umfallen, wenn sie von der Realität einer tatsächlichen, realen Anwendung gequetscht werden.
Robert Harvey
Schauen Sie sich MediatR Behaviours an. Grundsätzlich handelt es sich um eine Pipeline, mit der Sie Querschnittsthemen angehen können.
FML

Antworten:

14

Ich denke, Sie erwarten zu viel von dem Muster, das Sie verwenden. CQRS wurde speziell entwickelt, um den Modellunterschied zwischen Abfragen und Befehlen für die Datenbank zu beseitigen. MediatR ist eine reine In-Process-Messaging-Bibliothek. CQRS behauptet nicht, die Notwendigkeit für Geschäftslogik zu beseitigen, wie Sie sie erwarten. CQRS ist ein Muster für den Datenzugriff, aber Ihre Probleme betreffen die Präsentationsebene - Weiterleitungen, Ansichten, Controller.

Möglicherweise wenden Sie das CQRS-Muster falsch auf die Authentifizierung an. Mit login kann es in CQRS nicht als Befehl modelliert werden, weil

Befehle: Ändern Sie den Status eines Systems, geben Sie jedoch keinen Wert zurück
- Martin Fowler CommandQuerySeparation

Meiner Meinung nach ist die Authentifizierung eine schlechte Domain für CQRS. Bei der Authentifizierung ist ein stark konsistenter, synchroner Anforderungs-Antwort-Fluss erforderlich, damit Sie 1. die Anmeldeinformationen des Benutzers überprüfen 2. eine Sitzung für den Benutzer erstellen 3. alle von Ihnen identifizierten Randfälle behandeln 4. den Benutzer sofort gewähren oder verweigern können In Beantwortung.

Lohnt sich CQRS / MediatR bei der Entwicklung einer ASP.NET-Anwendung?

CQRS ist ein Muster, das sehr spezifische Verwendungen hat. Es dient dazu, Abfragen und Befehle zu modellieren, anstatt ein Modell für Datensätze zu haben, wie es in CRUD verwendet wird. Wenn Systeme komplexer werden, sind die Anforderungen an Ansichten oft komplexer als das Anzeigen eines einzelnen Datensatzes oder einer Handvoll Datensätze, und eine Abfrage kann die Anforderungen der Anwendung besser modellieren. In ähnlicher Weise können Befehle Änderungen an vielen Datensätzen anstelle von CRUD darstellen, bei denen Sie einzelne Datensätze ändern. Warnt Martin Fowler

Wie jedes Muster ist CQRS an einigen Stellen nützlich, an anderen jedoch nicht. Viele Systeme passen zu einem CRUD-Mentalmodell und sollten daher in diesem Stil erstellt werden. CQRS ist ein bedeutender mentaler Sprung für alle Beteiligten und sollte nur angegangen werden, wenn der Nutzen den Sprung wert ist. Obwohl ich CQRS erfolgreich eingesetzt habe, war die Mehrzahl der Fälle, in denen ich aufgetreten bin, bisher nicht so gut. CQRS wurde als eine wichtige Kraft angesehen, um ein Softwaresystem in ernsthafte Schwierigkeiten zu bringen.
- Martin Fowler CQRS

Um Ihre Frage zu beantworten, sollte CQRS nicht der erste Ausweg sein, wenn Sie eine Anwendung entwerfen, für die CRUD geeignet ist. Nichts in Ihrer Frage hat mir den Hinweis gegeben, dass Sie einen Grund haben, CQRS zu verwenden.

Was MediatR betrifft, handelt es sich um eine In-Process-Messaging-Bibliothek, mit der Anforderungen von der Anforderungsbearbeitung entkoppelt werden sollen. Sie müssen erneut entscheiden, ob die Verwendung dieser Bibliothek Ihr Design verbessern wird. Ich persönlich bin kein Befürworter von In-Process-Messaging. Loose-Coupling kann auf einfachere Weise als Messaging erreicht werden, und ich würde empfehlen, dass Sie dort beginnen.

Samuel
quelle
1
Ich stimme zu 100% zu. CQRS ist nur ein bisschen gehypt, also dachte ich, dass "sie" etwas gesehen haben, was ich nicht gesehen habe. Weil es mir schwer fällt, die Vorteile von CQRS in CRUD-Web-Apps zu erkennen. Bisher ist das einzige Szenario CQRS + ES, das für mich sinnvoll ist.
Snæbjørn
Ein Mitarbeiter meines neuen Jobs entschied sich dafür, MediatR auf ein neues ASP.Net-System zu setzen und behauptete, es sei eine Architektur. Die Implementierung, die er gemacht hat, ist weder DDD, noch SOLID, noch DRY oder KISS. Es ist ein kleines System voller YAGNI. Und es hat schon lange nach einigen Kommentaren wie Ihren begonnen. Ich versuche herauszufinden, wie ich den Code überarbeiten kann, um seine Architektur schrittweise anzupassen. Ich hatte die gleiche Meinung über CQRS außerhalb einer Geschäftsschicht und ich bin froh, dass es mehrere erfahrene Entwickler gibt, die so denken.
MFedatto
Es ist ein bisschen ironisch zu behaupten, dass die Idee, CQRS / MediatR zu integrieren, mit einer Menge YAGNI und einem Mangel an KISS verbunden sein könnte, wenn tatsächlich einige der populären Alternativen, wie das Repository-Muster, YAGNI fördern, indem sie die Repository-Klasse aufblähen und forcieren Schnittstellen, um eine Vielzahl von CRUD-Operationen für alle Stammaggregate anzugeben, die solche Schnittstellen implementieren möchten, wobei diese Methoden häufig entweder nicht verwendet oder mit "nicht implementierten" Ausnahmen gefüllt bleiben. Da CQRS diese Verallgemeinerungen nicht verwendet, kann es nur das implementieren, was benötigt wird.
Lesair Valmont
@LesairValmont Repository soll nur CRUD sein. "Viele CRUD-Operationen angeben" sollte nur 4 sein (oder 5 mit "Liste"). Wenn Sie spezifischere Abfragezugriffsmuster haben, sollten diese nicht in Ihrer Repository-Schnittstelle vorhanden sein. Ich bin noch nie auf ein Problem mit nicht verwendeten Repository-Methoden gestoßen. Kannst du ein Beispiel geben?
Samuel
@Samuel: Ich denke, das Repository-Muster ist für bestimmte Szenarien genau so gut wie für CQRS. Tatsächlich wird es bei einer großen Anwendung einige Teile geben, deren beste Anpassung das Repository-Muster ist, und andere, die von CQRS besser profitiert würden. Es hängt von vielen verschiedenen Faktoren ab, wie der Philosophie, die in diesem Teil der Anwendung verfolgt wird (z. B. aufgabenbasiert (CQRS) vs. CRUD (Repo)), dem verwendeten ORM (falls vorhanden), der Modellierung der Domäne ( zB DDD). Für einfache CRUD-Kataloge ist CQRS definitiv übertrieben, und einige Funktionen für die Zusammenarbeit in Echtzeit (wie ein Chat) würden keine von beiden verwenden.
Lesair Valmont
10

CQRS ist eher eine Datenverwaltungssache als und tendiert nicht dazu, zu stark in eine Anwendungsebene (oder Domain, wenn Sie dies vorziehen, da es in DDD-Systemen am häufigsten verwendet wird) zu bluten. Ihre MVC-Anwendung ist hingegen eine Präsentationsschicht-Anwendung und sollte vom Abfrage- / Persistenzkern des CQRS ziemlich gut getrennt sein.

Eine weitere erwähnenswerte Sache (angesichts Ihres Vergleichs der Standardmethode Loginund des Wunsches nach Thin Controllern): Ich würde den Standard-ASP.NET-Vorlagen / -Boilerplate-Code nicht genau befolgen, da wir uns um Best Practices kümmern sollten.

Ich mag auch dünne Controller, weil sie sehr einfach zu lesen sind. Jeder Controller, den ich normalerweise habe, verfügt über ein "Service" -Objekt, mit dem er gekoppelt ist und das im Wesentlichen die von dem Controller benötigte Logik verarbeitet:

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

Immer noch dünn genug, aber wir haben nicht wirklich geändert, wie der Code funktioniert. Delegieren Sie einfach die Behandlung an die Dienstmethode, die wirklich keinen anderen Zweck erfüllt, als die Controller-Aktionen einfach zu verdauen.

Bedenken Sie, dass diese Serviceklasse weiterhin dafür verantwortlich ist, die Logik nach Bedarf an das Modell / die Anwendung zu delegieren. Dies ist eigentlich nur eine kleine Erweiterung des Controllers, um den Code übersichtlich zu halten. Die Service-Methoden sind in der Regel auch ziemlich kurz.

Ich bin mir nicht sicher, ob der Mediator konzeptionell etwas anderes tun würde: eine grundlegende Steuerungslogik aus der Steuerung heraus und an einen anderen Ort zu verschieben, um sie zu verarbeiten.

(Ich hatte noch nie von diesem MediatR gehört, und ein kurzer Blick auf die Github-Seite scheint nicht darauf hinzudeuten, dass es bahnbrechend ist - sicherlich nicht so etwas wie CQRS - in der Tat sieht es so aus, als wäre es nur eine weitere Abstraktionsebene kann den Code dadurch komplizieren, dass er einfacher aussieht, aber das ist nur meine erste Einstellung.)

JLeach
quelle
5

Ich empfehle Ihnen dringend, Jimmy Bogards NDC-Präsentation zu seinem Ansatz zur Modellierung von http-Anfragen zu lesen: https://www.youtube.com/watch?v=SUiWfhAhgQw

Sie erhalten dann eine klare Vorstellung davon, wofür Mediatr angewendet wird.

Jimmy hält sich nicht blind an Muster und Abstraktionen. Er ist sehr pragmatisch. Mediatr räumt Controller-Aktionen auf. Was die Ausnahmebehandlung betrifft, schiebe ich das in eine übergeordnete Klasse, die so etwas wie Execute heißt. Sie erhalten also eine sehr saubere Controller-Aktion.

Etwas wie:

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

Die Verwendung sieht ungefähr so ​​aus:

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

Ich hoffe, das hilft.

DavidRogersDev
quelle
4

Viele Leute (ich habe es auch getan) verwechseln Muster mit einer Bibliothek. CQRS ist ein Muster, aber MediatR ist eine Bibliothek , mit der Sie dieses Muster implementieren können

Sie können CQRS ohne MediatR oder eine in Bearbeitung befindliche Nachrichtenbibliothek verwenden, und Sie können MediatR ohne CQRS verwenden:

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

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

CQS würde so aussehen:

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

Tatsächlich müssen Sie Ihre Eingabemodelle nicht wie oben beschrieben "Befehle" nennen CreateProductCommand. Und Eingabe Ihrer Abfragen "Abfragen". Befehle und Abfragen sind Methoden, keine Modelle.

Bei CQRS geht es um die Aufgabentrennung (Lesemethoden müssen an einer von Schreibmethoden getrennten Stelle stehen - isoliert). Es ist eine Erweiterung von CQS, aber der Unterschied besteht darin, dass Sie diese Methoden in eine Klasse einteilen können. (keine Aufteilung der Verantwortung, nur Trennung von Befehlen und Abfragen). Siehe Trennung vs. Trennung

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

Im Mittelpunkt steht der Gedanke, dass Sie ein anderes Modell zum Aktualisieren von Informationen verwenden können als das Modell, mit dem Sie Informationen lesen.

Es gibt Verwirrung in der Aussage, es geht nicht darum, ein separates Modell für Input und Output zu haben, es geht um die Trennung von Verantwortung.

Einschränkung der CQRS- und ID-Generierung

Es gibt eine Einschränkung bei der Verwendung von CQRS oder CQS

Technisch gesehen sollten Befehle in der ursprünglichen Beschreibung keinen Wert (void) zurückgeben, den ich für dumm halte, weil es keine einfache Möglichkeit gibt, eine ID aus einem neu erstellten Objekt zu generieren: /programming/4361889/how-to- get-id-in-create-when-apply-cqrs .

Sie müssen also jedes Mal eine ID generieren, anstatt dies von der Datenbank ausführen zu lassen.


Wenn Sie mehr erfahren möchten: https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf

Konrad
quelle
1
Ich fordere Ihre Bestätigung heraus, dass ein CQRS-Befehl zum Speichern neuer Daten in einer Datenbank, der nicht in der Lage ist, eine neu in der Datenbank generierte ID zurückzugeben, "dumm" ist. Ich denke eher, dass dies eine philosophische Angelegenheit ist. Denken Sie daran, dass es bei DDD und CQRS hauptsächlich um die Unveränderlichkeit von Daten geht. Wenn Sie zweimal darüber nachdenken, werden Sie feststellen, dass es sich bei dem Vorgang des Persistierens von Daten lediglich um eine Datenmutationsoperation handelt. Und es geht nicht nur um neue IDs, sondern es können auch Felder mit Standarddaten, Triggern und gespeicherten Prozessen sein, die Ihre Daten ebenfalls ändern können.
Lesair Valmont
Sicher können Sie eine Art Ereignis wie "ItemCreated" mit einem neuen Element als Argument senden. Wenn Sie sich lediglich mit dem Anforderungs-Antwort-Protokoll befassen und "echtes" CQRS verwenden, muss id im Voraus bekannt sein, damit Sie es an eine separate Abfragefunktion übergeben können - daran ist absolut nichts auszusetzen. In vielen Fällen ist CQRS nur übertrieben. Sie können ohne es leben. Es ist nichts anderes als eine Art, Ihren Code zu strukturieren, und das hängt hauptsächlich davon ab, welche Protokolle Sie auch verwenden.
Konrad
Und ohne CQRS
Konrad