ASP.NETアプリケーションを開発する場合、CQRS / MediatRは価値がありますか?


16

私は最近CQRS / MediatRを調査しています。しかし、ドリルダウンすればするほど、それが好きではなくなります。おそらく私は何か/すべてを誤解しました。

だから、これをあなたのコントローラーをこれに減らすと主張することから、それは驚くほど始まります

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

    return View(model);
}

Thin Controllerのガイドラインに完全に適合します。ただし、非常に重要な詳細-エラー処理は省略します。

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

これに対する1つの可能な解決策CommandResult<T>は、aの代わりにを返し、modelその後CommandResult、ポストアクションフィルターで処理することです。ここで説明したように

の1つの実装は次のCommandResultようになります

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

ソース

ただし、Login複数の障害状態があるため、実際のアクションの問題は解決しません。これらの追加の障害状態を追加することもできますがICommandResult、それは非常に肥大化したクラス/インターフェイスの素晴らしい出発点です。単一責任(SRP)に準拠していないと言う人もいるかもしれません。

別の問題は、returnUrlです。このreturn RedirectToLocal(returnUrl);コードがあります。どういうわけか、コマンドの成功状態に基づいて条件付き引数を処理する必要があります。私はそれができると思いますが(ModelBinderがFromBodyとFromQuery(returnUrlFromQuery)引数を単一のモデルにマッピングできるかどうかはわかりません)。どのようなクレイジーなシナリオが今後発生する可能性があるのか​​疑問に思うだけです。

モデルの検証も、エラーメッセージを返すことに伴い、より複雑になりました。これを例に取ります

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

モデルとともにエラーメッセージを添付します。この種のことは、モデルを必要とするため、Exception戦略を使用して行うことはできません(ここで提案されています)。おそらくモデルを取得できますRequestが、非常に複雑なプロセスになります。

全体として、この「単純な」アクションを変換するのに苦労しています。

入力を探しています。私はここで完全に間違っていますか?


6
関連する懸念事項をすでに十分に理解しているようですね。実用性を証明するおもちゃの例がありますが、実際の実際のアプリケーションの現実に圧迫されると必然的に倒れる「銀の弾丸」がたくさんあります。
ロバートハーベイ

MediatRの動作を確認してください。基本的に、横断的な懸念に取り組むことができるパイプラインです。
fml

回答:


14

使用しているパターンの多くを期待していると思います。CQRSは、データベースに対するクエリとコマンドのモデルの違いに対処するように特別に設計されており、MediatRは単なるインプロセスメッセージングライブラリです。CQRSは、期待するようなビジネスロジックの必要性を排除すると主張していません。CQRSはデータアクセスのパターンですが、問題はプレゼンテーションレイヤー(リダイレクト、ビュー、コントローラー)にあります。

CQRSパターンを認証に誤って適用している可能性があると思います。ログインで、CQRSのコマンドとしてモデル化できません。

コマンド:システムの状態を変更しますが、値を返しません-Martin
Fowler CommandQuerySeparation

私の意見では、認証はCQRSにとって不十分なドメインです。認証では、1。ユーザーの資格情報を確認します2.ユーザーのセッションを作成します3.特定したさまざまなエッジケースを処理しますに応じて。

ASP.NETアプリケーションを開発する場合、CQRS / MediatRは価値がありますか?

CQRSは非常に特定の用途があるパターンです。その目的は、CRUDで使用されるようなレコードのモデルを持つ代わりに、クエリとコマンドをモデル化することです。システムがより複雑になると、ビューの要求は1つのレコードまたは少数のレコードを表示するよりも複雑になることが多く、クエリはアプリケーションのニーズをより適切にモデル化できます。同様に、コマンドは、単一のレコードを変更するCRUDではなく、多くのレコードへの変更を表すことができます。マーティン・ファウラーは警告する

他のパターンと同様に、CQRSはある場所では便利ですが、他の場所では役に立ちません。多くのシステムはCRUDメンタルモデルに適合しているため、そのスタイルで行う必要があります。CQRSは関係者全員にとって大きな精神的飛躍であるため、利益にジャンプする価値がない限り、取り組むべきではありません。私はCQRSの成功した使用に出くわしましたが、これまで私が遭遇したケースの大部分はそれほど良くありませんでした。
-マーティン・ファウラーCQRS

したがって、CRUDが適切な場合にアプリケーションを設計するとき、CQRSがあなたの質問に答えるための最初の手段であってはなりません。あなたの質問には、CQRSを使用する理由があることを示すものは何もありませんでした。

MediatRについては、インプロセスメッセージングライブラリであり、リクエスト処理からリクエストを分離することを目的としています。このライブラリを使用するためにデザインを改善するかどうかを再度決定する必要があります。私は個人的にインプロセスメッセージングの擁護者ではありません。疎結合は、メッセージングよりも簡単な方法で実現できるため、そこから始めることをお勧めします。


