FluentValidation 驗證指南
用途:以 Fluent API 定義強型別、可測試的驗證規則
NuGet:
FluentValidation(核心)/FluentValidation.DependencyInjectionExtensions(DI 整合)版本:12.x(最新 12.1.1)
授權:Apache 2.0(.NET Foundation 成員)
支援:.NET 8+
為什麼選 FluentValidation
在工業自動化中,我們需要驗證各種輸入:Recipe 參數、設備設定、通訊設定。驗證邏輯需要:集中管理、可測試、訊息可自訂。
| 方案 | 優勢 | 劣勢 |
|---|---|---|
| FluentValidation | 集中管理、強型別、可測試、規則可組合 | 需學習 Fluent API |
| DataAnnotations | 簡單、與 ASP.NET 整合好 | 規則寫在 Model 上、不靈活、難以測試複雜規則 |
| 手寫 if-else | 完全自由 | 散亂在各處、難維護、容易遺漏 |
對比
// ❌ DataAnnotations:規則和 Model 綁死,跨欄位驗證困難
public class Recipe
{
[Required(ErrorMessage = "名稱不可為空")]
[MaxLength(50)]
public string Name { get; set; }
[Range(0, 500, ErrorMessage = "溫度必須在 0-500 之間")]
public double Temperature { get; set; }
}
// ❌ 手寫 if-else:散落各處
if (string.IsNullOrEmpty(recipe.Name))
errors.Add("名稱不可為空");
if (recipe.Temperature < 0 || recipe.Temperature > 500)
errors.Add("溫度必須在 0-500 之間");
// ✅ FluentValidation:集中、可讀、可測試
public class RecipeValidator : AbstractValidator<Recipe>
{
public RecipeValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("名稱不可為空")
.MaximumLength(50).WithMessage("名稱最多 50 個字元");
RuleFor(x => x.Temperature)
.InclusiveBetween(0, 500)
.WithMessage("溫度必須在 0-500°C 之間");
}
}
安裝
dotnet add package FluentValidation
dotnet add package FluentValidation.DependencyInjectionExtensions # DI 整合
using FluentValidation;
using FluentValidation.Results;
核心概念
AbstractValidator<T>
所有驗證器都繼承 AbstractValidator<T>,在建構子中定義規則:
public class DeviceSettingsValidator : AbstractValidator<DeviceSettings>
{
public DeviceSettingsValidator()
{
RuleFor(x => x.IpAddress)
.NotEmpty().WithMessage("IP 位址不可為空")
.Matches(@"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
.WithMessage("IP 位址格式不正確");
RuleFor(x => x.Port)
.InclusiveBetween(1, 65535)
.WithMessage("Port 必須在 1-65535 之間");
RuleFor(x => x.DeviceName)
.NotEmpty()
.MaximumLength(100);
RuleFor(x => x.PollingIntervalMs)
.GreaterThanOrEqualTo(100)
.WithMessage("輪詢間隔至少 100ms")
.LessThanOrEqualTo(60000)
.WithMessage("輪詢間隔最多 60000ms");
}
}
執行驗證
var validator = new DeviceSettingsValidator();
var settings = new DeviceSettings { IpAddress = "", Port = 0 };
ValidationResult result = validator.Validate(settings);
if (!result.IsValid)
{
foreach (var error in result.Errors)
{
Console.WriteLine($"[{error.PropertyName}] {error.ErrorMessage}");
}
}
// 輸出:
// [IpAddress] IP 位址不可為空
// [Port] Port 必須在 1-65535 之間
拋出例外模式
// ValidateAndThrow:驗證失敗直接拋 ValidationException
validator.ValidateAndThrow(settings);
常用內建驗證器
| 驗證器 | 用途 | 範例 |
|---|---|---|
NotEmpty() | 不可為空(含空白字串) | 設備名稱 |
NotNull() | 不可為 null | 必填欄位 |
MaximumLength(n) | 最大長度 | 名稱最多 50 字 |
MinimumLength(n) | 最小長度 | 密碼至少 8 字 |
InclusiveBetween(a, b) | 範圍(含端點) | 溫度 0-500°C |
ExclusiveBetween(a, b) | 範圍(不含端點) | |
GreaterThan(n) | 大於 | 冷卻速率 > 0 |
LessThan(n) | 小於 | |
GreaterThanOrEqualTo(n) | 大於等於 | 輪詢間隔 ≥ 100ms |
Equal(value) | 等於 | |
NotEqual(value) | 不等於 | |
Matches(regex) | 正則比對 | IP 格式 |
EmailAddress() | Email 格式 | |
IsInEnum() | 是有效的 enum 值 | 設備模式 |
Must(predicate) | 自訂條件 | 任意邏輯 |
WithMessage 自訂訊息
RuleFor(x => x.Temperature)
.InclusiveBetween(0, 500)
.WithMessage("溫度 {PropertyValue} 超出範圍,必須在 {From}-{To}°C 之間");
// 輸出:溫度 600 超出範圍,必須在 0-500°C 之間
可用的佔位符:{PropertyName}、{PropertyValue}、{ComparisonValue}、{From}、{To}。
CascadeMode — 驗證短路
預設情況下,一個屬性的所有規則都會執行(Continue)。用 Cascade(CascadeMode.Stop) 可以在第一個失敗時停止後續規則。下圖以 RuleFor(x.Name).NotEmpty().MaximumLength(50).Matches(regex) 為例對照兩種模式:
Continue(預設):所有規則都跑,累積錯誤
Stop:第一條失敗即停
何時用哪個:
- Continue(預設):表單驗證 — 一次顯示所有錯誤給使用者,避免「修一個跳一個」的體驗
- Stop:規則之間有相依(後一條依賴前一條的結果),或後續規則執行成本高(DB 查詢、API 呼叫)時,先擋掉廉價檢查
對應的 C# 寫法:
RuleFor(x => x.RecipeName)
.Cascade(CascadeMode.Stop) // 第一個失敗就停
.NotEmpty().WithMessage("名稱不可為空")
.MaximumLength(50).WithMessage("名稱過長")
.Must(BeUniqueName).WithMessage("名稱已存在"); // 這條會打 DB,先用 NotEmpty + MaximumLength 擋掉
全域設定:
ValidatorOptions.Global.DefaultRuleLevelCascadeMode = CascadeMode.Stop;
Child Validator 組合
複雜物件(如 Recipe 包含多個 Step)可用 SetValidator 把子物件的驗證委派給專屬的 Validator,避免單一 Validator 過大:
組合模式的特性:
SetValidator:對單一子屬性套用一個子 ValidatorRuleForEach + SetValidator:對集合中的每一個元素套用同一個子 Validator- 錯誤路徑會帶上巢狀路徑:子 Validator 的
RuleFor(s => s.Duration)失敗時,PropertyName會是Steps[0].Duration,方便 UI 對應到正確欄位
子 Validator 可以獨立測試與重用——StepValidator 可以同時被 RecipeValidator 與獨立的「新增 Step 表單」共用。
本指南結構
| 頁面 | 內容 |
|---|---|
| 概觀與核心用法(本頁) | AbstractValidator、內建驗證器、CascadeMode |
| 進階用法與場景 | 條件驗證、集合驗證、自訂驗證、DI、WPF/ReactiveUI 整合、測試 |