Last updated
Last updated
這是俺整理公司新訓內容的第七篇文章,目標是紀錄 Fluent Validation 這個好用套件。
FluentValidation 可以幫我們將 Api 傳入的參數的檢查用更口語、更乾淨的方式去處理,除了可以將檢查邏輯拆分成單獨的 Validator 類別,更提供了許多內建的檢查規則和自訂的彈性,相當方便。
並且因為將參數的檢查邏輯整理出去,就可以和 Controller 本身的工作做簡單的拆分,達到關注點分離的目標。
現在就讓我們來認識一下這個好用工具吧!首先要從很久很久以前開始說起…
西元前的某一天,憂心的皇帝在朝堂內繞著柱子走,突然大臣奪門而入。
大臣:「陛下!敵軍已經攻到國境內啦!」
皇帝大驚:『邊境的那些檢查站和關口難道都陷落了嗎?不可能!』
大臣:「陛下,有內奸和敵國勾結,檢查站完全沒檢查!髒資料已經闖進來了!」
皇帝喊了一聲:『怎麼可能!讓朕看看!』就打開 Controller 和前一個版本的 Git Log,這一看差點就昏了過去。
原來 Controller 的舊程式碼就已經很亂了,檢查參數的條件 if/else 和其他呼叫的方法、組裝資料都雜在一起。結果這次專案改動時,某一行就被內奸改壞了,關鍵的參數竟然沒檢查到!
『可,可惡!來人啊,把工程師推出午門斬首!』
「皇上!他已經離職啦!」
皇帝跌坐在地,懊悔地說:『如果當初有好好把檢查參數跟實際組資料的部份都拆開的話,也許就不會這樣了…』
「是啊,如果我們有用 Fluent Validation…!」
……如果當時他們有使用 Fluent Validation 來把驗證的邏輯和規則跟原本很亂的 Controller 切分的話,說不定就能及時發現問題吧,大概。
為了不要步上他們的後塵,就讓我們直接回到本系列的卡牌管理 API 服務來加上這個好用工具吧!
假設我們在新增一張新的卡牌時,會針對裡面的欄位做一連串檢查:
可以看到這個新增卡片的方法中,真正操作的只有最後呼叫相關服務來寫入資料的部份,前面就是針對參數做一整串的 if 檢查。隨著傳入參數要檢查的東西變多,檢查的過程也會越來越大坨。
這時候,只要有了 Fluent Validation,我們就可以在參數檢查上做得更好!
因為我們的示範專案是 .net Core 的 Api,所以讓我們安裝 FluentValidation.AspNetCore
註:這包裡面包含了 Fluent Validation 本體和支援 Dotnet Core 的 DI(DependencyInjection)工具。如果習慣將驗證部分拆成其他類別庫,或是不需要 DI 的朋友可以嘗試安裝 Fluent Validation 就好。
要使用 Fluent Validation 來驗證參數,首先我們必須建立一個針對該參數的驗證器(Validator),並繼承 AbstractValidator<T>
。
其中 <T>
的泛型選擇驗證對象的類別即可,接著就可以在 Validator 的建構式來註冊我們要的驗證邏輯。
現在就讓我們針對前面例子的 CardParameter
來建立 CardParameterValidator
吧:
現在我們已經針對 CardParameter
建立了驗證器,接著讓我們處理驗證邏輯的部分吧。
當我們要驗證某個欄位的時候,就需要使用 RuleFor
來告訴驗證器現在驗證的欄位,後面再利用 Fluent Validation 提供的各種驗證語法來進行驗證。
例如我們前面的「卡片的攻擊力不應為負數」,也就是 Attack 必須大於等於0,這邊就可以使用 GreaterThanOrEqualTo
:
如果驗證的對象是個串列之類的,也支援用 RuleForEach
例如我們的卡片可以有多個別名(List<string> Alias
之類的),且裡面每個別名都不可以是空的,就可以:
平常比較會遇到的就是 NotNull
、NotEmpty
和字串長度檢查或是數值大小的。如果是ㄧ些表單需要驗證的話,就還會用到 EmailAddress
等等。
當然,我們也會遇到內建的驗證規則不夠用的情況。這時候就可以使用 Must()
來傳入自訂的規則,例如:
只要在 Must
裡面指定要驗證的規則就可以囉!
除了規則可以彈性處理以外,有時候我們也會遇到「有某個條件成立才驗證指定欄位」的情況
假設我們的卡牌又分成「怪獸卡」和「魔法卡」等等,而卡牌本身又有個 int? 的攻擊力欄位
規則又要求:「怪獸卡必須是具有攻擊力的」
雖然直覺上就會想要用 if (卡牌是怪獸卡)
之類的方式去另外做,但就會變得有點兒醜
這時候我們就能用 When
的方式來指定驗證條件的前提:
就像 if 有 else,這邊的 When 也有 Otherwise 來幫忙處理剩下的狀況
假設我們除了怪獸卡以外的卡片,例如魔法卡之類的,都不應該有攻擊力,就可以這樣寫:
雖然內建的驗證規則都有提供制式的回傳訊息,例如對 Attack 做 .GreaterThanOrEqualTo(0)
驗證失敗時,會得到「‘Attack’ 必須大於或等於 ‘0’」的訊息
但我們也可以使用 WithMessage
來針對驗證規則指定失敗時的自訂訊息:
這樣在驗證完的 ValidationResult 裡,就會變成我們指定了錯誤訊息了。
那如果我們想用內建的訊息,但又希望「Attack」這個欄位名稱不要顯示出來,而是顯示我們要的「攻擊力」這個名稱呢?這時候就可以使用 WithName()
:
這樣原本的「‘Attack’ 必須大於或等於 ‘0’」,就會變成「‘攻擊力’ 必須大於或等於 ‘0’」囉!
當然,要把兩個結合起來用也是可以的,只要在字串加上 {PropertyName}
讓他去讀欄位名稱就好囉:
這樣就能拿到「卡片的攻擊力不可為負數」囉!
我們前面說了許多針對欄位驗證的工具,但平常我們的類別內的成員有可能會是另一個類別。這時候我們就可以用 SetValidator
來指定該成員的驗證器。
假設說我們的卡片怪獸現在能夠穿戴裝備了,同時我們也有裝備的 Validator:
這時候我們在寫規則的時候就可以:
很多時候,我們並不需要全部的規則都驗證完才返回,而是只要檢查清單中的一項不符合,那就直接掰掰。這時我們就可以更改驗證器的 CascadeMode
:
CascadeMode 原先預設會是 Continue
,也就是即使驗證失敗也會繼續執行
例如說它可能一口氣犯了好幾條,就會全部驗證完再一併列出所有驗證失敗的項目:
當我們把驗證器的 CascadeMode 指定為 Stop
之後,犯第一條就會直接原地遣返:
除了指定整個驗證器以外,我們也可以單獨指定某一條規則為天條:
如此一來只要觸犯這條就會直接送客,皆大歡喜。
前面我們介紹了如何撰寫一個 Validator,是時候讓我們來處理文章最一開始的範例了!
這邊附一下文章開頭的範例,也就是目前的卡牌系統 Controller 裡的新增卡片方法:
可以看到在範例中,我們針對一張新的卡牌,需要檢查的項目有:
攻擊力不可為負數
生命值不可為負數
使用成本不可為負數
敘述說明必須少於三十字
名稱不可以為空值
名稱必須少於十五字
現在讓我們建立 CardParameter 的 Validator,並用 RuleFor 加上這些規則吧:
可以感覺到比起整串 if/else,這邊整理得更加簡短、也更加口語了。
現在我們已經準備好了 Validator 了,讓我們回到原本的 Controller 來使用它吧!
首先讓我們把原本的 if/else 部分移除:
接著讓我們直接建立一個驗證器出來使用,並且用 Validate
來驗證參數:
加上驗證器的樣子是像這樣的:
接著我們就可以使用 Validate
回傳的 ValidationResult
來看驗證結果。
先讓我們用 Linqpad 的小範例把 ValidationResult
的內容印出來看看:
可以看到,IsValid
會告訴我們是不是有通過驗證。如果沒有通過驗證的話,Errors
就會有驗證失敗的內容。
現在讓我們加上驗證結果的檢查吧:
現在讓我們來呼叫 API 試試吧!
可以看到回傳的確變成了我們驗證失敗的訊息。
首先讓我們到熟悉的 Startup.cs
→ ConfigureServices
進行註冊:
補充:如果不想明確註冊每個類別的 Validator,也可以直接在 AddFluentValidation
的時候,使用反射組件自動註冊的方式來抓該組件底下所有的 Validator,比較不怕出錯、也更方便:
註冊好了之後就讓我們回到 Controller,並大膽地把驗證器相關的部分刪掉吧:
然後讓我們用 Swagger 再試一次看看:
可以看到 Fluent Validation 自動幫我們擋了下來!
當檢查參數的過程越來越冗長,為了做到關注點分離、讓方法本體更專注在流程上的處理,我們會選擇將檢查參數的邏輯拆分出去,例如拆成一個私有的 Function 等等。
這時候 Fluent Validation 就提供了我們一個更棒、更優雅的選擇。
本篇稍微記錄了 Fluent Validation 的基本用法,足夠應付大多數的使用場景。簡單小結如下:
繼承
來實作我們的驗證器
使用 RuleFor
來針對參數的欄位撰寫規則
有許多內建的規則可以使用;或是使用 Must
來自定規則
使用 When
可以指定規則生效的前提
使用 WithName
可以指定欄位在訊息顯示的名稱
使用 WithMessage
可以自訂驗證失敗時的訊息
使用 SetValidator
可以指定參數某個成員要用的驗證器
加上 CascadeMode.Stop
就可以在驗證失敗時直接跳出
使用 Validator 進行驗證
可以直接建立驗證器來驗證
如:new CardParameterValidator().Validate(parameter);
也可以註冊進行自動驗證
在 Startup
的 ConfigureServices
加上 AddFluentValidation
及驗證器的註冊
當然,FluentValidation 還有許多進階的應用可以探索,例如:
使用
RuleSets
來將驗證器規則分成多個規則集,再針對狀況使用
例如新增和更新的功能共用同個參數的時候,就可以考慮使用規則集來指定各自要驗證哪些規則
需要客製化驗證失敗時回傳的 ViewModel 時,可以將
包裝到 Attribute 裡進行攔截及驗證
實際案例,敝司對 API 回傳格式有嚴格規範,於是前輩就在 Attribute 裡實例化 Validator 再從 actionContext 抓出參數驗證…
諸如此類,畢竟在參數驗證的路上發生什麼事也不奇怪,請再根據狀況自由地調整吧。
那麼,我們下回見~
大臣提到的 是一套能幫我們把傳入參數的分離出去、用更口語化的方式去撰寫的工具。
大部份的狀況下,使用內建的驗證語法就很夠用了。可以參照官方文檔的 ,裡面每一項都有範例和參數說明。
那俺身為一個 懶惰 節能減碳工程師,當然有在 Linqpad 中準備一份範例 才能隨時抄嘛,這邊也會附在文末的。
這邊直接使用先前建置好的 頁面來測試,並且故意把攻擊力打成負數:
不過都已經到了 .net Core 時代, 已經是內建的功能下,還要用 new
一個驗證器這種直接依賴的方式還是有點不太舒服……所以 Fluent Validation 也有提供自動驗證的作法!
使用 來替驗證器寫單元測試