1
私は100%同意します。CQRSはちょっと誇張されているので、「彼ら」は私が見なかったものを見たと思った。CRUD WebアプリでCQRSの利点を見るのに苦労しているからです。これまでのところ、唯一のシナリオは、私にとって意味のあるCQRS + ESです。
スネービョルン

私の新しい仕事のある人が、MediatRを新しいASP.Netシステムに配置して、それをアーキテクチャとして主張することにしました。彼が行った実装は、DDD、SOLID、DRY、KISSではありません。YAGNIでいっぱいの小さなシステムです。そして、あなたのようなコメントがあなたのコメントを含めてずっと後に始まりました。徐々にアーキテクチャを適合させるためにコードを再現する方法を考えています。私はビジネス層以外のCQRSについても同じ意見を持っていて、そのように考えている経験豊富な開発者が何人かいることを嬉しく思います。
MFedatto

CQRS / MediatRを組み込むという考え方は、多くのYAGNIとKISSの欠如に関連している可能性があることを断言するのは少し皮肉なことです。このようなインターフェイスを実装するすべてのルートアグリゲートで多くのCRUD操作を指定するインターフェイス。多くの場合、これらのメソッドは使用されないか、「実装されていない」例外で埋められます。CQRSはこれらの一般化を使用しないため、必要なもののみを実装できます。
レセアバルモント

@LesairValmontリポジトリはCRUDのみになっています。「多くのCRUD操作を指定する」は、4(または「リスト」で5)のみにしてください。より具体的なクエリアクセスパターンがある場合は、リポジトリインターフェイスに配置しないでください。未使用のリポジトリメソッドの問題に遭遇したことはありません。例を挙げていただけますか?
サミュエル

@Samuel:CQRSがそうであるように、特定のシナリオではリポジトリパターンは完全にうまくいくと思います。実際、大規模なアプリケーションでは、リポジトリパターンが最適な部分と、CQRSの恩恵を受ける部分があります。それは、アプリケーションのその部分に続く哲学(タスクベース(CQRS)対CRUD(レポ)など)、使用されているORM(ある場合)、ドメインのモデリング(例:DDD)。単純なCRUDカタログの場合、CQRSは間違いなく過剰であり、リアルタイムコラボレーション機能(チャットなど)のいずれも使用しません。
レセアバルモント

10

CQRSは、アプリケーション層(またはDDDシステムで最も頻繁に使用される傾向があるため、必要に応じてドメイン)に大量に流出するというよりは、むしろデータ管理のものです。一方、MVCアプリケーションはプレゼンテーションレイヤーアプリケーションであり、CQRSのクエリ/永続性コアからかなり適切に分離する必要があります。

注目に値するもう1つのこと(既定の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をモデリングする彼のアプローチについて、ジミーボガードのNDCプレゼンテーションをご覧になることを強くお勧めします

そうすれば、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はそのパターンを実装するために使用できるライブラリです

MediatRまたはインプロセスメッセージングライブラリなしでCQRSを使用でき、CQRSなしでMediatRを使用できます。

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を使用するときに直面する制限が1つあります

技術的には、元の説明コマンドでは、新しく作成されたオブジェクトから生成されたIDを取得する簡単な方法がないため、愚かである値(void)を返すべきではありません:https : //stackoverflow.com/questions/4361889/how-to-取得-ID-で作成--cqrs適用するときに

そのため、データベースに実行させるのではなく、自分でidを生成する必要があります。


詳細を知りたい場合:https : //cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf


1
データベースで新しいデータを永続化するためのCQRSのコマンドが、新しくデータベースで生成されたIdを返すことができないというあなたの断言に挑戦します。むしろこれは哲学的な問題だと思います。DDDとCQRSの多くはデータの不変性に関するものであることを忘れないでください。考え直せば、データを永続化するだけの行為がデータ変更操作であることに気づき始めます。また、新しいIDだけでなく、デフォルトのデータ、トリガー、およびデータを変更する可能性のあるストアドプロシージャで満たされたフィールドでもあります。
レセアバルモント

確かに、新しいアイテムを引数として使用して、「ItemCreated」のようなイベントを送信できます。単に要求/応答プロトコルを処理し、「true」のCQRSを使用している場合、別のクエリ関数に渡すことができるようにidを事前に知っておく必要があります。まったく問題はありません。多くの場合、CQRSは過剰です。あなたはそれなしで生きることができます。それはあなたのコードを構造化する方法に他なりません、そしてそれはあなたが使用するプロトコルにもほとんど依存します。
コンラッド

また、CQRSなしでデータの不変性を実現できます
Konrad
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.