Contents

FluentValidation 簡介

FluentValidation 簡介

FluentValidation 是一套驗證套件,可以將驗證以口語化的方式呈現,除了靈活性高,易於客製性化,也可以將驗證邏輯與業務邏輯分離,讓各自程式碼更加簡潔。

拿微軟內建的天氣預報,添加一些變化來當範例,輸入某時間段某地的溫度,如:台灣台北下午1點至3點,27度。

1
2
3
4
5
6
7
8
9
public class CreateWeatherForecastRequest
{
    public string Nation { get; set; }
    public string City { get; set; }
    public int TemperatureC { get; set; }
    public string Summary { get; set; }
    public DateTime StartTime { get; set; }
    public DateTime EndTime { get; set; }
}

假設在新增天氣時,為了資料能正確填寫,需要針對輸入的欄位進行檢查,如

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[HttpPost]
public async Task<IActionResult> CreateWeatherForecast(
    [FromBody] CreateWeatherForecastRequest request)
{
    if (string.IsNullOrWhiteSpace(request.Nation)) return BadRequest("國家為必填項目");

    if (request.Nation.Length > 50) return BadRequest("國家字串不能大於50");

    if (request.StartTime > request.EndTime) return BadRequest("開始時間必須在結束時間之前");

    // 其他驗證...
    
    await Task.CompletedTask;
    return Ok();
}

但這樣的驗證,會讓程式碼快速增長,當欄位屬性越來越多時,程式碼很快就變成一長串,妨礙業務邏輯可讀性,所以大部分都會把驗證切開來,我們使用 FluentValidation 改寫看看

安裝 FluentValidation

/static/FluentValidation簡介_c1116638efbd4f849c5fe490409cb0a5/2023-10-04_14-54-22.png

新增 Validator

依照官方範例,要使用 FluentValidation,首先必須建立一個驗證目標的驗證器 Validator,並繼承 AbstractValidator<T>,其中 <T> 就是要驗證的類別。

我們要檢查的對象是 CreateWeatherForecastRequest,所以就建立一個繼承自 AbstractValidator<CreateWeatherForecastRequest> 的驗證器。

1
2
3
4
5
6
7
public class CreateWeatherForecastValidator : AbstractValidator<CreateWeatherForecastRequest>
{
    public CreateWeatherForecastValidator()
    { 
        // 驗證邏輯...
    }
}

撰寫驗證

內建驗證語法

FluentValidation 本身提供許多驗證方式,如

  • 不可為Null:NotNull
  • 不可為Empty:NotEmpty (同時驗證不可為值類型的預設值,例如 int 為 0,陣列等不能為空陣列)。
  • 最小字串長度:MinLength
  • 最大字串長度:MaxLength
  • Email 格式:EmailAddress
  • 數值範圍:InclusiveBetween(必須介於兩者之間)、ExclusiveBetween(不能介於兩者之間)

更多內建驗證方法,可以參考官方文件 Built-in Validators 內容。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class CreateWeatherForecastValidator : AbstractValidator<CreateWeatherForecastRequest>
{
    public CreateWeatherForecastValidator()
    {
		// 內建驗證
        RuleFor(x => x.Nation).NotEmpty().Length(1, 50);
        RuleFor(x => x.City).NotEmpty().Length(1, 50);
        RuleFor(x => x.TemperatureC).InclusiveBetween(-50, 50);
        RuleFor(x => x.Summary).MaximumLength(300);
    }
}

自訂驗證

如果內建的驗證語法無法達到需求,也可以自訂驗證,官方文件 Custom Validators

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class CreateWeatherForecastValidator : AbstractValidator<CreateWeatherForecastRequest>
{
    public CreateWeatherForecastValidator()
    {
        // 內建驗證
        RuleFor(x => x.Nation).NotEmpty().Length(1, 50);
        RuleFor(x => x.City).NotEmpty().Length(1, 50);
        RuleFor(x => x.TemperatureC).InclusiveBetween(-50, 50);
        RuleFor(x => x.Summary).MaximumLength(300);

        // 自訂驗證
        RuleFor(x => x).Must(x => x.StartTime < x.EndTime).WithMessage("開始時間必須在結束時間之前");
		//或
        //RuleFor(x => x).Custom((value, context) =>
        //{
        //    if (value.StartTime >= value.EndTime)
        //    {
        //        context.AddFailure("開始時間必須在結束時間之前");
        //    }
        //});
    }
}

