Contents

CQRS與FluentValidation(二)

CQRS與FluentValidation(二)

呈上篇,第二篇開始,就是開始使用一些套件,來簡化、優化原本的程式碼。預計會使用 FluentValidation、MediatR、AutoMapper,導入並沒有一定順序,都是獨立,也不一定都要使用,完全自由選擇,這邊只是逐篇引入,看一下最後的程式碼大概會長怎樣。

另外這些套件並不會逐一介紹使用方法,避免模糊焦點。

DataAnnotations 與 FluentValidation 各有優缺點,DataAnnotations 不需要額外安裝,且大部分情境都可以解決,而 FluentValidation 能以更優雅的方式將驗證切割開來,口語化驗證,易於客製化等。相關優缺點,放在參考。

環境

  • FluentValidation.DependencyInjectionExtensions

安裝 FluentValidation

FluentValidation.DependencyInjectionExtensions 版本已經包含 FluentValidation,並提供簡單的 DI註冊方式,所以我們直接安裝該套件即可。

/static/CQRS與FluentValidation(二)_5a45dc118d0741eab836ed93ed204d7b/2023-10-13_15-12-11.png

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

因為CommandHandlerValidator都是特定相依物件,並非整個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 格式刪除,查看一下,驗證結果。

/static/CQRS與FluentValidation(二)_5a45dc118d0741eab836ed93ed204d7b/2023-10-03_17-39-46.png

完整程式碼

CommandHandlerCommandValidator放在同一個 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