1. 引言
在.NET开发领域,FluentValidation以其优雅、易扩展的特性成为开发者进行属性验证的首选工具。它不仅适用于Web开发,如MVC、Web API和ASP.NET CORE,同样也能完美集成在WPF应用程序中,提供强大的数据验证功能。本文将深入探讨如何在C# WPF项目中运用FluentValidation进行属性验证,并展示如何通过MVVM模式实现这一功能。
2. 功能概覽
我們的目標是構建一個wpf應用程式,它能夠通過fluentvalidation實現以下驗證功能:
- 驗證viewmodel層的基本數據類型屬性,如int、string等。
- 對viewmodel中的複雜屬性進行驗證,這包括對象屬性的子屬性以及集合屬性。
- 提供兩種直觀的錯誤提示樣式,以增強用戶體驗。
先看實現效果圖:

3. 解決問題與探索
在調研過程中,我發現fluentvalidation官方文檔主要關注於web應用的驗證。對於wpf和複雜屬性的驗證,官方文檔提供的示例有限。然而,通過深入研究和實踐,我找到了將fluentvalidation與wpf結合使用的有效方法,特別是針對複雜屬性的驗證。
4. 開發步驟
4.1.創建工程、庫引入
首先,创建一个新的WPF项目,并引入FluentValidation库用于属性验证,以及Prism.Wpf库以简化MVVM模式的实现。
<ItemGroup>
<PackageReference Include="FluentValidation" Version="11.9.0" />
<PackageReference Include="Prism.Wpf" Version="9.0.271-pre" />
</ItemGroup>
4.2.創建實體類
我創建了兩個實體類:student和field,分別代表對象屬性和集合項屬性。這兩個類都實現了idataerrorinfo接口:
IDataErrorInfo接口常用于提供实体数据验证的错误信息。这个接口包含两个成员:一个索引器(this[string columnName])和一个Error属性。索引器用于按属性名称提供错误信息,而Error属性则用于提供整个实体的错误概述。- 两个实体类和另外在后面提及的ViewModel中也实现
IDataErrorInfo接口,并在this[string columnName]索引器和Error属性中使用FluentValidation来验证属性。
4.2.1.普通類 - student
學生類包含5個屬性:名字、年齡、郵政編碼、最小值和最大值,其中最小值和最大值涉及關聯驗證,即最小值變化後通知最大值驗證,反之同理。
/// <summary>
/// 学生实体
/// 继承BindableBase,即继承属性变化接口INotifyPropertyChanged
/// 实现IDataErrorInfo接口,用于FluentValidation验证,必须实现此接口
/// </summary>
public class Student : BindableBase, IDataErrorInfo
{
private int _age;
private string? _name;
private string? _zip;
private readonly StudentValidator _validator = new();
public string? Name
{
get => _name;
set => SetProperty(ref _name, value);
}
public int Age
{
get => _age;
set => SetProperty(ref _age, value);
}
public string? Zip
{
get => _zip;
set => SetProperty(ref _zip, value);
}
private int _minValue;
public int MinValue
{
get => _minValue;
set
{
SetProperty(ref _minValue, value);
// 关联更新最大值验证
RaisePropertyChanged(nameof(MaxValue));
}
}
private int _maxValue;
public int MaxValue
{
get => _maxValue;
set
{
SetProperty(ref _maxValue, value);
// 关联更新最小值验证
RaisePropertyChanged(nameof(MinValue));
}
}
public string this[string columnName]
{
get
{
var validateResult = _validator.Validate(this);
if (validateResult.IsValid)
{
return string.Empty;
}
var firstOrDefault =
validateResult.Errors.FirstOrDefault(error => error.PropertyName == columnName);
return firstOrDefault == null ? string.Empty : firstOrDefault.ErrorMessage;
}
}
public string Error
{
get
{
var validateResult = _validator.Validate(this);
if (validateResult.IsValid)
{
return string.Empty;
}
var errors = string.Join(Environment.NewLine, validateResult.Errors.Select(x => x.ErrorMessage).ToArray());
return errors;
}
}
}
上面关键代码在public string this[string columnName]:这里进行输入表单项的数据校验,FluentValidation调用就在这里,校验逻辑封装在StudentValidator,表单输入时会实时调用该处代码,columnName表示表单项的列名,就是View绑定的属性名。
4.2.2.集合類 - field
此類用作viewmodel中的集合項使用,模擬動態表單數據校驗,簡單包含4個屬性:欄位名稱、欄位顯示名稱、數據類型、數據值,表單主要根據數據類型驗證輸入的數據值是否合法。同樣此實體需要繼承idataerrorinfo接口,用於觸發fluentvalidation驗證使用。
/// <summary>
/// 扩展字段,用于生成动态表单
/// 继承BindableBase,即继承属性变化接口INotifyPropertyChanged
/// 实现IDataErrorInfo接口,用于FluentValidation验证,必须实现此接口
/// </summary>
public class Field : BindableBase, IDataErrorInfo
{
private string? _value;
private readonly FieldValidator _validator = new();
public Field(DataType type, string typeLabel, string name, string value)
{
Type = type;
TypeLabel = typeLabel;
Name = name;
Value = value;
}
/// <summary>
/// 数据类型
/// </summary>
public DataType Type { get; set; }
/// <summary>
/// 数据类型名称
/// </summary>
public string TypeLabel { get; set; }
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 值
/// </summary>
public string? Value
{
get => _value;
set => SetProperty(ref _value, value);
}
public string this[string columnName]
{
get
{
var validateResult = _validator.Validate(this);
if (validateResult.IsValid)
{
return string.Empty;
}
var firstOrDefault =
validateResult.Errors.FirstOrDefault(error => error.PropertyName == columnName);
return firstOrDefault == null ? string.Empty : firstOrDefault.ErrorMessage;
}
}
public string Error
{
get
{
var validateResult = _validator.Validate(this);
if (validateResult.IsValid)
{
return string.Empty;
}
var errors = string.Join(Environment.NewLine, validateResult.Errors.Select(x => x.ErrorMessage).ToArray());
return errors;
}
}
}
public enum DataType
{
Text,
Number,
Date
}
看上面代码,public string this[string columnName]代码处写法和Student类一样,只是_validator变量类型不同,前者为StudentValidator,这里是FieldValidator,下面我们看看这两个类怎么写。
4.3.創建驗證器
对于每个实体类,我都创建了一个对应的验证器类:StudentValidator和FieldValidator。这些验证器类继承自AbstractValidator,并在其中定义了验证规则。验证属性的写法有两种:
可以在實體屬性上方添加特性(本文不作特別說明,百度文章居間很多);
通過代碼的形式添加,如下方,創建一個驗證器類,繼承自abstractvalidator,在此驗證器構造函數中寫規則驗證屬性,方便管理。
本文使用第二种,下面通过创建StudentValidator和FieldValidator两个验证器类介绍。
4.3.1. StudentValidator
这是学生验证器StudentValidator,需要继承AbstractValidator,泛型指定前面需要验证的实体类Student:
public class StudentValidator : AbstractValidator<Student>
{
public StudentValidator()
{
RuleFor(vm => vm.Name)
.NotEmpty()
.WithMessage("请输入学生姓名!")
.Length(5, 30)
.WithMessage("学生姓名长度限制在5到30个字符之间!");
RuleFor(vm => vm.Age)
.GreaterThanOrEqualTo(0)
.WithMessage("学生年龄为整数!")
.ExclusiveBetween(10, 150)
.WithMessage("请正确输入学生年龄(10-150)");
_ = RuleFor(vm => vm.Zip)
.NotEmpty()
.WithMessage("邮政编码不能为空!")
.Must(BeAValidZip)
.WithMessage("邮政编码由六位数字组成。");
RuleFor(model => model.MinValue).Must((model, minValue) => minValue < model.MaxValue).WithMessage("最小值应该小于最大值");
RuleFor(model => model.MaxValue).Must((model, maxValue) => maxValue > model.MinValue).WithMessage("最大值应该大于最小值");
}
private static bool BeAValidZip(string? zip)
{
if (string.IsNullOrEmpty(zip))
{
return false;
}
var regex = new Regex(@"\d{6}");
return regex.IsMatch(zip);
}
}
代码简单,使用到数字的大小和范围验证(见Age)、字符串不能为空和长度限制(见Name)、字符串正则表达式验证(见Zip)、多属性关联验证(最小值和最大值,这里配合属性set时通知其他属性验证通知RaisePropertyChanged(nameof(MaxValue));)。
4.3.2. FieldValidator
动态表单数据值校验器,同理需要继承AbstractValidator,泛型指定前面需要验证的实体类Field::
public class FieldValidator : AbstractValidator<Field>
{
public FieldValidator()
{
RuleFor(field => field.Value)
.Must((field, value) => (field.Type == DataType.Text && !string.IsNullOrWhiteSpace(value))
|| (field.Type == DataType.Number && double.TryParse(value, out _))
|| (field.Type == DataType.Date && DateTime.TryParse(value, out _)))
.WithMessage("1.文本不能为空;2.数字类型请填写数字;3.日志类型请填写日期类型");
}
}
這裡寫的簡單了點:
- 文本數據類型,值不能為空;
- 数字数据类型,必须是
double类型; - 日期类型,必须能使用
DateTime转换;
本文只做简单演示,多种数据类型放Must方法中做统一验证,验证出错给出统一的提示信息,读者可按实际情况修改。
4.3.3. StudentViewModelValidator
此外,我还创建了一个StudentViewModelValidator,用于验证ViewModel层的属性。这个验证器能够处理基本数据类型、对象属性以及集合属性的验证。
public class StudentViewModelValidator : AbstractValidator<StudentViewModel>
{
public StudentViewModelValidator()
{
RuleFor(vm => vm.Title)
.NotEmpty()
.WithMessage("标题长度不能为空!")
.Length(5, 30)
.WithMessage("标题长度限制在5到30个字符之间!");
RuleFor(vm => vm.CurrentStudent).SetValidator(new StudentValidator());
RuleForEach(vm => vm.Fields).SetValidator(new FieldValidator());
}
}
Title用于关联验证基本数据类型(string类型);CurrentStudent用于验证对象属性(Student类的实例),设置验证该属性时使用StudentValidator验证器;Fields用于验证集合属性(ObservableCollection<Field>),设置验证该属性子项时使用FieldValidator验证器,注意前面使用的RuleForEach表示关联集合中的项验证器。
4.4. viewmodel層實現
StudentViewModel与Student实体类结构类似,都需要实现IDataErrorInfo接口,该类由一个简单的string属性(Title)和一个复杂的Student对象属性(CurrentStudent)、集合属性ObservableCollection<Field> Fields组成,代码如下:
/// <summary>
/// 视图ViewModel
/// 继承BindableBase,即继承属性变化接口INotifyPropertyChanged
/// 实现IDataErrorInfo接口,用于FluentValidation验证,必须实现此接口
/// </summary>
public class StudentViewModel : BindableBase, IDataErrorInfo
{
private Student _currentStudent;
private string _title;
private readonly StudentViewModelValidator _validator;
public string Title
{
get => _title;
set => SetProperty(ref _title, value);
}
public Student CurrentStudent
{
get => _currentStudent;
set => SetProperty(ref _currentStudent, value);
}
public ObservableCollection<Field> Fields { get; } = new();
private DelegateCommand _saveCommand;
public DelegateCommand SaveCommand => _saveCommand ??= new DelegateCommand(HandleSaveCommand,
HandleCanExecuteSaveCommand);
private DelegateCommand _cancelCommand;
public DelegateCommand CancelCommand =>
_cancelCommand ??= new DelegateCommand(HandleCancelCommand, () => true);
public StudentViewModel()
{
_validator = new StudentViewModelValidator();
CurrentStudent = new Student
{
Name = "李刚的儿",
Age = 23
};
Fields.Add(new Field(DataType.Text, "文本,比如:四川省成都市场", "地址", ""));
Fields.Add(new Field(DataType.Number, "数字,比如:12", "工龄", ""));
Fields.Add(new Field(DataType.Date, "时间,比如:2023-09-26 05:13:23", "培训时间", ""));
PropertyChanged += Validate;
CurrentStudent.PropertyChanged += Validate;
foreach (var field in Fields)
{
field.PropertyChanged += Validate;
}
}
~StudentViewModel()
{
PropertyChanged -= Validate;
CurrentStudent.PropertyChanged -= Validate;
foreach (var field in Fields)
{
field.PropertyChanged -= Validate;
}
}
private void Validate(object sender, PropertyChangedEventArgs e)
{
_isCanExecuteSaveCommand = _validator.Validate(this).IsValid;
SaveCommand.RaiseCanExecuteChanged();
}
private void HandleSaveCommand()
{
var validateResult = _validator.Validate(this);
if (validateResult.IsValid)
{
MessageBox.Show("看到我说明验证成功!");
}
else
{
var errorMsg = string.Join(Environment.NewLine,
validateResult.Errors.Select(x => x.ErrorMessage).ToArray());
MessageBox.Show($"慌啥子嘛,你再检查下输入噻:\r\n{errorMsg}");
}
}
private bool _isCanExecuteSaveCommand;
private bool HandleCanExecuteSaveCommand()
{
return _isCanExecuteSaveCommand;
}
private void HandleCancelCommand()
{
MessageBox.Show("我啥都不做,退休了");
}
public string this[string columnName]
{
get
{
var validateResult = _validator.Validate(this);
if (validateResult.IsValid)
{
return string.Empty;
}
var firstOrDefault =
validateResult.Errors.FirstOrDefault(error => error.PropertyName == columnName);
return firstOrDefault == null ? string.Empty : firstOrDefault.ErrorMessage;
}
}
public string Error
{
get
{
var validateResult = _validator.Validate(this);
if (validateResult.IsValid)
{
return string.Empty;
}
var errors = string.Join(Environment.NewLine, validateResult.Errors.Select(x => x.ErrorMessage).ToArray());
return errors;
}
}
}
ViewModel属性验证和Student及Field类似,这里我加上了保存(SaveCommand)和取消(CancelCommand)两个命令,其中保存命令需要所有属性验证通过才可用,通过注册属性的变化事件PropertyChanged,在变化事件处理程序中验证:
PropertyChanged += Validate;
CurrentStudent.PropertyChanged += Validate;
foreach (var field in Fields)
{
field.PropertyChanged += Validate;
}
private void Validate(object sender, PropertyChangedEventArgs e)
{
_isCanExecuteSaveCommand = _validator.Validate(this).IsValid;
SaveCommand.RaiseCanExecuteChanged();
}
4.5.視圖層實現
在视图层,我创建了一个用户控件StudentView,用于显示输入表单和验证结果。通过绑定ViewModel层的属性和命令,视图层能够与ViewModel层进行交互,并实时显示验证错误。这里比较简单,提供简单属性标题(Title)、复杂属性(包括学生姓名(CurrentStudent.Name)、学生年龄( CurrentStudent .Age)、学生邮政编码( CurrentStudent .Zip)、最小值(CurrentStudent.MinValue)、最大值(CurrentStudent.MaxValue))验证、集合属性验证(Fields),xaml代码如下:
<UserControl
x:Class="WpfFluentValidation.Views.StudentView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="clr-namespace:WpfFluentValidation.Models"
xmlns:vm="clr-namespace:WpfFluentValidation.ViewModels"
mc:Ignorable="d" Padding="10">
<UserControl.DataContext>
<vm:StudentViewModel />
</UserControl.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
<ScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Auto">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<GroupBox Header="ViewModel直接属性验证">
<StackPanel>
<Label Content="标题:" />
<TextBox Style="{StaticResource Styles.TextBox.ErrorStyle1}"
Text="{Binding Title, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</StackPanel>
</GroupBox>
<GroupBox Grid.Row="1" Header="ViewModel对象属性CurrentStudent的属性验证">
<StackPanel>
<StackPanel>
<Label Content="姓名:" />
<TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
Text="{Binding CurrentStudent.Name, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</StackPanel>
<StackPanel>
<Label Content="年龄:" />
<TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
Text="{Binding CurrentStudent.Age, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</StackPanel>
<StackPanel>
<Label Content="邮编:" />
<TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
Text="{Binding CurrentStudent.Zip, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</StackPanel>
<StackPanel>
<Label Content="最小值:" />
<TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
Text="{Binding CurrentStudent.MinValue, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</StackPanel>
<StackPanel>
<Label Content="最大值:" />
<TextBox Style="{StaticResource Styles.TextBox.ErrorStyle2}"
Text="{Binding CurrentStudent.MaxValue, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</StackPanel>
</StackPanel>
</GroupBox>
<GroupBox Grid.Row="2" Header="ViewModel集合属性Fields的属性验证">
<ItemsControl ItemsSource="{Binding Fields}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type models:Field}">
<Border Padding="10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Margin="0,0,0,5">
<Run Text="{Binding Name}" />
<Run Text="(" />
<Run Text="{Binding TypeLabel}" />
<Run Text=")" />
</TextBlock>
<TextBox Grid.Row="1" Style="{StaticResource Styles.TextBox.ErrorStyle2}"
Text="{Binding Value, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}" />
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</GroupBox>
</Grid>
</ScrollViewer>
<StackPanel Grid.Row="1" HorizontalAlignment="Right" Orientation="Horizontal">
<Button Content="取消" Command="{Binding CancelCommand}" Style="{StaticResource Styles.Button.Common}"
Margin="0 3 40 3" />
<Button Content="提交" Command="{Binding SaveCommand}" Style="{StaticResource Styles.Button.Blue}"
Margin="0 3 10 3" />
</StackPanel>
</Grid>
</UserControl>
4.6.錯誤提示樣式
為了提升用戶體驗,我定義了兩種錯誤提示樣式:一種是通過紅色圖標提示輸入框旁邊的錯誤,另一種是在輸入框右側顯示錯誤文字。這些樣式定義在app.xaml中,並可以在整個應用程式中復用。
<Application
x:Class="WpfFluentValidation.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
<Style TargetType="StackPanel">
<Setter Property="Margin" Value="0,5" />
</Style>
<!-- 第一种错误样式,红色边框 -->
<Style x:Key="Styles.TextBox.ErrorStyle1" TargetType="{x:Type TextBox}">
<Setter Property="Width" Value="250" />
<Setter Property="Height" Value="25" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel>
<Grid
Width="16"
Height="16"
Margin="3,0,0,0"
VerticalAlignment="Center"
DockPanel.Dock="Right">
<Ellipse
Width="16"
Height="16"
Fill="Red" />
<Ellipse
Width="3"
Height="8"
Margin="0,2,0,0"
HorizontalAlignment="Center"
VerticalAlignment="Top"
Fill="White" />
<Ellipse
Width="2"
Height="2"
Margin="0,0,0,2"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
Fill="White" />
</Grid>
<Border
BorderBrush="Red"
BorderThickness="2"
CornerRadius="2">
<AdornedElementPlaceholder />
</Border>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
<!-- 第二种错误样式,右键文字提示 -->
<Style x:Key="Styles.TextBox.ErrorStyle2" TargetType="{x:Type TextBox}">
<Setter Property="Width" Value="250" />
<Setter Property="Height" Value="25" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Padding" Value="5,0" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<StackPanel Orientation="Horizontal">
<AdornedElementPlaceholder x:Name="textBox" />
<Grid>
<TextBlock Margin="10 0 0 0" Width="130"
Foreground="Red" TextWrapping="Wrap"
Text="{Binding [0].ErrorContent}" />
</Grid>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}" />
<Setter Property="Background" Value="LightPink" />
<Setter Property="BorderBrush" Value="Red" />
<Setter Property="Foreground" Value="White" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="GroupBox">
<Setter Property="Margin" Value="5" />
<Setter Property="Padding" Value="2" />
<Setter Property="BorderBrush" Value="#FF0078D7" />
<Setter Property="BorderThickness" Value="2" />
<Setter Property="Background" Value="#FFF0F0F0" />
<Setter Property="Foreground" Value="#FF0078D7" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<Style x:Key="Styles.Button.Common" TargetType="{x:Type Button}">
<Setter Property="MinWidth" Value="75" />
<Setter Property="MinHeight" Value="25" />
<Setter Property="Background" Value="White" />
<Setter Property="Foreground" Value="Black" />
</Style>
<Style
x:Key="Styles.Button.Blue"
BasedOn="{StaticResource ResourceKey=Styles.Button.Common}"
TargetType="{x:Type Button}">
<Setter Property="Background" Value="Green" />
<Setter Property="Foreground" Value="White" />
</Style>
</Application.Resources>
</Application>
5. 效果展示
通過上述步驟的實現,我們得到了一個功能完善的wpf應用程式。它能夠根據用戶輸入實時進行驗證,並提供直觀的錯誤提示。當所有屬性都驗證通過時,提交按鈕將變為可用狀態。

6. 源碼分享
為了方便讀者學習和交流,本文將所有代碼同步到了gitee和github平台上。歡迎感興趣的開發者訪問以下連結獲取源碼:
- gitee: https://gitee.com/dotnet9/FluentValidationForWpf
- github: https://github.com/dotnet9/FluentValidationForWPF
7. 總結
通過本文的居間和實踐,我們成功將fluentvalidation應用於c# wpf項目中,實現了對viewmodel層屬性的全面驗證。這不僅提升了數據的安全性和準確性,也為用戶提供了更好的交互體驗。希望本文能對廣大開發者在wpf項目中使用fluentvalidation提供有益的參考和啟示。
參考:
- fluentvalidation官網:https://fluentvalidation.net/