Стоит ли CQRS / MediatR при разработке приложения ASP.NET?

17

Я в последнее время изучал CQRS / MediatR. Но чем больше я тренируюсь, тем меньше мне это нравится. Возможно, я что-то неправильно понял / все.

Так что все начинается с того, что вы утверждаете, что сводите свой контроллер к этому

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

    return View(model);
}

Что идеально сочетается с тонкой направляющей контроллера. Однако это оставляет некоторые довольно важные детали - обработку ошибок.

Давайте посмотрим на Loginдействие по умолчанию из нового проекта 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);
}

Преобразование, которое представляет нам кучу реальных проблем. Помните, что цель состоит в том, чтобы уменьшить его до

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

    return View(model);
}

Одним из возможных решений этой проблемы является возврат CommandResult<T>вместо а, modelа затем обработка CommandResultв фильтре после действия. Как обсуждено здесь .

Одна реализация CommandResultможет быть такой

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

источник

Однако это не решает нашу проблему в Loginдействии, потому что есть несколько состояний отказа. Мы могли бы добавить эти дополнительные состояния отказа кICommandResult но это отличное начало для очень раздутого класса / интерфейса. Можно сказать, что это не соответствует Единой Ответственности (SRP).

Другая проблема заключается в returnUrl. У нас есть этот return RedirectToLocal(returnUrl);кусок кода. Каким-то образом нам нужно обрабатывать условные аргументы, основанные на состоянии успеха команды. Хотя я думаю, что это можно сделать (я не уверен, что ModelBinder может сопоставить аргументы FromBody и FromQuery ( returnUrlэто FromQuery) одной модели). Можно только задаться вопросом, какие сумасшедшие сценарии могут возникнуть в будущем.

Проверка модели также стала более сложной, наряду с возвратом сообщений об ошибках. Возьми это как пример

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

Мы прилагаем сообщение об ошибке вместе с моделью. Такого рода вещи нельзя сделать с помощью Exceptionстратегии (как предлагается здесь ), потому что нам нужна модель. Возможно, вы можете получить модель изRequest но это будет очень сложный процесс.

В общем, мне трудно преобразовать это «простое» действие.

Я ищу входные данные. Я тут совершенно не прав?

Snæbjørn
источник
6
Похоже, вы уже хорошо понимаете соответствующие проблемы. Существует множество «серебряных пуль», в которых есть игрушечные примеры, которые доказывают свою полезность, но которые неизбежно падают, когда они сжимаются реальностью реального, реального применения.
Роберт Харви
Проверьте MediatR Поведения. Это в основном трубопровод, который позволяет вам решать сквозные проблемы.
FML

Ответы:

14

Я думаю, что вы ожидаете слишком много от шаблона, который вы используете. CQRS специально разработан для устранения различий в модели между запросом и командами для базы данных , а MediatR - это просто библиотека обмена сообщениями в процессе. CQRS не утверждает, что устраняет необходимость в бизнес-логике, как вы ожидаете. CQRS - это шаблон для доступа к данным, но ваши проблемы связаны с уровнем представления направления, представления, контроллеры.

Я думаю, что вы можете неправильно применять шаблон CQRS для аутентификации. При входе в систему это не может быть смоделировано как команда в CQRS, потому что

Команды: Изменить состояние системы, но не возвращают значение
- Martin Fowler CommandQuerySeparation

На мой взгляд, аутентификация - плохой домен для CQRS. При аутентификации вам нужен строго согласованный, синхронный поток запросов-ответов, чтобы вы могли: 1. проверить учетные данные пользователя 2. создать сеанс для пользователя 3. обработать любое из множества выявленных вами пограничных случаев 4. немедленно предоставить или отклонить пользователя в ответ.

Стоит ли CQRS / MediatR при разработке приложения ASP.NET?

CQRS - это шаблон, который имеет очень специфическое применение. Его цель состоит в том, чтобы моделировать запросы и команды вместо того, чтобы иметь модель для записей, используемых в CRUD. По мере усложнения систем требования к представлениям зачастую становятся более сложными, чем просто показ одной записи или нескольких записей, и запрос может лучше моделировать потребности приложения. Точно так же команды могут представлять изменения во многих записях вместо CRUD, в котором вы изменяете отдельные записи. Мартин Фаулер предупреждает

