Kerry 的筆記本
  • Table of contents
  • Kerry的Mac裝機必要
  • ASP.NET Core 教育訓練文件
    • .NET 9 OpenAPI 介紹與教學
    • 目錄
    • ASP.NET Core Authentication系列(一)理解Claim, ClaimsIdentity, ClaimsPrincipal
    • ASP.NET Core Authentication系列(三)Cookie選項
    • ASP.NET Core Authentication系列(二)實現認證、登錄和註銷
    • ASP.NET Core Authentication系列(四)基於Cookie實現多應用間單點登錄(SSO)
    • ASP.NET Core Consul 教學
    • ASP.NET Core Hangfire 排程管理
    • ASP.NET Core KeyCloak 實作
    • ASP.NET Core NLog-依照Environment使用Nlog.Config檔案
    • ASP.NET Core NLog-如何使用 NLog 將 log 寫到檔案
    • ASP.NET Core Nlog-發送訊息到ElasticSearch
    • 目錄
    • ASP.NET Core Quartz.NET 管理介面
    • ASP.NET Core RDLC 報表設計
    • ASP.NET Core SFTP (使用第三方套建 SSH.Net) - 類別庫為案例
    • ASP.NET Core 中使用 HttpReports 進行接口統計,分析, 可視化, 監控,追踪等
    • ASP.NET 使用 MassTransit 與 RabbitMQ,實現事件發佈、訂閱
    • Asp.Net Core 分散式Session – 使用 Redis
    • ASP.NET Core 前台會員修改個人資料
    • ASP.NET Core 前台會員忘記密碼與重設密碼
    • ASP.NET Core 前台會員登入
    • ASP.NET Core 前台會員註冊
    • ASP.NET Core 呼叫 API 發生 CORS 錯誤
    • ASP.NET Core 如何套網頁設計樣版
    • ASP.NET Core 客製化Model Validation 預設錯誤訊息
    • ASP.NET Core 後台查詢頁面教學
    • ASP.NET Core 網站生命週期
    • ASP.NET Feature Management 使用說明與教學
    • ASP.NET RulesEngine 介紹
    • ASP.NET WinForms APP 程式安裝檔
    • LinePay 支付完成後返回 LINE 應用而不跳出外部瀏覽器
    • EntityFramework
      • EF Core Migrations 完整教學手冊
      • EntityFramework Core DB Migrations
      • 使用 Entity Framework Core (EF Core) 的 Migrations 功能進行版本控制
    • NET 6
      • .NET 6 Autofac範例
      • .NET 6 Automapper範例
      • .NET 6 BenchmarkDotNet範例
      • .NET 6 Bogus範例
      • .NET 6 Dapper範例
      • .NET 6 Dapper語法說明
      • .NET 6 EFCore範例
      • .NET 6 EFCore語法說明
      • .NET 6 EPPlus圖表範例
      • .NET 6 EPPlus範例
      • .NET 6 Hangfire範例
      • .NET 6 HttpClient單元測試範例
      • .NET 6 MailKit前置作業
      • .NET 6 MailKit範例
      • .NET 6 Moq範例
      • .NET 6 NLog範例
      • .NET 6 NLog進階範例
      • .NET 6 Serilog範例
      • .NET 6 Serilog進階範例
      • .NET 6 Telegram.Bot前置作業
      • .NET 6 Telegram.Bot範例
      • .NET 6 Text.Json範例
      • .NET 6 swagger授權
      • .NET 6 swagger範例
      • .NET 6 xUnit範例
      • .NET 6 取得appsettings檔案內容
      • .NET 6 更改回傳Json時為大駝峰命名
      • .NET 6 解決System.Text.Json序列化後會將所有非ASCII轉為Unicode
    • WDMIS
      • CORS
      • FeatureManagement
      • Serilog
      • Spectre.Console
      • 資料模型實戰:從 MSSQL 設計到 .NET 8 WebAPI 實作(以刀具管理為例)
  • Azure
    • 如何在 ASP.NET CORE 5.0 WEB 應用程序中實現 AZURE AD 身份驗證
    • Azure App Configuration 使用教學
    • Azure Blob Storage
    • Azure DevOps 持續整合(CI) + Artifacts
  • CSharp
    • ASP.NET await 與 wait 的差異
    • AutoMapper —— 類別轉換超省力
    • C# 中的 HTTPClient — 入門指南
    • C# 正則表達式:從零到英雄指南
    • C# 集合, List<> 取交集、差集、聯集的方法
    • C#單元測試教學
    • CORS 介紹與設定方式
    • CSharp Coding Conventions
    • Using jQuery Unobtrusive AJAX in ASP.NET Core Razor Pages
    • 深入Dapper.NET源碼
    • 菜雞與物件導向
      • 菜雞與物件導向 (0): 前言
      • 菜雞與物件導向 (1): 類別、物件
      • 菜雞與物件導向 (10): 單一職責原則
      • 菜雞與物件導向 (11): 開放封閉原則
      • 菜雞與物件導向 (12): 里氏替換原則
      • 菜雞與物件導向 (13): 介面隔離原則
      • 菜雞與物件導向 (14): 依賴反轉原則
      • 菜雞與物件導向 (15): 最少知識原則
      • 菜雞與物件導向 (2): 建構式、多載
      • 菜雞與物件導向 (3): 封裝
      • 菜雞與物件導向 (4): 繼承
      • 菜雞與物件導向 (5): 多型
      • 菜雞與物件導向 (6): 抽象、覆寫
      • 菜雞與物件導向 (7): 介面
      • 菜雞與物件導向 (8): 內聚、耦合
      • 菜雞與物件導向 (9): SOLID
      • 菜雞與物件導向 (Ex1): 小結
  • DBeaver
    • 如何強制讓 DBeaver 在 Mac 上使用英文介面
  • DesignPattern
    • OAuth
    • Repository 模式 (Repository Pattern)
    • Single Sign On 實作方式介紹 (CAS)
    • 【SOP製作教學】新手適用,SOP範例、流程圖、製作流程全公開!
    • 【SOP製作教學】流程圖教學、重點範例、BPMN符號介紹!
    • 【SOP製作教學】流程圖符號整理、BPMN2.0進階符號教學!
    • 多奇數位 C# 程式碼撰寫規範 (C# Coding Guideline)
    • 軟體分層設計模式 (Software Layered Architecture Pattern)
    • 開源程式碼檢測平台 SonarQube
    • 菜雞新訓記
      • 菜雞新訓記 (0): 前言
      • 菜雞新訓記 (1): 使用 Git 來進行版本控制吧
      • 菜雞新訓記 (2): 認識 Api & 使用 .net Core 來建立簡單的 Web Api 服務吧
      • 菜雞新訓記 (3): 使用 Dapper 來連線到資料庫 CRUD 吧
      • 菜雞新訓記 (4): 使用 Swagger 來自動產生可互動的 API 文件吧
      • 菜雞新訓記 (5): 使用 三層式架構 來切分服務的關注點和職責吧
      • 菜雞新訓記 (6): 使用 依賴注入 (Dependency Injection) 來解除強耦合吧
      • 菜雞新訓記 (7): 使用 Fluent Validation 來驗證參數吧
  • DevOps
    • Repository 模式 (Repository Pattern)
    • pipeline工具研究
    • 單例模式 (Singleton Pattern)
    • 單元測試
    • 軟體分層設計模式 (Software Layered Architecture Pattern)
    • 雙重檢查鎖定模式 (Double-Checked Locking Pattern)
  • Docker
    • Docker 中部署 .NET 8 Web App 並支援 HTTPS
    • Docker指令大全
    • 第七章 安裝Nomad
    • Docker - 第三章 | 安裝 MSSQL
    • Docker - 第九章 | 安裝 datalust seq
    • 第二章 docker-compose 教學
    • Docker - 第五章 | 安裝 Redis
    • 第八章 安裝SonarQube
    • Docker - 第六章 | 安裝RabbitMQ
    • 第十一章 安裝 VtigerCRM
    • 第十二章 安裝KeyCloak
    • Docker - 第十章 | 安裝 Redmine
    • 第四章 安裝MySQL
    • Docker Desktop (含更改 Docker Image 路徑)
  • Git
    • Git Flow 指令大全(完整指令整理) 🚀
    • Git 安裝及配置SSH Key
    • Git 建立到上傳
    • 將現有專案的遠端儲存庫直接更改為新的儲存庫
    • Git 流程規劃
    • Git 語法大全
    • 30 天精通 Git 版本控管
      • 30 天精通 Git 版本控制
        • 第 01 天:认识 Git 版本控制
        • 第 02 天:在 Windows 平台必装的三套 Git 工具
        • 第 03 天:建立仓库
        • 第 04 天:常用的 Git 版本控制指令
        • 第 05 天:了解仓库、工作目录、物件与索引之间的关系
        • 第 06 天:解析 Git 资料结构 - 物件结构
        • 第 07 天:解析 Git 资料结构 - 索引结构
        • 第 08 天:关于分支的基本观念与使用方式
        • 第 09 天:比对文件与版本差异
        • 第 10 天:认识 Git 物件的绝对名称
        • 第 11 天:认识 Git 物件的一般参照与符号参照
        • 第 12 天:认识 Git 物件的相对名称
        • 第 13 天:暂存工作目录与索引的变更状态
        • 第 14 天: Git for Windows 选项设定
        • 第 15 天:标签 - 标记版本控制过程中的重要事件
        • 第 16 天:善用版本日志 git reflog 追踪变更轨迹
        • 第 17 天:关于合并的基本观念与使用方式
        • 第 18 天:修正 commit 过的版本历史记录 Part 1
        • 第 19 天:设定 .gitignore 忽略清单
        • 第 20 天:修正 commit 过的版本历史记录 Part 2
        • 第 21 天:修正 commit 过的版本历史记录 Part 3
        • 第 22 天:修正 commit 过的版本历史记录 Part 4 (Rebase)
        • 第 23 天:修正 commit 过的版本历史记录 Part 5
        • 第 24 天:使用 GitHub 远端仓库 - 入门篇
        • 第 25 天:使用 GitHub 远端仓库 - 观念篇
        • 第 26 天:多人在同一个远端仓库中进行版控
        • 第 27 天:通过分支在同一个远端仓库中进行版控
        • 第 28 天:了解 GitHub 的 fork 与 pull request 版控流程
        • 第 29 天:如何将 Subversion 项目汇入到 Git 仓库
        • 第 30 天:分享工作中几个好用的 Git 操作技巧
      • zh-tw
        • 第 01 天:認識 Git 版本控管
        • 第 02 天:在 Windows 平台必裝的三套 Git 工具
        • 第 03 天:建立儲存庫
        • 第 04 天:常用的 Git 版本控管指令
        • 第 05 天:了解儲存庫、工作目錄、物件與索引之間的關係
        • 第 06 天:解析 Git 資料結構 - 物件結構
        • 第 07 天:解析 Git 資料結構 - 索引結構
        • 第 08 天:關於分支的基本觀念與使用方式
        • 第 09 天:比對檔案與版本差異
        • 第 10 天:認識 Git 物件的絕對名稱
        • 第 11 天:認識 Git 物件的一般參照與符號參照
        • 第 12 天:認識 Git 物件的相對名稱
        • 第 13 天:暫存工作目錄與索引的變更狀態
        • 第 14 天: Git for Windows 選項設定
        • 第 15 天:標籤 - 標記版本控制過程中的重要事件
        • 第 16 天:善用版本日誌 git reflog 追蹤變更軌跡
        • 第 17 天:關於合併的基本觀念與使用方式
        • 第 18 天:修正 commit 過的版本歷史紀錄 Part 1
        • 第 19 天:設定 .gitignore 忽略清單
        • 第 20 天:修正 commit 過的版本歷史紀錄 Part 2
        • 第 21 天:修正 commit 過的版本歷史紀錄 Part 3
        • 第 22 天:修正 commit 過的版本歷史紀錄 Part 4 (Rebase)
        • 第 23 天:修正 commit 過的版本歷史紀錄 Part 5
        • 第 24 天:使用 GitHub 遠端儲存庫 - 入門篇
        • 第 25 天:使用 GitHub 遠端儲存庫 - 觀念篇
        • 第 26 天:多人在同一個遠端儲存庫中進行版控
        • 第 27 天:透過分支在同一個遠端儲存庫中進行版控
        • 第 28 天:了解 GitHub 的 fork 與 pull request 版控流程
        • 第 29 天:如何將 Subversion 專案匯入到 Git 儲存庫
        • 第 30 天:分享工作中幾個好用的 Git 操作技巧
  • Hands-On Labs - LineBotSDK 實作手札 (C#, .net core)
    • 00. 如何申請LINE Bot
    • CLI
      • 使用CLI來發送新的Channel Access Token(LINE Bot)
      • 使用CLI免費發送LINE Notify通知
    • basic
      • 如何發送LINE訊息(Push Message)
      • 如何發送LINE Template Messages
      • 如何發送ImageMap訊息
      • 如何發送Flex Message
      • 如何在訊息後面加上QuickReply快捷選項
    • liff
      • Lab 21: 建立第一個LIFF應用
    • webhook
      • 如何建立可Echo的基本LINE Bot
      • 如何在WebHook中取得用戶個人資訊(名稱、頭像、狀態)
      • 如何在WebHook中取得用戶上傳的圖片(Bytes)
  • Markdown
    • Markdown Cheatsheet 中文版
    • Markdown語法大全
    • 使用HackMD建立書本目錄
    • 使用HackMD建立簡報
  • SAP ABAP
    • ABAP開發環境和總體介紹
    • SAP MM模塊常用表總結
    • SAP QM數據庫表清單
    • SAP欄位與表的對應關係
  • SQL Server
    • [SQL SERVER] Like in
    • SQL Server 中,移除資料庫中所有的關聯限制
    • SQL Server 刪除資料庫中所有資料表
    • SQL Server View、Function 及 Stored Procedure 定義之快速備份
    • SSMS v18 清除登入畫面中,下拉選單歷史紀錄
    • [MS SQL]如何透過Database Mail進行郵件發送
    • [SQL SERVER]撰寫Stored Procedure小細節
    • 使用 Data Migration Assistant 移轉 SQL Server 資料庫與帳戶
    • 使用SSIS創建同步資料庫數據任務
  • Tools
    • 免費 FTP 伺服器 FileZilla Server 安裝教學 (新版設定)
  • VisualStudio
    • .NET CLI 指令碼介紹
    • Visual Studio 使用 Git 版本控制
    • 使用 Visual Studio 2022 可透過 .editorconfig 鎖定文字檔案的儲存編碼格式分享
  • Web API
    • ASP.NET Core 6 Web API 進行 JWT 令牌身份驗證
    • [ASP.NET Core]如何使用SwaggerAPI說明文件
    • ASP.NET Core Web Api實作JWT驗證筆記
    • ECFIT API 範例
    • JWT Token Authentication And Authorizations In .Net Core 6.0 Web API
    • 微服務架構 - 從狀態圖來驅動 API 的設計
  • Windows
    • [C#] 伺服器監控常用語法 (事件檢視器、CPU 硬碟使用率、程式執行狀況)
    • Configure IIS Web Server on Windows Server 2019
    • Log Paser Studio 分析 IIS W3C Log
    • Windows Server 2019 如何安裝 IIS 運行 ASP.NET 專案
    • 如何檢查安裝在 IIS 上的 .NET Core Hosting Bundle 版本
    • [IIS] 如何解決網站第一個請求 Request 特別慢 ?
    • IIS 不停機更版設置
    • SQL Server 2019 Standard 繁體中文標準版安裝
    • WINDOWS共用資料夾的網路認證密碼放在哪?如何清除?
    • 如何設定 ASP.NET CORE 網站應用程式持續執行在 IIS 上
  • 專案管理
    • SSDLC (Secure Software Development Life Cycle)
    • 系統開發原則
    • MIS及專案管理-使用Redmine
      • 第10章 - [日常管理]MIS部門週會工作進度追蹤
      • 第11章 - [日常管理]MIS部門主管月會報告管理
      • 第12章 - [日常管理]機房工作日誌
      • 第13章 - [日常管理]MIS部門耗用工時及工作進度檢討
      • 第14章 - [日常管理]MIS文件知識庫
      • 第15章 - [日常管理]整理及管理分享
      • 第16章 - [異常管理]使用者問題回報系統
      • 第17章 - [異常管理]資安事件及異常紀錄
      • 第18章 - [異常管理]整理及管理分享
      • 第19章 - [變革管理]MIS的專案及專案管理五大階段
      • 第1章 - [MIS及專案管理]中小企業MIS的鳥事
      • 第20章 - [變革管理]MIS的新專案管理:起始階段
      • 第21章 - [變革管理]MIS的新專案管理:規劃階段
      • 第22章 - [變革管理]MIS的新專案管理:執行階段
      • 第23章 - [變革管理]MIS的新專案管理:監控階段
      • 第24章 - [變革管理]MIS的新專案管理:結束階段
      • 第25章 - [變革管理]整理及管理分享
      • 第26章 - [ISMS管理]ISMS平台整體規劃
      • 第27章 - [ISMS管理]ISMS文管中心
      • 第28章 - [ISMS管理]ISMS表單紀錄的管理
      • 第29章 - [ISMS管理]整理及管理分享
      • 第2章 - [MIS及專案管理]專案管理的概念及MIS應用
      • 第30章 - 初心、來時路及感謝:系列文章總結回顧
      • 第3章 - [MIS及專案管理]管理工具的選擇
      • 第4章 - [Redmine]Redmine的安裝及設定
      • 第5章 - [Redmine]Redime系統邏輯說明
      • 第6章 - [Redmine]自行建立及維護表單
      • 第7章 - [Redmine]專案版面的規劃
      • 第8章 - [日常管理]AR管理
      • 第9章 - [日常管理]資訊服務申請
  • 微服務架構
    • DDD + CQRS + MediatR 專案架構
    • 微服務架構 #2, 按照架構,重構系統
    • 淺談微服務與網站架構的發展史
    • API First Workshop 設計概念與實做案例
      • API First #1 架構師觀點 - API First 的開發策略 - 觀念篇
      • API First #2 架構師觀點 - API First 的開發策略 - 設計實做篇
    • 基礎建設 - 建立微服務的執行環境
      • Part #1 微服務基礎建設 - Service Discovery
      • Part #2 微服務基礎建設 - 服務負載的控制
      • Part #3 微服務基礎建設 - 排隊機制設計
      • Part #4 可靠的微服務通訊 - Message Queue Based RPC
      • Part #5 非同步任務的處理機制 - Process Pool
    • 實做基礎技術 API & SDK Design
      • API & SDK Design #1, 資料分頁的處理方式
      • API & SDK Design #2, 設計專屬的 SDK
      • API & SDK Design #3, API 的向前相容機制
      • API & SDK Design #4, API 上線前的準備 - Swagger + Azure API Apps
      • API & SDK Design #5 如何強化微服務的安全性 API Token JWT 的應用
    • 建構微服務開發團隊
      • 架構面試題 #1, 線上交易的正確性
      • 架構面試題 #2, 連續資料的統計方式
      • 架構面試題 #3, RDBMS 處理樹狀結構的技巧
      • 架構面試題 #4 - 抽象化設計;折扣規則的設計機制
    • 架構師觀點 - 轉移到微服務架構的經驗分享
      • Part #1 改變架構的動機
      • Part #2 實際改變的架構案例
    • 案例實作 - IP 查詢服務的開發與設計
      • 容器化的微服務開發 #1 架構與開發範例
      • 容器化的微服務開發 #2 IIS or Self Host
  • 系統評估
    • RPA 與 WebAPI 評估
    • 數位轉型:從現有系統到數位化的未來
    • 數位轉型:從現有系統到數位化的未來
  • 面試
    • CV_黃子豪_2024
    • HR 問題集
    • .NET 工程師 面試問題集
    • 資深工程師 問題集
    • 資深開發人員 / 技術主管
    • 題目
Powered by GitBook
On this page

Last updated 1 year ago

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

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

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

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

前言

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

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

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

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

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

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

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

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

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

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

專案現況

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

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

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

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

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

安裝 Fluent Validation

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

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

撰寫 Validator

要使用 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() 來傳入自訂的規則,例如:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

指定 CascadeMode.Stop 來提早返回

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

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

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

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

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

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

將前述的規則實作成 Validator

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

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

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

  • 攻擊力不可為負數

  • 生命值不可為負數

  • 使用成本不可為負數

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

  • 名稱不可以為空值

  • 名稱必須少於十五字

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

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

使用 Validator 進行驗證

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

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

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

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

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

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

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

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

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

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

註冊 Validator 來自動進行驗證

首先讓我們到熟悉的 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 抓出參數驗證…

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

那麼,我們下回見~

參考資料

同系列文章

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

其他文章

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

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

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

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

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

使用 來替驗證器寫單元測試

/// <summary>
/// 新增卡片
/// </summary>
/// <param name="parameter">卡片參數</param>
/// <returns></returns>
[HttpPost]
public IActionResult Insert([FromBody] CardParameter parameter)
{
    // 這邊需要對參數做檢查
    if (parameter.Attack < 0)
    {
        return BadRequest("卡片的攻擊力不可為負數");
    }

    if (parameter.Health < 0)
    {
        return BadRequest("卡片的生命值不可為負數");
    }

    if (parameter.Cost < 0)
    {
        return BadRequest("卡片的使用成本不可為負數");
    }

    if (parameter.Description != null &&
        parameter.Description.Length > 30)
    {
        return BadRequest("卡片的敘述說明必須少於三十字");
    }

    if (string.IsNullOrWhiteSpace(parameter.Name))
    {
        return BadRequest("卡片的名稱不可為空白");
    }

    if (parameter.Name.Length > 15)
    {
        return BadRequest("卡片的名稱必須少於十五字");
    }

    // 用 AutoMapper 把 Parameter Model 轉換成 Info Model
    var info = this._mapper.Map<CardParameter, CardInfo>(parameter);

    // 呼叫依賴的 Service 層寫入資料
    var isInsertSuccess = this._cardService.Insert(info);
    if (isInsertSuccess)
    {
        return Ok();
    }
    return StatusCode(500);
}
/// <summary>
/// Card Parameter 的驗證器
/// </summary>
public class CardParameterValidator : AbstractValidator<CardParameter>
{
    /// <summary>
    /// 驗證器的建構式: 在這裡註冊我們要驗證的規則
    /// </summary>
    public CardParameterValidator()
    {

    }
}
/// <summary>
/// 驗證器建構式: 在這裡註冊我們要驗證的規則
/// </summary>
public CardParameterValidator()
{
    this.RuleFor(card => card.Attack)
        .GreaterThanOrEqualTo(0);
}
/// <summary>
/// 驗證器建構式: 在這裡註冊我們要驗證的規則
/// </summary>
public CardParameterValidator()
{
    this.RuleForEach(card => card.Alias)
        .NotEmpty(); // 不可為空
}
/// <summary>
/// 驗證器建構式: 在這裡註冊我們要驗證的規則
/// </summary>
public CardParameterValidator()
{
    // 使用 Must 來自訂規則
    this.RuleFor(card => card.Attack)
        .Must(attack => attack > 0 && attack <= 3000);
}
/// <summary>
/// 驗證器建構式: 在這裡註冊我們要驗證的規則
/// </summary>
public CardParameterValidator()
{
    // 目標:當 卡牌 是 怪獸卡 的時候,攻擊力不可為 Null 

    // 針對指定規則加上適用場景
    this.RuleFor(card => card.Attack)
        .NotNull()
        .When(card => card.CardType is CardType.Monster);

    // 針對指定場景加上適用規則,我個人比較喜歡這種
    this.When(card => card.CardType is CardType.Monster, () =>
    {
        this.RuleFor(card => card.Attack).NotNull();
    });

    // 以上兩種寫法是相同的,但我個人比較喜歡先 When 才指定規則
    // 除了比較符合日常口語以外,也能把同樣場景的規則整理在一起
}
/// <summary>
/// 驗證器建構式: 在這裡註冊我們要驗證的規則
/// </summary>
public CardParameterValidator()
{
    // 目標:當 卡牌 是 怪獸卡 的時候,攻擊力不可為 Null
    // 目標:當 卡牌 不是 怪獸卡 的時候,攻擊力必須為 Null

    this.When(card => card.CardType is CardType.Monster, () =>
    {
        this.RuleFor(card => card.Attack).NotNull();
    })
    .Otherwise(() =>
    {
        this.RuleFor(card => card.Attack).Null();
    });
}
/// <summary>
/// 驗證器建構式: 在這裡註冊我們要驗證的規則
/// </summary>
public CardParameterValidator()
{
    this.RuleFor(card => card.Attack)
        .GreaterThanOrEqualTo(0)
        .WithMessage("卡片的攻擊力不可為負數");
}
/// <summary>
/// 驗證器建構式: 在這裡註冊我們要驗證的規則
/// </summary>
public CardParameterValidator()
{
    this.RuleFor(card => card.Attack)
        .GreaterThanOrEqualTo(0)
        .WithName("攻擊力");
}
/// <summary>
/// 驗證器建構式: 在這裡註冊我們要驗證的規則
/// </summary>
public CardParameterValidator()
{
    this.RuleFor(card => card.Attack)
        .GreaterThanOrEqualTo(0)
        .WithName("攻擊力")
        .WithMessage("卡片的{PropertyName}不可為負數");
}
public class Card
{
	public CardType Type { get; set; }
	public int Cost { get; set; }
	public string Name { get; set; }
	public int Attack { get; set; }
	
	public Equipment Equipment { get; set;} // 可以穿裝備了!
}

public class Equipment { }
public class EquipmentValidator : AbstractValidator<Card> { }
/// <summary>
/// 驗證器建構式: 在這裡註冊我們要驗證的規則
/// </summary>
public CardParameterValidator()
{
    this.RuleFor(card => card.Equipment)
        .SetValidator(new EquipmentValidator());
}
/// <summary>
/// 驗證器建構式: 在這裡註冊我們要驗證的規則
/// </summary>
public CardParameterValidator()
{
    // 驗證失敗時即停止
    this.CascadeMode = FluentValidation.CascadeMode.Stop;

    this.RuleFor(card => card.Attack)
        .GreaterThanOrEqualTo(0);
}
/// <summary>
/// 驗證器建構式: 在這裡註冊我們要驗證的規則
/// </summary>
public CardParameterValidator()
{
    this.RuleFor(card => card.Attack)
        .Cascade(CascadeMode.Stop)
        .GreaterThanOrEqualTo(0);
}
/// <summary>
/// 新增卡片
/// </summary>
/// <param name="parameter">卡片參數</param>
/// <returns></returns>
[HttpPost]
public IActionResult Insert([FromBody] CardParameter parameter)
{
    // 一堆檢查
    if (parameter.Attack < 0)
    {
        return BadRequest("卡片的攻擊力不可為負數");
    }

    if (parameter.Health < 0)
    {
        return BadRequest("卡片的生命值不可為負數");
    }

    if (parameter.Cost < 0)
    {
        return BadRequest("卡片的使用成本不可為負數");
    }

    if (parameter.Description != null &&
        parameter.Description.Length > 30)
    {
        return BadRequest("卡片的敘述說明必須少於三十字");
    }

    if (string.IsNullOrWhiteSpace(parameter.Name))
    {
        return BadRequest("卡片的名稱不可為空白");
    }

    if (parameter.Name.Length > 15)
    {
        return BadRequest("卡片的名稱必須少於十五字");
    }

    // 用 AutoMapper 把 Parameter Model 轉換成 Info Model
    var info = this._mapper.Map<CardParameter, CardInfo>(parameter);

    // 呼叫依賴的 Service 層寫入資料
    var isInsertSuccess = this._cardService.Insert(info);
    if (isInsertSuccess)
    {
        return Ok();
    }
    return StatusCode(500);
}
/// <summary>
/// Card Parameter 的驗證器
/// </summary>
public class CardParameterValidator : AbstractValidator<CardParameter>
{
    /// <summary>
    /// 驗證器的建構式: 在這裡註冊我們要驗證的規則
    /// </summary>
    public CardParameterValidator()
    {
        this.RuleFor(card => card.Attack)
            .GreaterThanOrEqualTo(0);

        this.RuleFor(card => card.Health)
            .GreaterThanOrEqualTo(0);

        this.RuleFor(card => card.Cost)
            .GreaterThanOrEqualTo(0);

        this.RuleFor(card => card.Description)
            .NotNull()
            .MaximumLength(30);

        this.RuleFor(card => card.Name)
            .NotEmpty()
            .MaximumLength(15);
    }
}
/// <summary>
/// 新增卡片
/// </summary>
/// <param name="parameter">卡片參數</param>
/// <returns></returns>
[HttpPost]
public IActionResult Insert(
    [FromBody] CardParameter parameter)
{
    // 這邊需要對參數做檢查

    // 用 AutoMapper 轉換 Model
    var info = this._mapper.Map<CardParameter,CardInfo>(parameter);

    // 呼叫 Service 層寫入資料
    var isInsertSuccess = this._cardService.Insert(info);
    if (isInsertSuccess)
    {
        return Ok();
    }
    return StatusCode(500);
}
var validator = new CardParameterValidator();
var validationResult = validator.Validate(parameter);
/// <summary>
/// 新增卡片
/// </summary>
/// <param name="parameter">卡片參數</param>
/// <returns></returns>
[HttpPost]
public IActionResult Insert(
    [FromBody] CardParameter parameter)
{
    // 這邊需要對參數做檢查
    var validator = new CardParameterValidator();
    var validationResult = validator.Validate(parameter);

    // 用 AutoMapper 把 Parameter Model 轉換成 Info Model
    var info = this._mapper.Map<CardParameter, CardInfo>(parameter);

    // 呼叫依賴的 Service 層寫入資料
    var isInsertSuccess = this._cardService.Insert(info);
    if (isInsertSuccess)
    {
        return Ok();
    }
    return StatusCode(500);
}
/// <summary>
/// 新增卡片
/// </summary>
/// <param name="parameter">卡片參數</param>
/// <returns></returns>
[HttpPost]
public IActionResult Insert(
    [FromBody] CardParameter parameter)
{
    // 這邊需要對參數做檢查
    var validator = new CardParameterValidator();
    var validationResult = validator.Validate(parameter);

    // 如果沒有通過檢查,就把訊息串一串丟回去
    if (validationResult.IsValid is false)
    {
        var errorMessages = validationResult.Errors.Select(e => e.ErrorMessage);
        var resultMessage = string.Join(",", errorMessages);
        return BadRequest(resultMessage); // 直接回傳 400 + 錯誤訊息
    }

    // 用 AutoMapper 把 Parameter Model 轉換成 Info Model
    var info = this._mapper.Map<CardParameter, CardInfo>(parameter);

    // 呼叫依賴的 Service 層寫入資料
    var isInsertSuccess = this._cardService.Insert(info);
    if (isInsertSuccess)
    {
        return Ok();
    }
    return StatusCode(500);
}
services.AddFluentValidation();
services.AddTransient<IValidator<CardParameter>, CardParameterValidator>();
services.AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<Startup>());
/// <summary>
/// 新增卡片
/// </summary>
/// <param name="parameter">卡片參數</param>
/// <returns></returns>
[HttpPost]
public IActionResult Insert(
    [FromBody] CardParameter parameter)
{
    // 將原本的參數檢查刪掉了!

    // 用 AutoMapper 把 Parameter Model 轉換成 Info Model
    var info = this._mapper.Map<CardParameter, CardInfo>(parameter);

    // 呼叫依賴的 Service 層寫入資料
    var isInsertSuccess = this._cardService.Insert(info);
    if (isInsertSuccess)
    {
        return Ok();
    }
    return StatusCode(500);
}
AbstractValidator<T>
validator.Validate
void Main()
{
    var sut = new Card
    {
        Cost = 10,
        Name = "Blue-Eyes White Dragon",
        Type = CardType.Monster
    };
    
    var validator = new CardValidator();
    var result = validator.Validate(sut);
    
    result.Dump();
}

public class Card
{
    public CardType Type { get; set; }
    public int Cost { get; set; }
    public string Name { get; set; }
}

public enum CardType
{
    Monster = 0
}

public class CardValidator : AbstractValidator<Card>
{
    public CardValidator()
    {
        // Fluent Validation 的 驗證器請參照
        // https://docs.fluentvalidation.net/en/latest/built-in-validators.html

		// 驗證失敗時即停止
		//this.CascadeMode = FluentValidation.CascadeMode.Stop;

        // 為了示範所以做成變數,平時可以直接 RuleFor().XXX() 串接驗證器即可
        var name = this.RuleFor(card => card.Name);
        var cost = this.RuleFor(card => card.Cost);
        var type = this.RuleFor(card => card.Type);

        // 不可為 Null    
        name.NotNull();
        
        // 必須為 Null
        //name.Null();
        
        // 不可為空
        name.NotEmpty();
        
        // 必須為空
        //name.Empty();
        
        // 不可相同
        name.NotEqual("Test Card");

        // 不可相同:也支持 StringComparer
        name.NotEqual("Test Card", StringComparer.OrdinalIgnoreCase);
        
        // 不可相同:也可以比較其他欄位(大多驗證器都支援)
        name.NotEqual(card => card.Type.ToString());
        
        // 必須相同,其餘用法可參考 NotEqual
        name.Equal("Blue-Eyes White Dragon");
        
        // 長度限制,限定1~200
        name.Length(1, 200);
        
        // 最大長度限制
        name.MaximumLength(200);
        
        // 最小長度限制
        name.MinimumLength(0);
        
        // 數值需低於目標值
        cost.LessThan(11);
        
        // 數值需低於或等於目標值
        cost.LessThanOrEqualTo(10);
        
        // 數值需高於目標值
        cost.GreaterThan(0);
        
        // 數值需高於或等於目標值
        cost.GreaterThanOrEqualTo(0);
        
        // 數值需介於兩個目標值之間
        cost.ExclusiveBetween(0, 11);
        
        // 數值需介於兩個目標值之間(包含目標值)
        cost.InclusiveBetween(1, 10);
        
        // 檢查是否具有指定的位數,例如 (1, 4) = 小數點限1位、總位數限4位
        this.RuleFor(x => (decimal)x.Cost).ScalePrecision(0, 2);
        
        // 正則表達式
        name.Matches(@"^[a-zA-Z-' ]*$");
        
        // 必須為信箱格式
        //name.EmailAddress();
        
        // 必須為信用卡格式
        //name.CreditCard();
        
        // 必須包含在列舉中
        type.IsInEnum();

        // 必須包含在列舉名稱中
        //name.IsEnumName(typeof(CardType));
        
        // 指定驗證場景
        cost.GreaterThan(0).When(card => card.Type is CardType.Monster);

        // 指定驗證場景
        this.When(card => card.Type is CardType.Monster, () =>
        {
            cost.GreaterThan(0);
        });
        // .Otherwise(() => { cost.GreaterThan(0); });

        // 最終大絕招:自訂驗證器
        cost.Must(power => power > 0 && power <= 3000);
    }
}
  1. DesignPattern
  2. 菜雞新訓記

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

Previous菜雞新訓記 (6): 使用 依賴注入 (Dependency Injection) 來解除強耦合吧NextDevOps
  • 前言
  • 專案現況
  • 安裝 Fluent Validation
  • 撰寫 Validator
  • 使用內建的驗證規則
  • 使用 Must 來自訂驗證規則
  • 使用 When 來指定驗證條件適用的場景
  • 使用 WithName 和 WithMessage 來自訂驗證訊息
  • 使用 SetValidator 來指定成員的驗證器
  • 指定 CascadeMode.Stop 來提早返回
  • 將前述的規則實作成 Validator
  • 使用 Validator 進行驗證
  • 註冊 Validator 來自動進行驗證
  • 小結
  • 參考資料
  • 同系列文章
  • 附錄:FluentValidation 內建驗證方法 小抄
  • 其他文章
FluentValidation
Built-in Validators
附錄
Swagger
依賴注入
FluentValidation.TestHelper
料理佳餚 - 讓 Fluent Validation 把參數的檢查條件口語化 | 軟體主廚的程式料理廚房 - 點部落 (dotblogs.com.tw)
C# .Net MVC 06. 驗證參數- 透過FluentValidation (progressbar.tw)
DotnetCore 後端驗證神器:Fluent Validation | Eugene’s Blog (eugenesu0515.github.io)
ASP.NET Core — Fluent Validation documentation
Fluent Validation 使用ActionFilter來驗證參數 | 菜鳥工程師訓練營 - 點部落 (dotblogs.com.tw)
基于 .NET 的 Fluent Validation 验证教程-零度 (xcode.me)
FluentValidation documentation
菜雞新訓記 (0): 目錄
菜雞新訓記 (1): 使用 Git 來進行版本控制吧
菜雞新訓記 (2): 認識 Api & 使用 .net Core 來建立簡單的 Web Api 服務吧
菜雞新訓記 (3): 使用 Dapper 來連線到資料庫 CRUD 吧
菜雞新訓記 (4): 使用 Swagger 來自動產生可互動的 API 文件吧
菜雞新訓記 (5): 使用 三層式架構 來切分服務的關注點和職責吧
菜雞新訓記 (6): 使用 依賴注入 (Dependency Injection) 來解除強耦合吧
菜雞新訓記 (7): 使用 FluentValidation 來驗證傳入參數吧
菜雞新訓記 (6): 使用 依賴注入 (Dependency Injection) 來解除強耦合吧
菜雞新訓記 (5): 使用 三層式架構 來切分服務的關注點和職責吧
菜雞新訓記 (2): 認識 Api & 使用 .net Core 來建立簡單的 Web Api 服務吧
C#: BenchmarkDotnet —— 效能測試好簡單
菜雞新訓記 (4): 使用 Swagger 來自動產生可互動的 API 文件吧
Image
img
img
Image
Image
Image
Image
Image
Image
Image