先日の記事の続きです。
MediatRのIPipelineBehavior<,>
とFluentValidation
というライブラリを使用してQueryのValidationを行う方法です。
目次
- 目次
- サンプルコード
- 元となるQueryの作成
- IPipelineBehaviorによるValidationの追加
- FluentValidationによる"流れるような検証"
- 備考: QueryのPreProcessorBehaviorとPostProcessorBehavior
- おわり
サンプルコード
本エントリで作成したコードはGitHub上にアップロードしてあります。
元となるQueryの作成
元となるCQRSのQueryをMediatRライブラリで作成します。
Validation対象Query
.NET Coreの適当な汎用ホストProjectに下記コマンドでMediatRの汎用ホスト対応版ライブラリを追加します。
dotnet add package MediatR.Extensions.Microsoft.DependencyInjection
続いてCQRSのQuery一式を作成します。
今回はIdを指定してUser
を取得するGetUserByIdQuery
を作成します。
// Request Query public class GetUserByIdQuery : IRequest<User> { public Guid Id { get; set; } } // Response public class User { public Guid Id { get; set; } public string Name { get; set; } } // Query Handler public class GetUserByIdQueryHandler : IRequestHandler<GetUserByIdQuery, User> { private readonly ILogger<GetUserByIdQueryHandler> logger; public GetUserByIdQueryHandler(ILogger<GetUserByIdQueryHandler> logger) { this.logger = logger; } public Task<User> Handle(GetUserByIdQuery request, CancellationToken cancellationToken) { logger.LogInformation("Handling..."); // 本来はUserの検索処理をここに記述するが割愛 var user = new User { Id = request.Id, Name = "ほげほげ" }; return Task.FromResult(user); } }
QueryをIoC Containerへ登録
作成したQueryをIoC Containerに登録します。
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) => { // Mediator services.AddMediatR(typeof(Program).Assembly); // Service Worker services.AddHostedService<Worker>(); }); }
WorkerでQueryを実行してみる
作成したQueryを実行してみます。
public class Worker : BackgroundService { private readonly ILogger<Worker> _logger; private readonly IMediator _mediator; public Worker(ILogger<Worker> logger, IMediator mediator) { _mediator = mediator; _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Query var user = await _mediator.Send(new GetUserByIdQuery() { Id = Guid.NewGuid() }); _logger.LogInformation($"Id : {user.Id.ToString()}"); _logger.LogInformation($"Name : {user.Name?.ToString()}"); } }
ちゃんと動いていますね。
IPipelineBehaviorによるValidationの追加
Validation機能を追加していきます。
IPipelineBehavior<,>の作成
まずはIPiepelineBehavior<,>を追加してみます。
MediatRの公式のドキュメントを参考にValidationBehavior.cs
を作成します。
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { private readonly ILogger<ValidationBehavior<TRequest, TResponse>> logger; public ValidationBehavior(ILogger<ValidationBehavior<TRequest, TResponse>> logger) { this.logger = logger; } public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) { // Pre Processor logger.LogInformation("Handle Start."); // Execute Handler var response = next(); // Post Processor logger.LogInformation("Handle End."); return response; }
IPipelineBehaviorをIoC Containerに登録して実行
作成したValidationBehavior
をIoC Containerに登録します。
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) => { // Mediator services.AddMediatR(typeof(Program).Assembly); // IPipelineBehavior services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); // Service Worker services.AddHostedService<Worker>(); }); }
この状態でアプリを実行すると、こんな感じになります。
ValidationBehavior(Pre Processor)
→ CreateUserQueryHandler
→ ValidationBehavior(Post Processor)
→ Worker
の順に実行されていますね!
※GoFのDecoratorパターン
のようなイメージでしょうか。
特定のQueryのみValidationする
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
だと全てのQueryで本PipelineBehaviorが実行されてしまいます。
特定の型のTRequestのみに絞りたい場合は以下のようにwhere TRequest : hogehoge
を追記することでTRequest
の型を指定することができます。
※ここら辺はC#のGeneric type Constraintの話ですね。
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> where TRequest : GetUserByIdQuery { // 省略... }
ただ、where TRequest : hogehogeQuery
でQueryのValidationを書いていくのは大変そうですね。
FluentValidationによる"流れるような検証"
定番のFluentValidation
とFluentValidation.DependencyInjectionExtensions
を使用して楽しましょう。
FluentValidationライブラリの追加
下記コマンドでProjectにライブラリを追加します。
dotnet add package FluentValidation dotnet add package FluentValidation.DependencyInjectionExtensions
Validationの実装
GetUserByIdQuery
に対する検証処理を記述します。
検証を行うValidatorはAbstractValidator<T>
を継承します。
public class GetUserByIdQueryValidator : AbstractValidator<GetUserByIdQuery> { public GetUserByIdQueryValidator() { RuleFor(x => x.Id).NotEqual(_ => Guid.Empty); } }
GetUserByIdQuery
はId != Guid.Empty
という条件を満たす必要があります。
逆に言うと、Id == Guid.Empty
が真の場合、FluentValidation.ValidationException
という例外が投げられます。
ValidatorをIPipelineBehavior<,>に組込む
先程作成したValidationBehavior
にFluentValidation
を使用して作成したGetUserByIdQueryValidator
を組込みます。
といっても、IEnumerable<IValidator<TRequest>>
をvalidator
としてConstructor Injectionし、request
からValidationContext
を作成してValidationContext
に対応する検証処理を実行させます。
以下がそのコードです。
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { private readonly ILogger<ValidationBehavior<TRequest, TResponse>> logger; private readonly IEnumerable<IValidator<TRequest>> validator; public ValidationBehavior(ILogger<ValidationBehavior<TRequest, TResponse>> logger, IEnumerable<IValidator<TRequest>> validator) { this.logger = logger; this.validator = validator; } public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) { // Pre Proseccor logger.LogInformation("Handle Start."); // Validation var context = new ValidationContext(request); var failures = validator .Select(s => s.Validate(context)) .SelectMany(s => s.Errors) .Where(w => w != null) .ToList(); if (failures.Any()) { throw new ValidationException(failures); } // Execute Handler var response = next(); // Post Proseccor logger.LogInformation("Handle End."); return response; } }
ValidatorをIoC Containerに登録
services.AddValidatorsFromAssembly
でValidatorをIoC Containerに登録します。
Assembly Scanしてくれるのが便利ですね!
public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureServices((hostContext, services) => { services.AddHostedService<Worker>(); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); services.AddValidatorsFromAssembly(typeof(Program).Assembly); services.AddMediatR(typeof(Program).Assembly); });
FluentValidationによるValidation結果
Validationに引っかかるように、Id
にGuid.Empty
を与えQueryを実行します。
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { // Query var response = await _mediator.Send(new GetUserByIdQuery() { Id = Guid.Empty }); _logger.LogInformation($"Id : {response.Id.ToString()}"); _logger.LogInformation($"Name : {response.Name?.ToString()}"); }
実行結果は以下の通りです。
PS C:\Users\piyoe\work\dotnet\CQRSSample\src> dotnet run info: CQRSSample.Services.Validatior.ValidationBehavior[0] Handle Start. Unhandled exception. FluentValidation.ValidationException: Validation failed: -- Id: 'Id' は '00000000-0000-0000-0000-000000000000' と等しくなってはなりません。 at CQRSSample.Services.Validatior.ValidationBehavior
2.Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate
1 next) in C:\Users\piyoe\work\dotnet\CQRSSample\src\Services\Validatior\ValidationBehavior.cs:line 39 at MediatR.Internal.RequestHandlerWrapperImpl2.<>c__DisplayClass0_1.<Handle>b__2() at MediatR.Internal.RequestHandlerWrapperImpl
2.Handle(IRequest1 request, CancellationToken cancellationToken, ServiceFactory serviceFactory) at MediatR.Mediator.Send[TResponse](IRequest
1 request, CancellationToken cancellationToken) at CQRSSample.Worker.ExecuteAsync(CancellationToken stoppingToken) in C:\Users\piyoe\work\dotnet\CQRSSample\src\Worker.cs:line 28
at Microsoft.Extensions.Hosting.Internal.Host.StartAsync(CancellationToken cancellationToken) at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token) at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.RunAsync(IHost host, CancellationToken token) at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host) at CQRSSample.Program.Main(String[] args) in C:\Users\piyoe\work\dotnet\CQRSSample\src\Program.cs:line 13 PS C:\Users\piyoe\work\dotnet\CQRSSample\src>
Validatorが動いてくれました!
備考: QueryのPreProcessorBehaviorとPostProcessorBehavior
IPipelineBehavior<,>はPre/Post両方いっぺんに書けますが、Pre/Postをそれぞれ分離して記述することも可能なようです。
Pre ProcessorはIRequestPreProcessor<T>
Post ProcessorはIRequestPostProcessor<T>
というInterfaceが用意されています。
以下にサンプルを掲載します。
// Pre Processor public class ValidationRequestPreProcessorBehavior : IRequestPreProcessor<GetUserByIdQuery> { public Task Process(GetUserByIdQuery request, CancellationToken cancellationToken) { return Task.CompletedTask; } } // Post Processotr public class ValidationRequestPostProcessorBehavior : IRequestPostProcessor<GetUserByIdQuery, User> { public Task Process(GetUserByIdQuery request, User response, CancellationToken cancellationToken) { return Task.CompletedTask; } }
おわり
IPipelineBehavior<,>の基本的な使い方でした。
Validation用途だけでなくQueryのPre Processor
にキャッシュ処理を追加したり、Query毎にLogを吐くといった横断的関心事を吸収したり、ちょっとした用途に使えるのが好みです。
例外発生時に日本語でエラーが出るのもいい感じなポイントですね。
以上