Как и любой шаблон, CQRS полезен в некоторых местах, но не в других. Многие системы соответствуют ментальной модели CRUD, и поэтому должны быть выполнены в этом стиле. CQRS является значительным умственным скачком для всех заинтересованных сторон, поэтому его не следует решать, если вы не выиграете. Хотя я сталкивался с успешным использованием CQRS, до сих пор большинство случаев, с которыми я сталкивался, были не столь хорошими, поскольку CQRS рассматривался как значительная сила для получения программной системы серьезных трудностей.
- Мартин Фаулер CQRS

Поэтому, чтобы ответить на ваш вопрос, CQRS не должен быть первым средством при разработке приложения, когда подходит CRUD. Ничто в вашем вопросе не указывало на то, что у вас есть причина использовать CQRS.

Что касается MediatR, это библиотека обмена сообщениями в процессе, она предназначена для отделения запросов от обработки запросов. Вы должны снова решить, улучшит ли это ваш дизайн, чтобы использовать эту библиотеку. Я лично не сторонник обмена сообщениями в процессе. Слабая связь может быть достигнута более простыми способами, чем обмен сообщениями, и я бы порекомендовал вам начать там.

Самуил
источник
1
Я на 100% согласен. CQRS просто немного раскручен, поэтому я подумал, что «они» увидели то, чего я не увидел. Потому что мне тяжело видеть преимущества CQRS в веб-приложениях CRUD. Пока что единственный сценарий - это CQRS + ES, который имеет смысл для меня.
Snæbjørn
Один парень на моей новой работе решил включить MediatR в новую систему ASP.Net, назвав ее архитектурой. Реализация, которую он сделал, не DDD, ни SOLID, ни DRY, ни KISS. Это маленькая система, полная ЯГНИ. И это началось задолго до того, как некоторые ваши комментарии, включая ваши. Я пытаюсь понять, как я могу изменить код, чтобы постепенно адаптировать его архитектуру. У меня было такое же мнение о CQRS вне бизнес-уровня, и я рад, что так думают несколько опытных разработчиков.
MFedatto
Немного иронично утверждать, что идея включения CQRS / MediatR может быть связана с большим количеством YAGNI и отсутствием KISS, когда на самом деле некоторые из популярных альтернатив, такие как шаблон Repository, продвигают YAGNI, вздувая класс репозитория и заставляя интерфейсы для указания множества операций CRUD на всех корневых агрегатах, которые хотят реализовать такие интерфейсы, часто оставляя эти методы либо неиспользованными, либо заполненными «не реализованными» исключениями. Поскольку CQRS не использует эти обобщения, он может реализовать только то, что необходимо.
Lesair Valmont
Репозиторий @LesairValmont должен быть только CRUD. «указать много операций CRUD» должно быть только 4 (или 5 с «список»). Если у вас есть более конкретные шаблоны доступа к запросам, их не должно быть в вашем интерфейсе репозитория. Я никогда не сталкивался с проблемой неиспользуемых методов хранилища. Можете привести пример?
Самуил
@ Самуэль: Я думаю, что шаблон репозитория идеально подходит для определенных сценариев, как и CQRS. На самом деле, в больших приложениях будут некоторые части, которые лучше всего подойдут к шаблону хранилища, а другие будут более полезны для CQRS. Это зависит от множества различных факторов, таких как философия, используемая в этой части приложения (например, основанная на задачах (CQRS) или CRUD (репо)), используемый ORM (если есть), моделирование домена ( например, DDD). Для простых каталогов CRUD CQRS определенно излишним, и некоторые функции совместной работы в реальном времени (например, чат) не будут использовать ни те, ни другие.
Lesair Valmont
10

CQRS скорее относится к управлению данными, чем к частому прикладному уровню (или домену, если вы предпочитаете, так как он чаще всего используется в системах DDD) и не склонен слишком сильно вливаться в него. Приложение MVC, с другой стороны, является приложением уровня представления и должно быть достаточно хорошо отделено от ядра запросов / постоянства CQRS.

Стоит отметить еще одну вещь (учитывая ваше сравнение по умолчанию Login и желания использовать тонкие контроллеры): я бы не стал точно следовать шаблонам ASP.NET по умолчанию / шаблонному коду, как о чем-то, о чем мы должны беспокоиться для получения лучших практик.

Мне также нравятся тонкие контроллеры, потому что их очень легко читать. У каждого контроллера, который у меня обычно есть, есть «служебный» объект, с которым он соединяется, который по существу обрабатывает логику, требуемую для контроллера:

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

