Чи варто 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але це був би дуже задіяний процес.

Так що загалом мені важко перетворити цю "просту" дію.

Я шукаю вхідні дані. Я тут абсолютно не так?


6
Здається, ви вже досить добре розумієте відповідні проблеми. Там багато "срібних куль", які мають іграшкові приклади, які підтверджують їхню корисність, але які неминуче перепадають, коли їх стискає реальність справжнього реального застосування.
Роберт Харві

Перевірте поведінку MediatR. Це в основному трубопровід, який дозволяє вирішити проблеми, що перетинаються між собою.
fml

Відповіді:


14

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

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

Команди: Змініть стан системи, але не повертайте значення
- Мартін Фаулер 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. Це невелика система, повна YAGNI. І це почалося довго після включення деяких коментарів, таких як ваш, включаючи ваші. Я намагаюся зрозуміти, як я можу відновити код, щоб поступово адаптувати його архітектуру. У мене була така ж думка щодо CQRS поза бізнес-шаром, і я радий, що так думають кілька розробників.
MFedatto

Трохи іронічно стверджувати, що ідея включення CQRS / MediatR може бути пов'язана з великою кількістю YAGNI та відсутністю KISS, коли насправді деякі популярні альтернативи, як, наприклад, модель Репозиторію, просувають YAGNI шляхом роздуття класу сховищ та примушування інтерфейси, щоб вказати багато операцій CRUD на всіх кореневих агрегатах, які хочуть реалізувати такі інтерфейси, часто залишаючи ці методи або невикористаними, або заповненими винятками "не реалізованих". Оскільки CQRS не використовує цих узагальнень, він може реалізовувати лише те, що потрібно.
Лесаїр Валмонт

@ LeesairValmont Repository повинен бути лише CRUD. "вказати багато операцій CRUD" має бути лише 4 (або 5 зі "списком"). Якщо у вас є більш конкретні схеми доступу до запитів, вони не повинні знаходитись в інтерфейсі вашого сховища. Я ніколи не стикався з проблемою невикористаних методів сховища. Чи можете ви навести приклад?
Самуїл

@Samuel: Я думаю, що модель сховища ідеально підходить для певних сценаріїв, як і 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");
    }
}

Ще досить тонкий, але ми не дуже змінили, як працює код, просто делегуємо обробку методу обслуговування, який насправді не виконує ніяких цілей, крім того, щоб полегшити засвоєння дій контролера.

Майте на увазі, що цей клас обслуговування все ще відповідає за делегування логіки моделі / додатку у міру необхідності, це дійсно лише незначне розширення контролера, щоб зберегти код акуратним. Як правило, методи обслуговування також досить короткі.

Я не впевнений, що посередник робить щось концептуально інше, ніж це: переміщення якоїсь основної логіки контролера з контролера і кудись інше, яке потрібно обробити.

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


5

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

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

Джиммі не має сліпого прихильності до моделей та абстракцій. Він дуже прагматичний. 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);
}

Сподіваюся, що це допомагає.


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 та id

Є одне обмеження, з яким ви зіткнетесь під час використання CQRS або CQS

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

тому вам доведеться щоразу створювати ідентифікатор, замість того, щоб дозволити цьому робити базу даних.


Якщо ви хочете дізнатися більше: https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf


1
Я заперечую ваше твердження, що команда CQRS щодо збереження нових даних у базі даних не в змозі повернути щойно створений базу даних Id "дурна". Я радше думаю, що це філософська справа. Пам'ятайте, що велика частина DDD і CQRS стосується незмінності даних. Коли ви думаєте про це двічі, ви починаєте розуміти, що сам акт збереження даних - це операція мутації даних. І справа не тільки в нових ідентифікаторах, але це можуть бути також поля, заповнені даними за замовчуванням, тригерами та збереженими документами, які також можуть змінювати ваші дані.
Лесаїр Валмонт

Звичайно, ви можете надіслати якусь подію на зразок "ItemCreate" з новим елементом як аргумент. Якщо ви маєте справу лише з протоколом відповіді на запит і використовуєте "справжній" CQRS, тоді ідентифікатор повинен бути відомий вперед, щоб ви могли передати його окремій функції запиту - абсолютно нічого поганого в цьому немає. У багатьох випадках CQRS є просто надмірним. Можна жити і без цього. Це не що інше, як спосіб структурування коду, і це залежить, головним чином, від того, які протоколи ви також використовуєте.
Конрад

І ви можете досягти незмінності даних без CQRS
Конрад
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.