趣味アプリのコアロジック部分をCQRSで作った結果、DI Containerに登録する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が辛くなってしまいました。
DI HELL🔥からの脱出
楽にCQRSによる開発を行うための選択肢として
- Assembly単位で自動Injectionが可能なDI Containerに変更する
- CQRS用のライブラリに乗り換える
を検討しました。
DI Containerを変更する
既定のDI ContainerをAutoFac
に切り替えます。
AutoFac
とAutoFac.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.cs
のCreateHostBuilder
にUseServiceProviderFactory
を追加します。
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>(); }); }
使用する際はIMediatR
のPublish
を実行します。
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.DependencyInjection
やMediatR
のコードを読むしかないのかなー。
総評
以下2点を試してみました。
- Assembly単位で自動Injectionが可能なDI Containerに変更する
- CQRS用のライブラリに乗り換える
前者はCompositeを自前実装する必要あり、後者はDummyのCommand/Query HandlerのService挿入に難あり、といった課題がありました。
どちらも解決できる課題なので、どっちでもいいかなーって感じですね。
おわり
最近巷でClean ArchitectureやらCQRSやらDDDやらの話題を多く見かけますね。
一握りのエンジニアが、エンジニア界全体のレベルを底上げするような活動されているのは本当に頭が下がりますね...。MediatR
はIPipelineBehavior<,>
でDecotatorっぽい動きも簡単に実装できるようです。
便利そうだったのでちょっと調べてみたいところ。