Все еще достаточно тонкий, но мы на самом деле не изменили, как работает код, просто делегируем обработку методу service, который на самом деле не служит никакой другой цели, кроме упрощения восприятия действий контроллера.

Имейте в виду, что этот класс обслуживания по-прежнему отвечает за делегирование логики модели / приложению по мере необходимости, на самом деле это всего лишь небольшое расширение контроллера для поддержания аккуратного кода. Методы обслуживания, как правило, довольно короткие.

Я не уверен, что посредник будет делать что-то принципиально иное, чем это: переместить некоторую базовую логику контроллера из контроллера в другое место для обработки.

(Я не слышал об этом MediatR раньше, и быстрый взгляд на страницу github, похоже, не указывает на то, что это что-то новаторское - конечно, не что-то вроде CQRS - на самом деле, это похоже на еще один уровень абстракции, который вы можно вставить, чтобы усложнить код, сделав его более простым, но это только мое первоначальное решение)

jleach
источник
5

Я настоятельно рекомендую вам ознакомиться с презентацией Джимми Богарда для NDC о его подходе к моделированию http-запросов. https://www.youtube.com/watch?v=SUiWfhAhgQw

После этого вы получите четкое представление о том, для чего используется Mediatr.

Джимми не имеет слепой приверженности шаблонам и абстракциям. Он очень прагматичный. Медиатр очищает действия контроллера. Что касается обработки исключений, я помещаю это в родительский класс, называемый чем-то вроде Execute. Таким образом, вы получите очень чистое действие контроллера.

Что-то вроде:

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

Использование выглядит примерно так:

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

Надеюсь, это поможет.

DavidRogersDev
источник
4

Многие люди (я тоже так делал) путают шаблон с библиотекой. CQRS - это шаблон, но MediatR - это библиотека, которую вы можете использовать для реализации этого шаблона.

Вы можете использовать CQRS без MediatR или любую библиотеку обмена сообщениями в процессе, и вы можете использовать MediatR без CQRS:

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

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

CQS будет выглядеть так:

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

На самом деле, вам не нужно называть ваши входные модели "Команды", как указано выше CreateProductCommand. И ввод ваших запросов "Запросы". Команда и запросы являются методами, а не моделями.

CQRS - это разделение ответственности (методы чтения должны быть отделены от методов записи - изолированными). Это расширение CQS, но разница в CQS, вы можете поместить эти методы в 1 класс. (нет разделения ответственности, только разделение команд и запросов). См разделение против сегрегации

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

В основе лежит идея о том, что вы можете использовать другую модель для обновления информации, нежели модель, которую вы используете для чтения информации.

Существует путаница в том, что в нем говорится, речь идет не о наличии отдельной модели для ввода и вывода, а о разделении ответственности.

CQRS и ограничение генерации идентификатора

Есть одно ограничение, с которым вы столкнетесь при использовании CQRS или CQS

Технически в оригинальном описании команды не должны возвращать никакого значения (void), которое я считаю глупым, потому что нет простого способа получить сгенерированный идентификатор из вновь созданного объекта: /programming/4361889/how-to- get-id-in-create-when-apply-cqrs .

так что вам нужно каждый раз генерировать id, а не позволять базе данных делать это.


Если вы хотите узнать больше: https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf

Konrad
источник
1
Я оспариваю ваше утверждение о том, что команда CQRS для сохранения новых данных в базе данных, которая не может вернуть новый сгенерированный базой данных идентификатор, является «глупой». Я скорее думаю, что это философский вопрос. Помните, что большая часть DDD и CQRS связана с неизменностью данных. Когда вы дважды об этом думаете, вы начинаете понимать, что простой акт сохранения данных - это операция мутации данных. И речь идет не только о новых идентификаторах, но также могут быть поля, заполненные данными по умолчанию, триггерами и хранимыми процедурами, которые также могут изменить ваши данные.
Lesair Valmont
Конечно, вы можете отправить какое-то событие, например «ItemCreated», с новым элементом в качестве аргумента. Если вы имеете дело только с протоколом запрос-ответ и используете «истинный» CQRS, тогда идентификатор должен быть известен заранее, чтобы вы могли передать его в отдельную функцию запроса - в этом нет абсолютно ничего плохого. Во многих случаях CQRS просто перебор. Вы можете жить без этого. Это не что иное, как способ структурирования вашего кода, и это зависит главным образом от того, какие протоколы вы используете.
Конрад
И вы можете добиться неизменности данных без CQRS
Конрад