🐥note.

小鳥とMicrosoft <3 なエンジニアの技術Blog📚

CQRSで実装したときDI Containerに登録するInterface/Serviceが大量すぎて辛かった話

趣味アプリのコアロジック部分をCQRSで作った結果、DI Containerに登録するInterface/Serviceの数が増えすぎてしまい辛くなってしまいました。

f:id:piyo_esq:20191222165804p:plain
DI Containerへ登録する大量のCommand/QueryのInterfaceやService

次にCQRSでアプリを作る際はもっと楽に開発したいので、そこらへんのプラクティスを試行した結果をまとめます。

目次

CQRSの実装

CQRSは下記基底Interfaceを継承する形で作成しました。
実装クラスの入出力パラメータはIRequest, IResponse<T>を継承する形です。

Command/Query基底Interface

// Interface of Command
public interface ICommand { }

// Interface of Request
public interface IRequest { }

// Interface of Response
public interface IResponse<T> where T : IRequest { }

// Base Interface of Command
public interface ICommandHandler<T> where T : ICommand
{
    void Handle(T input);
}

// Base Interface of Query
public interface IQueryHandler<in TRequest, out TResponse>
    where TRequest : IRequest
    where TResponse : IResponse<TRequest>
{
        TResponse Handle(TRequest input);
}

Commandの例

// InputParameter of Hoge Command
public class HogeCommand : ICommand
{
    public string Message { get; set; }
}

// Interface of Hoge Command
public interface IHogeCommandHandler : ICommandHandler<HogeCommand> { }

// Implementation of Hoge Command Class
public class HogeCommandHandler : IHogeCommandHandler
{
    public void Handle(HogeCommand input)
    {
        // Commandの実装
    }
}

QueryHandlerの例

// Request of Foo Query
public class FooQueryRequest : IRequest
{
    public int Id { get; set; }
}

// Response of Foo Queyr
public class FooQueryResponse : IResponse<FooQueryRequest>
{
    public string Message { get; set; }
}

// Interface of Foo Query
public interface IFooQueryHandler : IQueryHandler<FooQueryRequest, FooQueryResponse> { }

// Impllementation of Foo Query
public class FooQueryHandler : IFooQueryHandler
{
    public FooQueryResponse Handle(FooQueryRequest input)
    {
        // Queryの実装
        return new FooQueryResponse();
    }
}

Dependency Injection HELL🔥

この方法だとコアロジックが膨らむにつれInterfaceと実装クラスの数が多くなりすぎてしまい、DIが辛くなってしまいました。

f:id:piyo_esq:20191222165804p:plain
DI Containerへ登録する大量のCommand/QueryのInterfaceやService

DI HELL🔥からの脱出

楽にCQRSによる開発を行うための選択肢として

  • Assembly単位で自動Injectionが可能なDI Containerに変更する
  • CQRS用のライブラリに乗り換える

を検討しました。

DI Containerを変更する

既定のDI ContainerをAutoFacに切り替えます。

AutoFacAutoFac.Extensions.DependencyInjectionをプロジェクトに追加します。
※プロジェクトは.NET Core 3.1 LTSの汎用ホストを想定

dotnet new worker -o HogeHoge
cd HogeHoge
dotnet add package AutoFac
dotnet add package AutoFac.Extensions.DependencyInjection

Startup.csCreateHostBuilderUseServiceProviderFactoryを追加します。
AutoFacServiceProviderFactoryを作成しAction<ContainerBuilder>内でAssembly単位でInterfaceと実装クラスを登録します。

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .UseServiceProviderFactory(new AutofacServiceProviderFactory(builder =>
        {
            builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()).AsImplementedInterfaces();
        }));

これだけです。
手動でInterfaceと実装クラスの組合わせを登録する手間が省けました。

CQRS用のライブラリに乗り換える

次にCQRS用のライブラリを試してみましょう。

Commandのevent publishingに対応しているMediatR使ってみましょうか。

MediatRのインストール

