Czy CQRS / MediatR jest tego warty przy tworzeniu aplikacji ASP.NET?

17

Ostatnio badałem CQRS / MediatR. Ale im więcej drążę, tym mniej mi się podoba. Być może źle zrozumiałem coś / wszystko.

Zaczyna się więc niesamowicie, twierdząc, że zredukowałeś do tego kontroler

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

    return View(model);
}

Co idealnie pasuje do cienkich wytycznych kontrolera. Pomija jednak kilka bardzo ważnych szczegółów - obsługę błędów.

Przyjrzyjmy się domyślnej Loginakcji z nowego projektu 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);
}

Konwersja, która stwarza nam wiele problemów w świecie rzeczywistym. Pamiętaj, że celem jest ograniczenie tego do

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

    return View(model);
}

Jednym z możliwych rozwiązań jest zwrócenie CommandResult<T>zamiast a, modela następnie obsłużenie CommandResultfiltru po działaniu. Jak omówiono tutaj .

Jedna implementacja CommandResultmoże być taka

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

źródło

Jednak to tak naprawdę nie rozwiązuje naszego problemu w Logindziałaniu, ponieważ istnieje wiele stanów awarii. Możemy dodać te dodatkowe stany awarii, ICommandResultale jest to świetny początek dla bardzo rozdętej klasy / interfejsu. Można powiedzieć, że nie jest on zgodny z Single Responsibility (SRP).

Innym problemem jest returnUrl. Mamy ten return RedirectToLocal(returnUrl);fragment kodu. Jakoś musimy obsługiwać argumenty warunkowe w oparciu o stan powodzenia polecenia. Chociaż myślę, że można to zrobić (nie jestem pewien, czy ModelBinder może zmapować argumenty FromBody i FromQuery ( returnUrljest FromQuery) na pojedynczy model). Można się tylko zastanawiać, jakie szalone scenariusze mogą przyjść na później.

Sprawdzanie poprawności modelu stało się również bardziej złożone wraz ze zwracaniem komunikatów o błędach. Weź to jako przykład

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

Do modelu dołączamy komunikat o błędzie. Tego rodzaju rzeczy nie można zrobić przy użyciu Exceptionstrategii (jak sugerowano tutaj ), ponieważ potrzebujemy modelu. Być może możesz pobrać model z, Requestale byłby to bardzo zaangażowany proces.

Podsumowując, trudno mi było przekonwertować tę „prostą” akcję.

Szukam danych wejściowych. Czy tutaj całkowicie się mylę?

Snæbjørn
źródło
6
Wygląda na to, że całkiem dobrze rozumiesz odpowiednie obawy. Istnieje wiele „srebrnych kul”, które mają przykłady zabawek, które dowodzą ich przydatności, ale które nieuchronnie przewracają się, gdy są ściśnięte przez rzeczywistość rzeczywistej aplikacji.
Robert Harvey
Sprawdź zachowania MediatR. Jest to w zasadzie rurociąg, który pozwala rozwiązywać problemy przekrojowe.
fml

Odpowiedzi:

14

Myślę, że oczekujesz zbyt wiele używanego wzoru. CQRS został specjalnie zaprojektowany, aby zaradzić różnicy w modelu między zapytaniami a poleceniami w bazie danych , a MediatR to tylko biblioteka komunikatów w trakcie przetwarzania. CQRS nie twierdzi, że eliminuje potrzebę logiki biznesowej tak, jak tego oczekujesz. CQRS to wzorzec dostępu do danych, ale twoje problemy dotyczą warstwy prezentacji - przekierowań, widoków, kontrolerów.

Myślę, że niewłaściwie stosujesz wzorzec CQRS do uwierzytelniania. Przy logowaniu nie może być modelowany jako polecenie w CQRS, ponieważ

Polecenia: zmień stan systemu, ale nie zwracaj wartości
- Martin Fowler CommandQuerySeparation

Moim zdaniem uwierzytelnianie jest kiepską domeną dla CQRS. Przy uwierzytelnianiu potrzebujesz bardzo spójnego, synchronicznego przepływu żądania-odpowiedzi, abyś mógł 1. sprawdzić dane uwierzytelniające użytkownika 2. utworzyć sesję dla użytkownika 3. obsłużyć dowolną z wielu zidentyfikowanych przez Ciebie przypadków brzegowych 4. natychmiast przyznać lub odrzucić użytkownika w odpowiedzi.

Czy CQRS / MediatR jest tego warty przy tworzeniu aplikacji ASP.NET?

CQRS to wzorzec, który ma bardzo specyficzne zastosowania. Jego celem jest modelowanie zapytań i poleceń zamiast posiadania modelu dla rekordów używanych w CRUD. W miarę, jak systemy stają się coraz bardziej złożone, wymagania dotyczące widoków są często bardziej złożone niż tylko pokazanie jednego rekordu lub garści rekordów, a zapytanie może lepiej modelować potrzeby aplikacji. Podobnie polecenia mogą reprezentować zmiany w wielu rekordach zamiast CRUD, które zmieniają pojedyncze rekordy. Martin Fowler ostrzega

Jak każdy wzór, CQRS jest przydatny w niektórych miejscach, ale w innych nie. Wiele systemów pasuje do modelu mentalnego CRUD i dlatego należy to robić w tym stylu. CQRS to znaczący skok mentalny dla wszystkich zainteresowanych, więc nie należy się nim zajmować, chyba że korzyść jest warta skoku. Wprawdzie natknąłem się na udane zastosowania CQRS, ale jak dotąd większość spraw, na które natknąłem się, nie była tak dobra, a CQRS jest postrzegany jako znacząca siła, która powoduje poważne trudności w oprogramowaniu.
- Martin Fowler CQRS

Aby odpowiedzieć na twoje pytanie, CQRS nie powinno być pierwszym rozwiązaniem przy projektowaniu aplikacji, gdy CRUD jest odpowiedni. Nic w twoim pytaniu nie wskazało mi, że masz powód do korzystania z CQRS.

Jeśli chodzi o MediatR, jest to biblioteka wiadomości w trakcie przetwarzania, której celem jest oddzielenie żądań od obsługi żądań. Musisz ponownie zdecydować, czy poprawi to twój projekt korzystania z tej biblioteki. Osobiście nie jestem zwolennikiem przesyłania wiadomości w toku. Luźne sprzężenie można osiągnąć w prostszy sposób niż wysyłanie wiadomości, więc polecam zacząć od tego.

