菜雞新訓記 (7): 使用 Fluent Validation 來驗證參數吧

Image

這是俺整理公司新訓內容的第七篇文章,目標是紀錄 Fluent Validation 這個好用套件。

FluentValidation 可以幫我們將 Api 傳入的參數的檢查用更口語、更乾淨的方式去處理,除了可以將檢查邏輯拆分成單獨的 Validator 類別,更提供了許多內建的檢查規則和自訂的彈性,相當方便。

並且因為將參數的檢查邏輯整理出去,就可以和 Controller 本身的工作做簡單的拆分,達到關注點分離的目標。

現在就讓我們來認識一下這個好用工具吧!首先要從很久很久以前開始說起…

前言

西元前的某一天,憂心的皇帝在朝堂內繞著柱子走,突然大臣奪門而入。

大臣:「陛下!敵軍已經攻到國境內啦!」

皇帝大驚:『邊境的那些檢查站和關口難道都陷落了嗎?不可能!』

大臣:「陛下,有內奸和敵國勾結,檢查站完全沒檢查!髒資料已經闖進來了!」

皇帝喊了一聲:『怎麼可能!讓朕看看!』就打開 Controller 和前一個版本的 Git Log,這一看差點就昏了過去。

原來 Controller 的舊程式碼就已經很亂了,檢查參數的條件 if/else 和其他呼叫的方法、組裝資料都雜在一起。結果這次專案改動時,某一行就被內奸改壞了,關鍵的參數竟然沒檢查到!

『可,可惡!來人啊,把工程師推出午門斬首!』

「皇上!他已經離職啦!」

皇帝跌坐在地,懊悔地說:『如果當初有好好把檢查參數跟實際組資料的部份都拆開的話,也許就不會這樣了…』

「是啊,如果我們有用 Fluent Validation…!」

專案現況

大臣提到的 FluentValidation 是一套能幫我們把傳入參數的分離出去、用更口語化的方式去撰寫的工具。

……如果當時他們有使用 Fluent Validation 來把驗證的邏輯和規則跟原本很亂的 Controller 切分的話,說不定就能及時發現問題吧,大概。

為了不要步上他們的後塵,就讓我們直接回到本系列的卡牌管理 API 服務來加上這個好用工具吧!

假設我們在新增一張新的卡牌時,會針對裡面的欄位做一連串檢查:

可以看到這個新增卡片的方法中,真正操作的只有最後呼叫相關服務來寫入資料的部份,前面就是針對參數做一整串的 if 檢查。隨著傳入參數要檢查的東西變多,檢查的過程也會越來越大坨。

這時候,只要有了 Fluent Validation,我們就可以在參數檢查上做得更好!

安裝 Fluent Validation

因為我們的示範專案是 .net Core 的 Api,所以讓我們安裝 FluentValidation.AspNetCore

img

註:這包裡面包含了 Fluent Validation 本體和支援 Dotnet Core 的 DI(DependencyInjection)工具。如果習慣將驗證部分拆成其他類別庫,或是不需要 DI 的朋友可以嘗試安裝 Fluent Validation 就好。

撰寫 Validator

要使用 Fluent Validation 來驗證參數,首先我們必須建立一個針對該參數的驗證器(Validator),並繼承 AbstractValidator<T>

其中 <T> 的泛型選擇驗證對象的類別即可,接著就可以在 Validator 的建構式來註冊我們要的驗證邏輯。

現在就讓我們針對前面例子的 CardParameter 來建立 CardParameterValidator 吧:

img

使用內建的驗證規則

現在我們已經針對 CardParameter 建立了驗證器,接著讓我們處理驗證邏輯的部分吧。

當我們要驗證某個欄位的時候,就需要使用 RuleFor 來告訴驗證器現在驗證的欄位,後面再利用 Fluent Validation 提供的各種驗證語法來進行驗證。

例如我們前面的「卡片的攻擊力不應為負數」,也就是 Attack 必須大於等於0,這邊就可以使用 GreaterThanOrEqualTo

如果驗證的對象是個串列之類的,也支援用 RuleForEach

例如我們的卡片可以有多個別名(List<string> Alias 之類的),且裡面每個別名都不可以是空的,就可以:

