web-dev-qa-db-ja.com

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

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

ですから、コントローラーをこれまでに削減すると主張することで、

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

    return View(model);
}
_

これは、薄いコントローラーのガイドラインに完全に適合します。ただし、かなり重要な詳細(エラー処理)は省略しています。

新しいMVCプロジェクトからのデフォルトのLoginアクションを見てみましょう

_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つの可能な解決策は、modelの代わりに_CommandResult<T>_を返し、ポストアクションフィルターでCommandResultを処理することです。議論したように ここ

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

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

ソース

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

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

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

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

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

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

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

19
Snæbjørn

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

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

コマンド:システムの状態を変更するが、値を返さない
-マーティンファウラー CommandQuerySeparation

私の意見では、認証はCQRSの貧弱なドメインです。認証では、強整合性のある同期要求応答フローが必要なので、次のことができます。1.ユーザーの資格情報を確認する2.ユーザーのセッションを作成する3.特定したさまざまなEdgeケースを処理する4.ユーザーをすぐに許可または拒否するに応じて。

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

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

他のパターンと同様に、CQRSは一部の場所では役立ちますが、他の場所では役立ちません。多くのシステムはCRUDメンタルモデルに適合しているため、そのスタイルで実行する必要があります。 CQRSは関係者全員にとって重要な精神的飛躍であり、その利益がジャンプの価値がある場合を除いて、取り組むべきではありません。私はCQRSの使用に成功しましたが、これまでに遭遇したケースの大部分はそれほど良好ではなく、CQRSはソフトウェアシステムを深刻な困難に陥らせる大きな力とみなされています。
-マーティンファウラー [〜#〜] cqrs [〜#〜]

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

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

15
Samuel

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

まだ十分に薄いですが、コードの動作を実際に変更することはありません。処理をserviceメソッドに委譲するだけです。これは、コントローラーのアクションを簡単にダイジェスト化する以外に何の役にも立ちません。

このサービスクラスは、必要に応じてモデル/アプリケーションにロジックを委任する責任があります。実際には、コードを適切に保つためのコントローラーのわずかな拡張にすぎません。サービスメソッドも一般的にかなり短いです。

メディエーターが概念的にそれとは異なる何かを行うかどうかはわかりません。いくつかの基本的なコントローラーロジックをコントローラーから他の場所に移動して処理します。

(私は以前にこのMediatRについて聞いたことがなく、githubページを一目見ただけでは、それが画期的なことを示しているようには見えません-確かにCQRSのようなものではありません-実際、それは単なる別の抽象化レイヤーのように見えます単純に見えるようにすることでコードを複雑にすることができますが、それは私の最初の見方です)

10
jleach

Jimmy Bogardのhttpリクエストのモデリングへのアプローチに関するNDCプレゼンテーションをご覧になることを強くお勧めします 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);
}

お役に立てば幸いです。

5
DavidRogersDev

多くの人々(私もやった)は、パターンをライブラリと混同しています。 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-get-id-in-create-when-applying-cqrs

そのため、データベースに任せる代わりに、自分でIDを生成する必要があります。


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

4
Konrad