進階用法與公司場景
條件驗證(When / Unless)
只在特定條件下才執行某些規則。
public class ProcessParameterValidator : AbstractValidator<ProcessParameter>
{
public ProcessParameterValidator()
{
RuleFor(x => x.CoolingRate)
.GreaterThan(0)
.WithMessage("冷卻速率必須大於 0")
.When(x => x.Mode == ProcessMode.Cooling);
// 只有冷卻模式才驗證冷卻速率
RuleFor(x => x.HeatingRate)
.GreaterThan(0)
.InclusiveBetween(1, 50)
.WithMessage("加熱速率必須在 1-50°C/min 之間")
.When(x => x.Mode == ProcessMode.Heating);
// Unless = When 的反面
RuleFor(x => x.HoldDuration)
.GreaterThan(0)
.Unless(x => x.Mode == ProcessMode.Manual);
// 手動模式不需要設定保持時間
}
}
集合驗證(RuleForEach)
驗證集合中的每個項目。
public class RecipeValidator : AbstractValidator<Recipe>
{
public RecipeValidator()
{
RuleFor(x => x.Name)
.NotEmpty().MaximumLength(50);
// 至少要有一個步驟
RuleFor(x => x.Steps)
.NotEmpty().WithMessage("Recipe 至少需要一個步驟");
// 每個步驟都要驗證
RuleForEach(x => x.Steps).SetValidator(new RecipeStepValidator());
}
}
public class RecipeStepValidator : AbstractValidator<RecipeStep>
{
public RecipeStepValidator()
{
RuleFor(x => x.StepName)
.NotEmpty().WithMessage("步驟名稱不可為空");
RuleFor(x => x.TargetTemperature)
.InclusiveBetween(0, 500)
.WithMessage("步驟溫度必須在 0-500°C 之間");
RuleFor(x => x.Duration)
.GreaterThan(TimeSpan.Zero)
.WithMessage("步驟時間必須大於 0");
}
}
跨欄位驗證
public class TemperatureRangeValidator : AbstractValidator<TemperatureRange>
{
public TemperatureRangeValidator()
{
RuleFor(x => x.MinTemperature)
.LessThan(x => x.MaxTemperature)
.WithMessage("最低溫度必須小於最高溫度");
RuleFor(x => x.EndTemperature)
.GreaterThanOrEqualTo(x => x.StartTemperature)
.When(x => x.Mode == ProcessMode.Heating)
.WithMessage("加熱模式下,結束溫度必須 ≥ 起始溫度");
RuleFor(x => x.EndTemperature)
.LessThanOrEqualTo(x => x.StartTemperature)
.When(x => x.Mode == ProcessMode.Cooling)
.WithMessage("冷卻模式下,結束溫度必須 ≤ 起始溫度");
}
}
自訂驗證器(Must / MustAsync)
簡單自訂邏輯
RuleFor(x => x.DeviceName)
.Must(name => !name.Contains("TEST"))
.WithMessage("正式環境不可使用測試名稱");
非同步自訂驗證(呼叫外部服務)
public class DeviceConfigValidator : AbstractValidator<DeviceConfig>
{
private readonly IDeviceRepository _repository;
public DeviceConfigValidator(IDeviceRepository repository)
{
_repository = repository;
RuleFor(x => x.DeviceName)
.NotEmpty()
.MustAsync(BeUniqueNameAsync)
.WithMessage("設備名稱 '{PropertyValue}' 已存在");
RuleFor(x => x.IpAddress)
.MustAsync(BeReachableAsync)
.WithMessage("IP 位址 {PropertyValue} 無法連線")
.When(x => !string.IsNullOrEmpty(x.IpAddress));
}
private async Task<bool> BeUniqueNameAsync(
string name, CancellationToken ct)
{
return !await _repository.ExistsAsync(name, ct);
}
private async Task<bool> BeReachableAsync(
string ip, CancellationToken ct)
{
try
{
using var ping = new System.Net.NetworkInformation.Ping();
var reply = await ping.SendPingAsync(ip, 2000);
return reply.Status == System.Net.NetworkInformation.IPStatus.Success;
}
catch { return false; }
}
}
驗證嚴重度(Severity)
區分 Error(必須修正)和 Warning(建議修正)。
public class SensorCalibrationValidator : AbstractValidator<CalibrationData>
{
public SensorCalibrationValidator()
{
// Error:必須修正
RuleFor(x => x.Offset)
.InclusiveBetween(-100, 100)
.WithMessage("偏移量超出允許範圍")
.WithSeverity(Severity.Error);
// Warning:超出建議值但未超限制
RuleFor(x => x.Offset)
.InclusiveBetween(-10, 10)
.WithMessage("偏移量超出建議範圍(±10),請確認")
.WithSeverity(Severity.Warning);
}
}
使用時分開處理:
var result = validator.Validate(data);
var errors = result.Errors
.Where(e => e.Severity == Severity.Error).ToList();
var warnings = result.Errors
.Where(e => e.Severity == Severity.Warning).ToList();
if (errors.Any())
ShowErrorDialog(errors);
else if (warnings.Any())
ShowWarningDialog(warnings); // 讓使用者選擇是否繼續
warning
即使只有 Severity.Warning 的失敗,result.IsValid 仍然會回傳 false。你必須自行依 Severity 過濾判斷是否要阻擋操作。
DI 整合
註冊 Validator
// 自動掃描並註冊所有 Validator
services.AddValidatorsFromAssemblyContaining<RecipeValidator>();
// 或手動註冊
services.AddScoped<IValidator<Recipe>, RecipeValidator>();
在 Service 中注入使用
public class RecipeService
{
private readonly IValidator<Recipe> _validator;
public RecipeService(IValidator<Recipe> validator)
{
_validator = validator;
}
public async Task SaveRecipeAsync(Recipe recipe)
{
var result = await _validator.ValidateAsync(recipe);
if (!result.IsValid)
throw new ValidationException(result.Errors);
await _repository.SaveAsync(recipe);
}
}
與 WPF 整合:INotifyDataErrorInfo
WPF 的 {Binding} 支援 INotifyDataErrorInfo 來顯示驗證錯誤。
搭配 CommunityToolkit.Mvvm
public partial class RecipeEditorViewModel : ObservableValidator
{
private readonly RecipeValidator _validator = new();
[ObservableProperty]
private string _recipeName = "";
partial void OnRecipeNameChanged(string value)
{
ValidateProperty(value);
}
private void ValidateProperty(object value,
[CallerMemberName] string propertyName = "")
{
ClearErrors(propertyName);
var result = _validator.Validate(
new Recipe { Name = RecipeName /* ... */ });
foreach (var error in result.Errors
.Where(e => e.PropertyName == propertyName))
{
SetErrors(propertyName, new[] { error.ErrorMessage });
}
}
}
XAML 顯示錯誤
<TextBox Text="{Binding RecipeName, UpdateSourceTrigger=PropertyChanged,
ValidatesOnNotifyDataErrors=True}" />
<!-- 預設會在 TextBox 周圍顯示紅色框線 -->
與 ReactiveUI 整合:響應式驗證
搭配 ReactiveUI 可以做到即時響應式驗證:
public class RecipeEditorViewModel : ReactiveObject
{
[Reactive] public string RecipeName { get; set; } = "";
[Reactive] public double Temperature { get; set; }
[ObservableAsProperty] public string NameError { get; }
[ObservableAsProperty] public string TemperatureError { get; }
[ObservableAsProperty] public bool IsValid { get; }
public RecipeEditorViewModel()
{
var validator = new RecipeValidator();
// 任何屬性變化都觸發驗證
var validationResult = this.WhenAnyValue(
x => x.RecipeName,
x => x.Temperature)
.Throttle(TimeSpan.FromMilliseconds(200))
.Select(_ => validator.Validate(
new Recipe { Name = RecipeName, Temperature = Temperature }));
// 將錯誤訊息綁定到 UI
validationResult
.Select(r => r.Errors
.FirstOrDefault(e => e.PropertyName == nameof(RecipeName))
?.ErrorMessage ?? "")
.ToPropertyEx(this, x => x.NameError);
validationResult
.Select(r => r.Errors
.FirstOrDefault(e => e.PropertyName == nameof(Temperature))
?.ErrorMessage ?? "")
.ToPropertyEx(this, x => x.TemperatureError);
validationResult
.Select(r => r.IsValid)
.ToPropertyEx(this, x => x.IsValid);
}
}
測試
FluentValidation 提供便利的測試擴充方法。
dotnet add package FluentValidation # 測試方法已含在核心套件
基本測試
[TestClass]
public class RecipeValidatorTests
{
private readonly RecipeValidator _validator = new();
[TestMethod]
public void Name_WhenEmpty_ShouldHaveError()
{
var recipe = new Recipe { Name = "", Temperature = 100 };
var result = _validator.TestValidate(recipe);
result.ShouldHaveValidationErrorFor(x => x.Name)
.WithErrorMessage("名稱不可為空");
}
[TestMethod]
public void Name_WhenValid_ShouldNotHaveError()
{
var recipe = new Recipe { Name = "測試 Recipe", Temperature = 100 };
var result = _validator.TestValidate(recipe);
result.ShouldNotHaveValidationErrorFor(x => x.Name);
}
[TestMethod]
public void Temperature_WhenOutOfRange_ShouldHaveError()
{
var recipe = new Recipe { Name = "Test", Temperature = 600 };
var result = _validator.TestValidate(recipe);
result.ShouldHaveValidationErrorFor(x => x.Temperature);
}
[TestMethod]
public void Temperature_WhenInRange_ShouldNotHaveError()
{
var recipe = new Recipe { Name = "Test", Temperature = 200 };
var result = _validator.TestValidate(recipe);
result.ShouldNotHaveValidationErrorFor(x => x.Temperature);
}
[TestMethod]
public void Steps_WhenEmpty_ShouldHaveError()
{
var recipe = new Recipe
{
Name = "Test",
Temperature = 100,
Steps = new List<RecipeStep>()
};
var result = _validator.TestValidate(recipe);
result.ShouldHaveValidationErrorFor(x => x.Steps)
.WithErrorMessage("Recipe 至少需要一個步驟");
}
}
測試條件驗證
[TestMethod]
public void CoolingRate_WhenCoolingMode_ShouldValidate()
{
var param = new ProcessParameter
{
Mode = ProcessMode.Cooling,
CoolingRate = 0 // 無效
};
var result = _validator.TestValidate(param);
result.ShouldHaveValidationErrorFor(x => x.CoolingRate);
}
[TestMethod]
public void CoolingRate_WhenHeatingMode_ShouldSkipValidation()
{
var param = new ProcessParameter
{
Mode = ProcessMode.Heating,
CoolingRate = 0 // 加熱模式下不驗證此欄位
};
var result = _validator.TestValidate(param);
result.ShouldNotHaveValidationErrorFor(x => x.CoolingRate);
}
延伸閱讀:
- 概觀 — 核心概念與內建驗證器
- ReactiveUI 核心概念 — 搭配響應式驗證
- ReactiveUI 場景 Pattern — Recipe Editor Pattern