大部份的狀況下,使用內建的驗證語法就很夠用了。可以參照官方文檔的 Built-in Validators,裡面每一項都有範例和參數說明。

平常比較會遇到的就是 NotNullNotEmpty 和字串長度檢查或是數值大小的。如果是ㄧ些表單需要驗證的話,就還會用到 EmailAddress 等等。

那俺身為一個 懶惰 節能減碳工程師,當然有在 Linqpad 中準備一份範例 才能隨時抄嘛,這邊也會附在文末的附錄

使用 Must 來自訂驗證規則

當然,我們也會遇到內建的驗證規則不夠用的情況。這時候就可以使用 Must() 來傳入自訂的規則,例如:

只要在 Must 裡面指定要驗證的規則就可以囉!

使用 When 來指定驗證條件適用的場景

除了規則可以彈性處理以外,有時候我們也會遇到「有某個條件成立才驗證指定欄位」的情況

假設我們的卡牌又分成「怪獸卡」和「魔法卡」等等,而卡牌本身又有個 int? 的攻擊力欄位

規則又要求:「怪獸卡必須是具有攻擊力的」

雖然直覺上就會想要用 if (卡牌是怪獸卡) 之類的方式去另外做,但就會變得有點兒醜

這時候我們就能用 When 的方式來指定驗證條件的前提:

就像 if 有 else,這邊的 When 也有 Otherwise 來幫忙處理剩下的狀況

假設我們除了怪獸卡以外的卡片,例如魔法卡之類的,都不應該有攻擊力,就可以這樣寫:

使用 WithName 和 WithMessage 來自訂驗證訊息

雖然內建的驗證規則都有提供制式的回傳訊息,例如對 Attack 做 .GreaterThanOrEqualTo(0) 驗證失敗時,會得到「‘Attack’ 必須大於或等於 ‘0’」的訊息

Image

但我們也可以使用 WithMessage 來針對驗證規則指定失敗時的自訂訊息:

這樣在驗證完的 ValidationResult 裡,就會變成我們指定了錯誤訊息了。

那如果我們想用內建的訊息,但又希望「Attack」這個欄位名稱不要顯示出來,而是顯示我們要的「攻擊力」這個名稱呢?這時候就可以使用 WithName()

這樣原本的「‘Attack’ 必須大於或等於 ‘0’」,就會變成「‘攻擊力’ 必須大於或等於 ‘0’」囉!

當然,要把兩個結合起來用也是可以的,只要在字串加上 {PropertyName} 讓他去讀欄位名稱就好囉:

這樣就能拿到「卡片的攻擊力不可為負數」囉!

使用 SetValidator 來指定成員的驗證器

我們前面說了許多針對欄位驗證的工具,但平常我們的類別內的成員有可能會是另一個類別。這時候我們就可以用 SetValidator 來指定該成員的驗證器。

假設說我們的卡片怪獸現在能夠穿戴裝備了,同時我們也有裝備的 Validator:

這時候我們在寫規則的時候就可以:

指定 CascadeMode.Stop 來提早返回

很多時候,我們並不需要全部的規則都驗證完才返回,而是只要檢查清單中的一項不符合,那就直接掰掰。這時我們就可以更改驗證器的 CascadeMode

CascadeMode 原先預設會是 Continue,也就是即使驗證失敗也會繼續執行

例如說它可能一口氣犯了好幾條,就會全部驗證完再一併列出所有驗證失敗的項目:

Image

當我們把驗證器的 CascadeMode 指定為 Stop 之後,犯第一條就會直接原地遣返:

Image

除了指定整個驗證器以外,我們也可以單獨指定某一條規則為天條:

如此一來只要觸犯這條就會直接送客,皆大歡喜。

將前述的規則實作成 Validator

前面我們介紹了如何撰寫一個 Validator,是時候讓我們來處理文章最一開始的範例了!

這邊附一下文章開頭的範例,也就是目前的卡牌系統 Controller 裡的新增卡片方法:

可以看到在範例中,我們針對一張新的卡牌,需要檢查的項目有:

  • 攻擊力不可為負數

  • 生命值不可為負數

  • 使用成本不可為負數

  • 敘述說明必須少於三十字

  • 名稱不可以為空值

  • 名稱必須少於十五字

