ASP.NET CoreにおけるCQRSとMediatR:「数千行」に肥大化したServiceへの解決策

Development tutorial - IT technology blog
Development tutorial - IT technology blog

なぜ従来のコーディング手法を変える必要があるのか?

6ヶ月前、私はかなり大規模な物流管理Web APIプロジェクトに参加しました。チームは5人のデベロッパーで構成され、全員が伝統的な3層(3-tier)アーキテクチャを採用していました。しかし、わずか3ヶ月後、Serviceファイルは2,000行を超える「超高層ビル」へと変貌してしまったのです。

注文更新部分のわずかなロジックを修正するたびに、複雑に絡み合った数十もの関数の中を「泳ぐ」必要がありました。これは他の機能への連鎖的なバグを引き起こすリスクが非常に高く、非常に危険な状態でした。そこで私は、CQRS (Command Query Responsibility Segregation)MediatR ライブラリの導入を提案しました。

結果は劇的でした。運用開始から6ヶ月後、チームのデバッグ時間は約30%短縮され、Unit Testの作成も格段に楽になりました。もしあなたが肥大化したControllerやServiceに頭を悩ませているなら、この記事がその解決策になるはずです。

5分で試せるクイックスタート (Quick Start)

理屈を並べるよりも、まずは実際に手を動かして、その仕組みを確認してみましょう。

ステップ1:ライブラリのインストール

ASP.NET Coreプロジェクトのターミナルを開き、以下のコマンドを実行してMediatRパッケージを追加します。

dotnet add package MediatR

ステップ2:CommandとHandlerの作成

新しく製品を作成する場合を想定してみましょう。Serviceにロジックを詰め込む代わりに、独立した CreateProductCommand クラスを作成します。

public record CreateProductCommand(string Name, decimal Price) : IRequest<int>;

public class CreateProductHandler : IRequestHandler<CreateProductCommand, int>
{
    public async Task<int> Handle(CreateProductCommand request, CancellationToken cancellationToken)
    {
        // データベースへの保存をシミュレートし、製品IDを返す
        Console.WriteLine($"製品を作成中: {request.Name}");
        return await Task.FromResult(new Random().Next(1, 1000));
    }
}

ステップ3:MediatRの登録

Program.cs ファイルに、MediatRがHandlerを自動的にスキャンするためのコードを1行追加します。

builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));

ステップ4:Controllerからの呼び出し

[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
    private readonly IMediator _mediator;
    public ProductsController(IMediator mediator) => _mediator = mediator;

    [HttpPost]
    public async Task<IActionResult> Create(CreateProductCommand command)
    {
        var id = await _mediator.Send(command);
        return Ok(id);
    }
}

CQRSとMediatRの本質

1. CQRS – 分割して統治せよ

CQRSは魔法のようなフレームワークではなく、単純な設計思想です。読み取りと書き込みで同じモデルを共有する代わりに、それらを2つの独立した経路に分離します。

  • Commands: データを変更するアクション(追加、修正、削除)。通常、IDまたは成功ステータスのみを返します。
  • Queries: データを取得するアクション(詳細取得、リスト取得)。これらの関数は、データベースの内容を一切変更してはいけません。

2. MediatR – アプリケーション内の「運び屋」

MediatRは仲介者(In-process Messaging)の役割を果たします。Controllerはもはや、どのServiceがロジックを処理するかを気にする必要はありません。リクエスト(Request)を送信するだけで、MediatRが適切な「宛先」(Handler)を自動的に見つけて実行してくれます。この手法により、大量のServiceをInjectすることでControllerのConstructorが肥大化するのを防ぐことができます。

応用:バリデーションとロギングの集中管理

実際のプロジェクトでは、Handler内にバリデーションのコードを直接書くことはありません。MediatRには Pipeline Behavior という強力な機能があり、Application層向けのMiddlewareのように動作します。

例えば、すべてのリクエストのパフォーマンスを測定するために、以下のようなLogging Behaviorを作成できます。

Share: