CQRS與FluentValidation(二)
呈上篇,第二篇開始,就是開始使用一些套件,來簡化、優化原本的程式碼。預計會使用 FluentValidation、MediatR、AutoMapper,導入並沒有一定順序,都是獨立,也不一定都要使用,完全自由選擇,這邊只是逐篇引入,看一下最後的程式碼大概會長怎樣。
另外這些套件並不會逐一介紹使用方法,避免模糊焦點。
DataAnnotations 與 FluentValidation 各有優缺點,DataAnnotations 不需要額外安裝,且大部分情境都可以解決,而 FluentValidation 能以更優雅的方式將驗證切割開來,口語化驗證,易於客製化等。相關優缺點,放在參考。
環境
- FluentValidation.DependencyInjectionExtensions
安裝 FluentValidation
FluentValidation.DependencyInjectionExtensions
版本已經包含 FluentValidation,並提供簡單的 DI註冊方式,所以我們直接安裝該套件即可。
Application 層建立 Validator 驗證
FluentValidation 驗證寫法不是本篇討論重點,可以參考官網,或 FluentValidation簡介
1
2
3
4
5
6
7
8
9
10
|
public class CreateWeatherForecastValidator : AbstractValidator<CreateWeatherForecastCommand>
{
public CreateWeatherForecastValidator()
{
RuleFor(x => x.Nation).NotEmpty().WithName("國家");
RuleFor(x => x.City).NotEmpty().WithName("城市");
RuleFor(x => x.TemperatureC).InclusiveBetween(-50, 50).WithName("溫度");
RuleFor(x => x.Summary).MaximumLength(300);
}
}
|
註冊Validator
1
2
3
|
using FluentValidation;
builder.Services.AddValidatorsFromAssemblyContaining<CreateWeatherForecastValidator>();
|
Controller 使用 Validator
因為CommandHandler
和Validator
都是特定相依物件,並非整個Controller
都使用,所以我選擇了方法注入(Method Injection),當然也可以選擇其他注入方式,但要留意建構子過於龐大的異樣。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
[HttpPost]
public async Task<IActionResult> CreateWeatherForecast(
[FromBody] CreateWeatherForecastRequest request,
[FromServices] CreateWeatherForecastCommandHandler handler,
[FromServices] IValidator<CreateWeatherForecastCommand> validator)
{
var command = new CreateWeatherForecastCommand
{
Nation = request.Nation,
City = request.City,
Date = request.Date,
TemperatureC = request.TemperatureC,
Summary = request.Summary,
};
var validate = await validator.ValidateAsync(command);
if (validate.IsValid is false) return BadRequest(validate.ToDictionary());
var result = await handler.Execute(command);
return result ? Ok() : BadRequest();
}
|
查看驗證結果
先將 CreateWeatherForecastRequest
內的 DataAnnotations 格式刪除,查看一下,驗證結果。
完整程式碼
將 CommandHandler
、Command
、Validator
放在同一個 Class 純屬個人偏好,我覺得這樣不用寫三個類別,同時可以一目了然,而拆分三個 Class 當然是比較常見與正規的作法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
public class CreateWeatherForecastCommandHandler : ICommandHandler<CreateWeatherForecastCommand>
{
private readonly IWeatherForecastRepository _weatherForecastRepository;
private readonly IUnitOfWork _unitOfWork;
public CreateWeatherForecastCommandHandler(IWeatherForecastRepository weatherForecastRepository, IUnitOfWork unitOfWork)
{
_weatherForecastRepository = weatherForecastRepository ?? throw new ArgumentNullException(nameof(weatherForecastRepository));
_unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
}
public async Task<bool> Execute(CreateWeatherForecastCommand command)
{
WeatherForecast weatherForecast = new(command.Nation, command.City, command.Date, command.TemperatureC)
{
Summary = command.Summary,
};
_weatherForecastRepository.AddWeatherForecast(weatherForecast);
await _unitOfWork.SaveChangesAsync();
return true;
}
}
public class CreateWeatherForecastCommand : ICommand
{
public string Nation { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
}
public class CreateWeatherForecastValidator : AbstractValidator<CreateWeatherForecastCommand>
{
public CreateWeatherForecastValidator()
{
RuleFor(x => x.Nation).NotEmpty().WithName("國家");
RuleFor(x => x.City).NotEmpty().WithName("城市");
RuleFor(x => x.TemperatureC).InclusiveBetween(-50, 50).WithName("溫度");
RuleFor(x => x.Summary).MaximumLength(300);
}
}
|
參考
FluentValidation
https://docs.fluentvalidation.net/en/latest/
Fluent Validation or Data Annotations in C# – Which Is Better?
https://www.bytehide.com/blog/fluent-validation-vs-data-annotations-csharp