現在讓我們建立 CardParameter 的 Validator,並用 RuleFor 加上這些規則吧:

可以感覺到比起整串 if/else,這邊整理得更加簡短、也更加口語了。

使用 Validator 進行驗證

現在我們已經準備好了 Validator 了,讓我們回到原本的 Controller 來使用它吧!

首先讓我們把原本的 if/else 部分移除:

接著讓我們直接建立一個驗證器出來使用,並且用 Validate 來驗證參數:

加上驗證器的樣子是像這樣的:

接著我們就可以使用 Validate 回傳的 ValidationResult 來看驗證結果。

先讓我們用 Linqpad 的小範例把 ValidationResult 的內容印出來看看:

Image

可以看到,IsValid 會告訴我們是不是有通過驗證。如果沒有通過驗證的話,Errors 就會有驗證失敗的內容。

現在讓我們加上驗證結果的檢查吧:

現在讓我們來呼叫 API 試試吧!

這邊直接使用先前建置好的 Swagger 頁面來測試,並且故意把攻擊力打成負數:

Image
Image

可以看到回傳的確變成了我們驗證失敗的訊息。

註冊 Validator 來自動進行驗證

不過都已經到了 .net Core 時代,依賴注入 已經是內建的功能下,還要用 new 一個驗證器這種直接依賴的方式還是有點不太舒服……所以 Fluent Validation 也有提供自動驗證的作法!

首先讓我們到熟悉的 Startup.csConfigureServices 進行註冊:

補充:如果不想明確註冊每個類別的 Validator,也可以直接在 AddFluentValidation 的時候,使用反射組件自動註冊的方式來抓該組件底下所有的 Validator,比較不怕出錯、也更方便:

註冊好了之後就讓我們回到 Controller,並大膽地把驗證器相關的部分刪掉吧:

然後讓我們用 Swagger 再試一次看看:

Image

可以看到 Fluent Validation 自動幫我們擋了下來!

小結

當檢查參數的過程越來越冗長,為了做到關注點分離、讓方法本體更專注在流程上的處理,我們會選擇將檢查參數的邏輯拆分出去,例如拆成一個私有的 Function 等等。

這時候 Fluent Validation 就提供了我們一個更棒、更優雅的選擇。

本篇稍微記錄了 Fluent Validation 的基本用法,足夠應付大多數的使用場景。簡單小結如下:

  • 繼承

    來實作我們的驗證器

    • 使用 RuleFor 來針對參數的欄位撰寫規則

    • 有許多內建的規則可以使用;或是使用 Must 來自定規則

    • 使用 When 可以指定規則生效的前提

    • 使用 WithName 可以指定欄位在訊息顯示的名稱

    • 使用 WithMessage 可以自訂驗證失敗時的訊息

    • 使用 SetValidator 可以指定參數某個成員要用的驗證器

    • 加上 CascadeMode.Stop 就可以在驗證失敗時直接跳出

  • 使用 Validator 進行驗證

    • 可以直接建立驗證器來驗證

      • 如:new CardParameterValidator().Validate(parameter);

    • 也可以註冊進行自動驗證

      • StartupConfigureServices 加上 AddFluentValidation 及驗證器的註冊

當然,FluentValidation 還有許多進階的應用可以探索,例如:

  • 使用 FluentValidation.TestHelper 來替驗證器寫單元測試

  • 使用

    RuleSets

    來將驗證器規則分成多個規則集,再針對狀況使用

    • 例如新增和更新的功能共用同個參數的時候,就可以考慮使用規則集來指定各自要驗證哪些規則

  • 需要客製化驗證失敗時回傳的 ViewModel 時,可以將

    包裝到 Attribute 裡進行攔截及驗證

    • 實際案例,敝司對 API 回傳格式有嚴格規範,於是前輩就在 Attribute 裡實例化 Validator 再從 actionContext 抓出參數驗證…

諸如此類,畢竟在參數驗證的路上發生什麼事也不奇怪,請再根據狀況自由地調整吧。

那麼,我們下回見~

參考資料

同系列文章

附錄:FluentValidation 內建驗證方法 小抄

其他文章

Last updated