驗證陣列

RuleFor,是針對單一屬性,如: intstring…,如果是屬性是 List,則可以使用 RuleForEach

我們將範例稍加改變測試一下,改成輸入各時段的溫度

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class CreateWeatherForecastRequest
{
    public string Nation { get; set; }
    public string City { get; set; }
    public string Summary { get; set; }
    public List<Temperature> Temperatures { get; set; }
}

public class Temperature
{
    public DateTime StartTime { get; set; }
    public DateTime EndTime { get; set; }
    public int TemperatureC { get; set; }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class CreateWeatherForecastValidator : AbstractValidator<CreateWeatherForecastRequest>
{
    public CreateWeatherForecastValidator()
    {
        // 內建驗證
        RuleFor(x => x.Nation).NotEmpty().Length(1, 50);
        RuleFor(x => x.City).NotEmpty().Length(1, 50);
        RuleFor(x => x.Summary).MaximumLength(300);

        // 自訂驗證
        //RuleFor(x => x).Must(x => x.StartTime < x.EndTime).WithMessage("開始時間必須在結束時間之前");

        // List 項目驗證
        RuleForEach(x => x.Temperatures).ChildRules(t =>
        {
            t.RuleFor(x => x).Must(t => t.StartTime < t.EndTime).WithMessage("開始時間必須在結束時間之前");
            t.RuleFor(x => x.TemperatureC).InclusiveBetween(-50, 50).WithName("溫度");
        });
    }
}

其他使用驗證方式,請查看官方最新文件。

如何使用 Validator

直接使用

建立完驗證器後,下一步便是使用驗證器做驗證,而最簡單的方式就是直接使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[HttpPost]
public async Task<IActionResult> CreateWeatherForecast([FromBody] CreateWeatherForecastRequest request)
{
    // 使用驗證器
    var validator = new CreateWeatherForecastValidator();

    // 驗證
    var validate = await validator.ValidateAsync(request);

    // 驗證結果
    if (validate.IsValid is false) return BadRequest(validate.ToDictionary());

    await Task.CompletedTask;
    return Ok();
}

使用依賴注入(Dependency Injection,DI)

自 NET Core 版本以來,使用 Dependency Injection 來反轉依賴已經很常見了,而官方也有提供Dependency Injection 說明

  1. 自動註冊 Validator
1
2
3
using FluentValidation;

builder.Services.AddValidatorsFromAssemblyContaining<CreateWeatherForecastValidator>();
  1. 使用方法注入服務(也可以選擇建構式注入)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[HttpPost]
public async Task<IActionResult> CreateWeatherForecast(
    [FromBody] CreateWeatherForecastRequest request,
    [FromServices] IValidator<CreateWeatherForecastRequest> validator)
{
    // 驗證
    var validate = await validator.ValidateAsync(request);

    // 驗證結果
    if (validate.IsValid is false) return BadRequest(validate.ToDictionary());

    await Task.CompletedTask;
    return Ok();
}

驗證結果

/static/FluentValidation簡介_c1116638efbd4f849c5fe490409cb0a5/2023-10-04_14-38-55.png

其他

與資料庫互動

有時候我們驗證是需要與資料庫互動,例如驗證 User 的 Email 是否已存在,這時候我們可以在建構式注入要使用的服務。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
public class UserValidator : AbstractValidator<User>
{
    public UserValidator(IUserRepository userRepository)
    {
        RuleFor(x => x.Email).Must(email =>
        {
            return userRepository.IsEmailUnique(email);
        });

		// 非同步方法
        //RuleFor(x => x.Email).MustAsync(async (email, _) =>
        //{
        //    return await userRepository.IsEmailUniqueAsync(email);
        //});
    }
}