以下のコマンド汎用ホストに対応したMediatR拡張機能をインストールします。
※プロジェクトは.NET Core 3.1 LTSの汎用ホストを想定

dotnet new worker -o FugaFuga
cd FugaFuga
MediatR.Extensions.Microsoft.DependencyInjection

MediatRを使ったCommandの実装

入力パラメータはINotificationを継承させ、Commandの実装クラスはINotificationHandler<T>を継承させます。 記述量がぐっと減っていますね。

// InputParameter of Hoge Command
public class HogeCommand : INotification
{
    public string Message { get; set; }
}

// Implementation of Hoge Command Class
public class HogeCommandHandler : INotificationHandler<HogeCommand>
{
    public Task Handle(HogeCommand notification, CancellationToken cancellationToken)
    {
        // Commandの実装
    }
}

MediatRを使ったQueryの実装

QueryのRequestとResponseを定義し実装クラスでIRequestHandler<Request, Response>を継承します。

// Request of Foo Query
// `public class FooQueryRequest : IRequest` も 可
public class FooQueryRequest : IRequest<FooQueryResponse>
{
    public Guid Id { get; set; }
}

// Response of Foo Query
public class FooQueryResponse
{
    public string Message {get; set;}
}

// Implementation of Foo Query Class
public class FooQueryHandler : IRequestHandler<FooQueryRequest, FooQueryResponse>
{
    public Task<Foo> Handle(FooQueryRequest request, CancellationToken cancellationToken)
    {
        // Queryの実装
    }
}

MediatRのCommand/Queryを実行する

Command/Queryを使用するためにDI Containerに登録します。
※.NET Coreの汎用ホストを想定しています。

using MediatR;
public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostContext, services) =>
            {
                // アセンブリ単位で登録
                services.AddMediatR(typeof(Program).Assembly);
                // Service Worker
                services.AddHostedService<Worker>();
            });
}

使用する際はIMediatRPublishを実行します。

private readonly IMediator mediatr;
public Worker(IMediator mediatr)
{
    this.mediatr = mediatr;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    // Command
    await mediatr.Publish(new HogeCommand { Message = "hoge hoge" });
    // QUery
    FooQueryResponse result = await mediatr.Send(new FooQueryResuest { Id = Guid.Empty });
}

かなり楽ですね。

CommandのPublishは同じINotificationHandler<T>を実装している全てのCommandHandlerにevent publishingできるので大変便利ですね。
※所謂Composite

MediatRだとDIの上書きができない…

MediatRいい感じじゃないですか!と思いきや、ちょっと困る挙動を見つけてしまいました。

.NET Core 汎用ホストのデフォルトで使用するMicrosoft.Extensions.DependencyInjectionでは後に登録したServiceが優先されます。

.ConfigureServices((hostContext, services) =>
  {
      // こちらは使用されない
      services.AddSingleton<IUserRepository, UserRepository>();

      // 同じInterfaceの登録を行う場合後に登録された方が優先される!
      services.AddSingleton<IUserRepository, DummyUserRepository>();
  }

MediatRの場合、後に登録したServiceが優先されません。
これではダミーのCommand/Query ServiceをDIすることができないではないか…。
MediatR.Extensions.Microsoft.DependencyInjectionMediatRのコードを読むしかないのかなー。

総評

以下2点を試してみました。

  • Assembly単位で自動Injectionが可能なDI Containerに変更する
  • CQRS用のライブラリに乗り換える

前者はCompositeを自前実装する必要あり、後者はDummyのCommand/Query HandlerのService挿入に難あり、といった課題がありました。
どちらも解決できる課題なので、どっちでもいいかなーって感じですね。

おわり

  • 最近巷でClean ArchitectureやらCQRSやらDDDやらの話題を多く見かけますね。
    一握りのエンジニアが、エンジニア界全体のレベルを底上げするような活動されているのは本当に頭が下がりますね...。

  • MediatRIPipelineBehavior<,>でDecotatorっぽい動きも簡単に実装できるようです。
    便利そうだったのでちょっと調べてみたいところ。