Samuel
źródło
1
100% się zgadzam. CQRS jest trochę podekscytowany, więc pomyślałem, że „widzieli” coś, czego nie widziałem. Ponieważ trudno mi dostrzec zalety CQRS w aplikacjach CRUD. Jak dotąd jedynym scenariuszem jest CQRS + ES, który ma dla mnie sens.
Snæbjørn
Jakiś facet z mojej nowej pracy postanowił umieścić MediatR na nowym systemie ASP.Net, twierdząc, że jest to architektura. Implementacja, którą wykonał, nie jest DDD, SOLID, SUCHO ani KISS. Jest to mały system pełen YAGNI. Zaczęło się długo po komentarzach takich jak twój, w tym twój. Próbuję wymyślić, w jaki sposób mogę zmienić kod, aby stopniowo dostosowywać jego architekturę. Miałem taką samą opinię na temat CQRS poza warstwą biznesową i cieszę się, że jest kilku doświadczonych deweloperów myślących w ten sposób.
MFedatto
Ironiczne jest stwierdzenie, że pomysł włączenia CQRS / MediatR może być związany z dużą ilością YAGNI i brakiem KISS, podczas gdy w rzeczywistości niektóre z popularnych alternatyw, takich jak wzorzec repozytorium, promują YAGNI poprzez rozdęcie klasy repozytorium i wymuszenie interfejsy do określania wielu operacji CRUD na wszystkich agregatach głównych, które chcą zaimplementować takie interfejsy, często pozostawiając te metody nieużywane lub wypełnione wyjątkami „nie zaimplementowanymi”. Ponieważ CQRS nie używa tych uogólnień, może implementować tylko to, co jest potrzebne.
Lesair Valmont,
@LesairValmont Repository ma być tylko CRUD. „określ wiele operacji CRUD” powinno wynosić tylko 4 (lub 5 z „listą”). Jeśli masz bardziej szczegółowe wzorce dostępu do zapytań, nie powinny one znajdować się w interfejsie repozytorium. Nigdy nie spotkałem się z problemem nieużywanych metod repozytorium. Czy możesz podać przykład?
Samuel
@Samuel: Myślę, że wzorzec repozytorium jest idealnie odpowiedni dla niektórych scenariuszy, podobnie jak CQRS. W rzeczywistości, w przypadku dużej aplikacji, będą takie części, które najlepiej pasują do wzorca repozytorium, a inne, które byłyby bardziej korzystne dla CQRS. Zależy to od wielu różnych czynników, takich jak filozofia zastosowana w tej części aplikacji (np. Oparte na zadaniach (CQRS) vs. CRUD (repo)), używana ORM (jeśli istnieje), modelowanie domeny ( np. DDD). W przypadku prostych katalogów CRUD CQRS jest zdecydowanie przesadny, a niektóre funkcje współpracy w czasie rzeczywistym (takie jak czat) nie skorzystałyby z nich.
Lesair Valmont
10

CQRS jest bardziej rzeczą do zarządzania danymi niż i nie ma tendencji do zbyt silnego upuszczania do warstwy aplikacji (lub domeny, jeśli wolisz, ponieważ najczęściej jest używana w systemach DDD). Z drugiej strony twoja aplikacja MVC jest aplikacją warstwy prezentacji i powinna być dość dobrze oddzielona od rdzenia zapytania / trwałości CQRS.

Kolejna rzecz warta odnotowania (biorąc pod uwagę porównanie domyślnej Loginmetody i chęci posiadania cienkich kontrolerów): nie przestrzegałbym domyślnych szablonów / kodu szablonu ASP.NET, ponieważ powinniśmy się martwić o najlepsze praktyki.

Lubię też cienkie kontrolery, ponieważ są bardzo łatwe do odczytania. Każdy kontroler, który mam zwykle ma obiekt „usługi”, który łączy się z nim, co zasadniczo obsługuje logikę wymaganą przez kontroler:

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

Wciąż wystarczająco cienki, ale tak naprawdę nie zmieniliśmy sposobu działania kodu, po prostu przekaż obsługę metody serwisowej, która tak naprawdę nie służy żadnemu celowi poza tym, że czynności kontrolera są łatwe do strawienia.

Należy pamiętać, że ta klasa usług jest nadal odpowiedzialna za delegowanie logiki do modelu / aplikacji zgodnie z wymaganiami, to tak naprawdę tylko niewielkie rozszerzenie kontrolera, aby utrzymać porządek w kodzie. Metody serwisowe są również na ogół dość krótkie.

Nie jestem pewien, czy mediator zrobiłby coś koncepcyjnie odmiennego: przeniesienie podstawowej logiki kontrolera z kontrolera do innego miejsca do przetworzenia.

(Nie słyszałem wcześniej o tym MediatR, a szybkie spojrzenie na stronę github nie wydaje się wskazywać, że jest to coś przełomowego - na pewno nie coś takiego jak CQRS - w rzeczywistości wygląda to jak kolejna warstwa abstrakcji można wprowadzić, aby skomplikować kod, dzięki czemu wygląda on prostiej, ale to tylko moje początkowe podejście)

Jleach
źródło
5

Bardzo polecam obejrzenie prezentacji NDC Jimmy'ego Bogarda na temat jego podejścia do modelowania zapytań http https://www.youtube.com/watch?v=SUiWfhAhgQw

Następnie uzyskasz jasne pojęcie o tym, do czego służy Mediatr.

Jimmy nie ślepo przestrzega wzorów i abstrakcji. On jest bardzo pragmatyczny. Mediatr usuwa działania kontrolera. Jeśli chodzi o obsługę wyjątków, wypycham to do klasy nadrzędnej o nazwie coś takiego jak Execute. W rezultacie otrzymujesz bardzo czyste działanie kontrolera.

Coś jak:

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

Sposób użycia wygląda trochę tak:

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

Mam nadzieję, że to pomaga.

DavidRogersDev
źródło
4

Wiele osób (ja też to zrobiłem) myli wzór z biblioteką. CQRS to wzorzec, ale MediatR to biblioteka , której można użyć do wdrożenia tego wzorca

Możesz używać CQRS bez MediatR lub dowolnej biblioteki komunikatów w toku i możesz używać MediatR bez CQRS:

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

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

CQS wyglądałby tak:

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

W rzeczywistości nie musisz nazywać modeli wejściowych „Poleceniami” jak wyżej CreateProductCommand. I wprowadzanie zapytań „Zapytania”. Polecenia i zapytania to metody, a nie modele.

CQRS polega na segregacji odpowiedzialności (metody odczytu muszą znajdować się w innym miejscu niż metody pisania - izolowane). Jest to rozszerzenie do CQS, ale różnica polega na tym, że można umieścić te metody w 1 klasie. (bez segregacji odpowiedzialności, po prostu separacja poleceń i zapytań). Zobacz rozdział a segregacja

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

Jego sednem jest koncepcja, że ​​można użyć innego modelu do aktualizacji informacji niż model używany do odczytu informacji.

Jest zamieszanie w tym, co mówi, nie chodzi o oddzielny model dla danych wejściowych i wyjściowych, chodzi o rozdzielenie odpowiedzialności.

CQRS i ograniczenie generowania identyfikatora

Podczas korzystania z CQRS lub CQS napotkasz jedno ograniczenie

Technicznie w oryginalnym opisie polecenia nie powinny zwracać żadnych wartości (void), które uważam za głupie, ponieważ nie ma łatwego sposobu na uzyskanie wygenerowanego identyfikatora z nowo utworzonego obiektu: /programming/4361889/how-to- get-id-in-create-when-application-cqrs .

więc musisz generować identyfikator za każdym razem, zamiast pozwolić bazie danych to zrobić.


Jeśli chcesz dowiedzieć się więcej: https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf

Konrad
źródło
1
Kwestionuję twoje potwierdzenie, że polecenie CQRS dotyczące utrwalania nowych danych w bazie danych, które nie może zwrócić nowo wygenerowanego identyfikatora bazy danych, jest „głupie”. Raczej myślę, że jest to kwestia filozoficzna. Pamiętaj, że wiele z DDD i CQRS dotyczy niezmienności danych. Kiedy pomyślisz o tym dwa razy, zaczynasz zdawać sobie sprawę, że sam akt utrwalania danych jest operacją mutacji danych. I nie chodzi tylko o nowe identyfikatory, ale mogą to być również pola wypełnione domyślnymi danymi, wyzwalacze i przechowywane procy, które również mogą zmienić twoje dane.
Lesair Valmont
Pewnie możesz wysłać jakieś zdarzenie, takie jak „ItemCreated” z nowym przedmiotem jako argumentem. Jeśli masz do czynienia tylko z protokołem żądanie-odpowiedź i używasz „prawdziwego” CQRS, to identyfikator musi być znany z góry, abyś mógł przekazać go do osobnej funkcji zapytania - absolutnie nic złego z tym. W wielu przypadkach CQRS to po prostu przesada. Możesz bez niego żyć. To nic innego jak sposób ustrukturyzowania kodu i zależy to głównie od używanych protokołów.
Konrad
I można osiągnąć niezmienność danych bez CQRS
Konrad