0%

[DotnetCore]FluentValidation再續前緣

前情提要

筆者最近在公司負責開發一個全新的API對外系統,因沒有舊系統包袱,可以設計統一Response,統一的ExceptionHandlerMiddleware,只多不嫌少的各種CustomizeException等等,然而接受客戶端的Request時,勢必要進行Validation作業,可以重捨FluentValidation的懷抱了,此篇就以筆者遇到的情境及解法介紹為主,讓我們看下去吧。

先列一下筆者這邊使用的套件的版本號

  • Fluent Validation 11.7.1
  • Fluent.Validation.AspNetCore 11.3.0

再來敘述一下筆者這邊的環境遇到的問題,因EncryptKey放在資料庫的因素,需要Validator中連線資料庫並取得EncryptKey,這樣才能提早驗證客戶端傳來的加密字串是否可以成功解密,就不用等到商業邏輯處理時才爆錯,DB Access相關程式碼會使用到Async方法,參考https://docs.fluentvalidation.net/en/latest/async.html此篇中的說明,不適用ASP.Net CoreValidaion Pipeline,等於沒有使用到AutoValidation的好處,自己需要爾外透過IVlidator<T>.Validate這種手動驗證的方式進行驗證,也因此無法透過設定ConfigureApiBehaviorOptions來指定InvalidModelStateResponseFactory的統一Response,稍嫌可惜,筆者說的都在這篇文章上https://medium.com/codex/custom-error-responses-with-asp-net-core-6-web-api-and-fluentvalidation-888a3b16c80f

驗證思路

筆者想像中,在每一個Action開頭中多了一個Validate的作業,只是型別不一樣,但行為是一致的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class OrderController: ControllerBase
{
private readonly IValidator<OrderRequestDto> _validator;
public class OrderController(IValidator<OrderRequestDto> validator)
{
_validator = validtor;
}

public async Task<IActionResult> Create(OrderRequestDto request)
{
var validateResult = await _validator.Validate(request);
if(!validateResult.IsValid())
{
return BadRequest(new CommonResponse(){ Message = validateResult.Errors.Select(x => x.ErrorMessage).Aggregate((x, y) => $"{x} | {y}")});
}
// 以下省略
// Call Business Logic Service
}
}

寫到這邊筆者想起剛開始撰寫MVC5的時候,老是想用上ActionFilter,盡量讓統一行為在ActionFilter中進行,達到DRY的精神。但不外乎常用到的情境就是ModelState的驗證,不過時回應統一的Response,這次情境只是多了一個型別的宣告,那就來用各Generic Action Filter吧,接著就誕生ValidationFilterAttribute

實作IAsyncActionFilter

因為非同步的行為,筆者這邊採用IAsyncActionFilter

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
public class ValidationFilterAttribute<T> : IAsyncActionFilter
where T : class
{
private readonly IValidator<T> _validator;

public ValidationFilterAttribute(IValidator<T> validator)
{
_validator = validator;
}
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var param = context.ActionArguments.First(p => p.Value is T);
var validateResult = await _validator.ValidateAsync((T)param.Value);
if (!validateResult.IsValid)
{
context.Result = new BadRequestObjectResult(new CommonResponse()
{
Message = validateResult.Errors.Select(x => x.ErrorMessage).Aggregate((x, y) => $"{x} | {y}")
});
}
else
{
await next();
}
}
}

註冊IAsyncActionFilter

註冊方式是滿簡單的,筆者這邊就透過dotnet core預設的DI,註冊為Scope類型服務

1
services.AddScoped(typeof(ValidationFilterAttribute<>));

套用IAsyncActionFilter

撰寫完畢,註冊完成後,將套用在想套用的Action上吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class OrderController : ControllerBase
{
private readonly IOrderService _orderService;
private readonly IMapper _mapper;

public OrderController(IOrderService orderService, IMapper mapper)
{
_orderService = orderService;
_mapper = mapper;
}

/// <summary>
/// 建立訂單作業
/// </summary>
/// <param name="orderRedeemRequestDto"></param>
/// <returns></returns>
[ServiceFilter(typeof(ValidationFilterAttribute<OrderRequestDto>))]
[HttpPost("create")]
public async Task<IActionResult> Redeem(OrderRequestDto orderRequestDto)
{
return Ok(await _orderService.Create(orderRequestDto););
}
}

設定CascadeMode

依照Validate的角度來說,有前後相依性的,以筆者這邊的例子來說,加密字串為空的,根本不需要驗證解密是否可以成功,且解密驗證關係到連線資料庫,若能夠於加密字串為空這個Validation Rule驗證不過時,就不用往下驗證,對於執行效能來說是好的,FluentValidation也有提供其方便設定的方式,依照版本參考對應的設定,筆者這邊的版本11來說,分為

  • Global Level
  • Class Level
  • Rule Level
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class OrderRequestValidator : AbstractValidator<OrderRequestDto>
{
private readonly IOrderRepository _orderRepo;
private readonly IEncryptService _encryptService;
public OrderRedeemRequestValidator(IOrderRepository orderRepo, IEncryptService encryptService)
{
_orderRepo= orderRepo;
_encryptService= encryptService;

this.ClassLevelCascadeMode = CascadeMode.Stop;

// 以下Rule設定省略
}
}

結論

因目前開發的是一個全新的API站台對外系統,比較沒有舊系統的包袱,可以照比較正統的方式設計其系統,各司其職的概念,尚未包含商業邏輯簡單驗證還是交給Fluent Validation,擋在前面,到Service層就專心處理商業邏輯就好,職責明確之外,也少了很多code塞在同一個地方窘境,偵錯起來也不會亂,一舉數得,還不行動嗎?

參考