注:本文隸屬于《了解ASP.NET Core》系列文章,請檢視置頂部落格或點選此處檢視全文目錄
模型綁定
什麼是模型綁定?簡單說就是将HTTP請求參數綁定到程式方法入參上,該變量可以是簡單類型,也可以是複雜類。
綁定源
所謂綁定源,是指用于模型綁定的值來源。
先舉個例子:
[Route("api/[controller]")]
public class UserController : ControllerBase
{
[Route("{id}")]
public string Get([FromRoute] string id)
{
return id;
}
}
就拿上面的例子來說,
Get
方法的參數
id
,被
[FromRoute]
标注,表示其綁定源是路由。當然,綁定源不僅僅隻有這一種:
-
:從Url的查詢字元串中擷取值。查詢字元串就是Url中問号([FromQuery]
)後面拼接的參數?
-
:從路由資料中擷取值。例如上例中的[FromRoute]
{id}
-
:從表單中擷取值。[FromForm]
-
:從請求正文中擷取值。[FromBody]
-
:從請求标頭中擷取值。[FromHeader]
-
:從DI容器中擷取服務。相比其他源,它特殊在值不是來源于HTTP請求,而是DI容器。[FromServices]
建議大家在編寫接口時,盡量顯式指明綁定源。
在綁定的時候,可能會遇到以下兩種情況:
情況一:模型屬性在綁定源中不存在
什麼是模型屬性在綁定源中不存在?給大家舉個例子:
[HttpPost]
public string Post1([FromForm] CreateUserDto input)
{
return JsonSerializer.Serialize(input);
}
[HttpPost]
public string Post2([FromRoute]int[] numbers)
{
return JsonSerializer.Serialize(numbers);
}
如
Post2
方法的模型屬性
numbers
要求從路由中尋找值,但是很明顯我們的路由中并未提供,這種情況就是模型屬性在綁定源中不存在。
預設的,若模型屬性在綁定源中不存在,且不加任何驗證條件時,不會将其标記為模型狀态錯誤,而是會将該屬性設定為
null
或預設值:
- 可以為Null的簡單類型設定為
null
- 不可為Null的值類型設定為
default
- 如果是複雜類型,則通過預設構造函數建立該執行個體。如例子中的
,如果我們沒有通過表單傳值,你會發現會得到一個使用Post1
預設構造函數建立的執行個體。CreateUserDto
- 數組則設定為
,不過Array.Empty<T>()
數組設定為byte[]
。如例子中的null
,你會得到一個空數組。Post2
情況二:綁定源無法轉換為模型中的目标類型
比如,當嘗試将綁定源中的字元串
abc
轉換為模型中的值類型
int
時,會發生類型轉換錯誤,此時,會将該模型狀态标記為無效。
綁定格式
int
、
string
、模型類等綁定格式大家已經很熟悉了,我就不再贅述了。這次,隻給大家介紹一些比較特殊的綁定格式。
集合
假設存在以下接口,接口參數是一個數組:
public string[] Post([FromQuery] string[] ids)
public string[] Post([FromForm] string[] ids)
參數為:[1,2]
為了将參數綁定到數組
ids
上,你可以通過表單或查詢字元串傳入,可以采用以下格式之一:
-
ids=1&ids=2
-
ids[0]=1&ids[1]=2
-
[0]=1&[1]=2
-
ids[a]=1&ids[b]=2&ids.index=a&ids.index=b
-
[a]=1&[b]=2&index=a&index=b
此外,表單還可以支援一種格式:
ids[]=1&ids[]=2
如果通過查詢字元串傳遞請求參數,你就要注意,由于浏覽器對于Url的長度是有限制的,若傳遞的集合過長,超過了長度限制,就會有截斷的風險。是以,建議将該集合放到一個模型類裡面,該模型類作為接口參數。
字典
假設存在以下接口,接口參數是一個字典:
public Dictionary<int, string> Post([FromQuery] Dictionary<int, string> idNames)
參數為:{ [1] = "j", [2] = "k" }
為了将參數綁定到字典
idNames
-
,注意:方括号中的數字是字典的keyidNames[1]=j&idNames[2]=k
-
[1]=j&[2]=k
-
,注意:方括号中的數字是索引,不是字典的keyidNames[0].key=1&idNames[0].value=j&idNames[1].key=2&idNames[1].value=k
-
[0].key=1&[0].value=j&[1].key=2&[1].value=k
同樣,請注意Url長度限制問題。
模型驗證
聊完了模型綁定,那接下來就是要驗證綁定的模型是否有效。
假設
UserController
中存在一個
Post
方法:
public class UserController : ControllerBase
{
[HttpPost]
public string Post([FromBody] CreateUserDto input)
{
// 模型狀态無效,傳回錯誤消息
if (!ModelState.IsValid)
{
return "模型狀态無效:"
+ string.Join(Environment.NewLine,
ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage)));
}
return JsonSerializer.Serialize(input);
}
}
public class CreateUserDto
{
public int Age { get; set; }
}
現在,我們請求
Post
,傳入以下參數:
{
"age":"abc"
}
會得到如下響應:
模型狀态無效:The JSON value could not be converted to System.Int32. Path: $.age | LineNumber: 1 | BytePositionInLine: 15.
我們得到了模型狀态無效的錯誤消息,這是因為字元串
“abc”
無法轉換為
int
類型。
你也看到了,我們通過
ModelState.IsValid
來檢查模型狀态是否有效。
另外,對于Web Api應用,由于标記了
[ApiController]
特性,其會自動執行
ModelState.IsValid
檢察,詳細說明檢視Web Api中的模型驗證
ModelStateDictionary
ModelState
的類型為
ModelStateDictionary
,也就是一個字典,
Key
就是無效節點的辨別,
Value
就是無效節點詳情。
我們一起看一下
ModelStateDictionary
的核心類結構:
public class ModelStateDictionary : IReadOnlyDictionary<string, ModelStateEntry>
{
public static readonly int DefaultMaxAllowedErrors = 200;
public ModelStateDictionary()
: this(DefaultMaxAllowedErrors) { }
public ModelStateDictionary(int maxAllowedErrors) { ... }
public ModelStateDictionary(ModelStateDictionary dictionary)
: this(dictionary?.MaxAllowedErrors ?? DefaultMaxAllowedErrors) { ... }
public ModelStateEntry Root { get; }
// 允許的模型狀态最大錯誤數量,預設是 200
public int MaxAllowedErrors { get; set; }
// 訓示模型狀态錯誤數量是否達到最大值
public bool HasReachedMaxErrors { get; }
// 通過`AddModelError`或`TryAddModelError`方法添加的錯誤數量
public int ErrorCount { get; }
// 無效節點的數量
public int Count { get; }
public KeyEnumerable Keys { get; }
IEnumerable<string> IReadOnlyDictionary<string, ModelStateEntry>.Keys => Keys;
public ValueEnumerable Values { get; }
IEnumerable<ModelStateEntry> IReadOnlyDictionary<string, ModelStateEntry>.Values => Values;
// 枚舉,模型驗證狀态,有 Unvalidated、Invalid、Valid、Skipped 共4種
public ModelValidationState ValidationState { get; }
// 訓示模型狀态是否有效,當驗證狀态為 Valid 和 Skipped 有效
public bool IsValid { get; }
public ModelStateEntry this[string key] { get; }
}
-
:允許的模型狀态錯誤數量,預設是 200。MaxAllowedErrors
- 當錯誤數量達到
時,若還要添加錯誤,則該錯誤不會被添加,而是添加一個MaxAllowedErrors - 1
錯誤TooManyModelErrorsException
- 可以通過
或AddModelError
方法添加錯誤TryAddModelError
- 另外,若是直接修改
,那錯誤數量不會受該屬性限制ModelStateEntry
- 當錯誤數量達到
-
:模型驗證狀态ValidationState
-
:未驗證。當模型尚未進行驗證或任意一個Unvalidated
驗證狀态為ModelStateEntry
時,該值為未驗證。Unvalidated
-
:無效。當模型已驗證完畢(即沒有Invalid
ModelStateEntry
)并且任意一個Unvalidated
ModelStateEntry
,該值為無效。Invalid
-
:有效。當模型已驗證完畢,且所有Valid
驗證狀态僅包含ModelStateEntry
和Valid
時,該值為有效。Skipped
-
:跳過。整個模型跳過驗證時,該值為跳過。Skipped
-
重新驗證
預設情況下,模型驗證是自動進行的。不過有時,需要為模型進行一番自定義操作後,重新進行模型驗證。可以先通過
ModelStateDictionary.ClearValidationState
方法清除驗證狀态,然後調用
ControllerBase.TryValidateModel
方法重新驗證:
public class CreateUserDto
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
}
[HttpPost]
public string Post([FromBody] CreateUserDto input)
{
if (input.FirstName is null)
{
input.FirstName = "first";
}
if (input.LastName is null)
{
input.LastName = "last";
}
// 先清除驗證狀态
ModelState.ClearValidationState(string.Empty);
// 重新進行驗證
if (!TryValidateModel(input, string.Empty))
{
return "模型狀态無效:"
+ string.Join(Environment.NewLine,
ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage)));
}
return JsonSerializer.Serialize(input);
}
驗證特性
針對一些常用的驗證:如判斷是否為
null
、字元串格式是否為郵箱等,為了減少大家的工作量,減少代碼備援,可以通過特性的方式在模型的屬性上進行标注。
微軟為我們内置了一部分驗證特性,位于
System.ComponentModel.DataAnnotations
命名空間下(隻列舉一部分):
-
:驗證屬性是否為[Required]
。該特性作用在可為null
的資料類型上才有效null
- 作用于字元串類型時,允許使用
屬性訓示是否允許空字元串,預設AllowEmptyStrings
false
- 作用于字元串類型時,允許使用
-
:驗證字元串屬性的長度是否在指定範圍内[StringLength]
-
:驗證數值屬性是否在指定範圍内[Range]
-
:驗證屬性的格式是否為URL[Url]
-
:驗證屬性的格式是否為電話号碼[Phone]
-
:驗證屬性的格式是否為郵箱位址[EmailAddress]
-
:驗證目前屬性和指定的屬性是否比對[Compare]
-
:驗證屬性是否和正規表達式比對[RegularExpression]
大家一定或多或少都接觸過這些特性。不過,我并不打算詳細介紹這些特性的使用,因為這些特性的局限性較高,不夠靈活。
那有沒有更好用的呢?當然有,接下來就給大家介紹一款驗證庫——
FluentValidation
!
FluentValidation
FluentValidation
是一款免費開源的模型驗證庫,通過它,你可以使用Fluent接口和Lambda表達式來建構強類型的驗證規則。
- 檢視github
- 檢視官網
接下來,跟我一起感受
FluentValidation
的魅力吧!
為了更好的展示,我們先豐富一下
CreateUserDto
:
public class CreateUserDto
{
public string Name { get; set; }
public int Age { get; set; }
}
安裝
今天,我們要安裝兩個包,分别是
FluentValidation
FluentValidation.AspNetCore
(後者依賴前者):
- FluentValidation:是整個驗證庫的核心
- FluentValidation.AspNetCore:用于與ASP.NET Core內建
選擇你喜歡的安裝方式:
- 方式1:通過NuGet安裝:
Install-Package FluentValidation
Install-Package FluentValidation.AspNetCore
- 方式2:通過CLI安裝
dotnet add package FluentValidation
dotnet add package FluentValidation.AspNetCore
建立 CreateUserDto 的驗證器
為了配置
CreateUserDto
各個屬性的驗證規則,我們需要為它建立一個驗證器(validator),該驗證器繼承自抽象類
AbstractValidator<T>
,
T
就是你要驗證的類型,這裡就是
CreateUserDto
。
public class CreateUserDtoValidator : AbstractValidator<CreateUserDto>
{
public CreateUserDtoValidator()
{
RuleFor(x => x.Name).NotEmpty();
RuleFor(x => x.Age).GreaterThan(0);
}
}
驗證器很簡單,隻有一個構造函數,所有的驗證規則,都将寫入到該構造函數中。
通過
RuleFor
并傳入Lambda表達式為指定屬性設定驗證規則,然後,就可以以Fluent的方式添加驗證規則。這裡我添加了兩個驗證規則:Name 不能為空、Age 必須大于 0
現在,改寫一下
Post
[HttpPost]
public string Post([FromBody] CreateUserDto input)
{
var validator = new CreateUserDtoValidator();
var result = validator.Validate(input);
if (!result.IsValid)
{
return $"模型狀态無效:{result}";
}
return JsonSerializer.Serialize(input);
}
方法,可以将所有錯誤消息組合為一條錯誤消息,預設分隔符是換行(
ValidationResult.ToString
),但是你也可以傳入自定義分隔符。
Environment.NewLine
當我們傳入一個空的json對象時,會得到以下響應:
模型狀态無效:Name' 不能為空。
'Age' 必須大于 '0'。
雖然我們已經基本實作了驗證功能,但是不免有人會吐槽:驗證代碼也太多了吧,而且還要手動 new 一個指定類型的驗證器對象,太麻煩了,我還是喜歡用
ModelState
下面就滿足你的要求。
與ASP.NET Core內建
首先,通過
AddFluentValidation
擴充方法注冊相關服務,并注冊驗證器
CreateUserDtoValidator
注冊驗證器的方式有兩種:
- 一種是手動注冊,如
services.AddTransient<IValidator<CreateUserDto>, CreateUserDtoValidator>();
- 另一種是通過指定程式集,程式集内的所有(public、非抽象、繼承自
)驗證器将會被自動注冊AbstractValidator<T>
我們使用第二種方式:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews()
.AddFluentValidation(fv =>
fv.RegisterValidatorsFromAssemblyContaining<CreateUserDtoValidator>());
}
注意:必須在
AddFluentValidation
之後注冊,因為其需要使用Mvc的服務。
AddMvc
RegisterValidatorsFromAssemblyContaining<T>
方法,可以自動查找指定類型所屬的程式集。
該方法可以指定一個
filter
,可以對要注冊的驗證器進行篩選。
需要注意的是,這些驗證器預設注冊的生命周期是
Scoped
,你也可以修改成其他的:
fv.RegisterValidatorsFromAssemblyContaining<CreateUserDtoValidator>(lifetime: ServiceLifetime.Transient)
不過,不建議将其注冊為
Singleton
,因為開發時很容易就在不經意間,在單例的驗證器中依賴了
Transient
Scoped
的服務,這會導緻生命周期提升。
另外,如果你想将
internal
的驗證器也自動注冊到DI容器中,可以通過指定參數
includeInternalTypes
來實作:
fv.RegisterValidatorsFromAssemblyContaining<CreateUserDtoValidator>(includeInternalTypes: true)
好了,現在将
Post
方法改回我們熟悉的樣子:
[HttpPost]
public string Post([FromBody] CreateUserDto input)
{
if (!ModelState.IsValid)
{
return "模型狀态無效:"
+ string.Join(Environment.NewLine,
ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage)));
}
return JsonSerializer.Serialize(input);
}
再次傳入一個空的json對象時,就可以得到錯誤響應啦!
驗證擴充
現在,在ASP.NET Core中使用FluentValidation已經初見成效了。不過,我們還有一些細節問題需要解決,如複雜屬性驗證、集合驗證、組合驗證等。
複雜屬性驗證
首先,改造一下
CreateUserDto
public class CreateUserDto
{
public CreateUserNameDto Name { get; set; }
public int Age { get; set; }
}
public class CreateUserNameDto
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class CreateUserNameDtoValidator : AbstractValidator<CreateUserNameDto>
{
public CreateUserNameDtoValidator()
{
RuleFor(x => x.FirstName).NotEmpty();
RuleFor(x => x.LastName).NotEmpty();
}
}
現在,我們的
Name
重新封裝為了一個類
CreateUserNameDto
,該類包含了
FirstName
LastName
兩個屬性,并為其建立了一個驗證器。很顯然,我們希望在驗證
CreateUserDtoValidator
中,可以使用
CreateUserNameDtoValidator
來驗證
Name
。這可以通過
SetValidator
public class CreateUserDtoValidator : AbstractValidator<CreateUserDto>
{
public CreateUserDtoValidator()
{
RuleFor(x => x.Name).SetValidator(new CreateUserNameDtoValidator());
RuleFor(x => x.Age).GreaterThan(0);
}
}
需要說明的是,如果
Name is null
(如果是集合,則若為
null
或空集合),那麼不會執行
CreateUserNameDtoValidator
。如果要驗證
Name is not null
,請使用
NotNull()
NotEmpty()
集合驗證
CreateUserDto
public class CreateUserDto
{
public int Age { get; set; }
public List<string> Hobbies { get; set; }
public List<CreateUserNameDto> Names { get; set; }
}
可以看到,新增了兩個集合:簡單集合
Hobbies
和複雜集合
Names
。如果僅使用
RuleFor
設定驗證規則,那麼其驗證的是集合整體,而不是集合中的每個項。
為了驗證集合中的每個項,需要使用
RuleForEach
或在
RuleFor
後跟
ForEach
public class CreateUserDtoValidator : AbstractValidator<CreateUserDto>
{
public CreateUserDtoValidator()
{
RuleFor(x => x.Age).GreaterThan(0);
// Hobbies 集合不能為空
RuleFor(x => x.Hobbies).NotEmpty();
// Hobbies 集合中的每一項不能為空
RuleForEach(x => x.Hobbies).NotEmpty();
RuleFor(x => x.Names).NotEmpty();
RuleForEach(x => x.Names).NotEmpty().SetValidator(new CreateUserNameDtoValidator());
}
}
驗證規則組合
有時,一個類的驗證規則,可能會有很多很多,這時,如果都放在一個驗證器中,就會顯得代碼又多又亂。那該怎麼辦呢?
我們可以為這個類建立多個驗證器,将所有驗證規則配置設定到這些驗證器中,最後再通過
Include
合并到一個驗證器中。
public class CreateUserDtoNameValidator : AbstractValidator<CreateUserDto>
{
public CreateUserDtoNameValidator()
{
RuleFor(x => x.Name).NotEmpty();
}
}
public class CreateUserDtoAgeValidator : AbstractValidator<CreateUserDto>
{
public CreateUserDtoAgeValidator()
{
RuleFor(x => x.Age).GreaterThan(0);
}
}
public class CreateUserDtoValidator : AbstractValidator<CreateUserDto>
{
public CreateUserDtoValidator()
{
Include(new CreateUserDtoNameValidator());
Include(new CreateUserDtoAgeValidator());
}
}
繼承驗證
雖然模型綁定不支援反序列化接口類型,但是它在其他場景中還是有用途的。
CreateUserDto
public class CreateUserDto
{
public int Age { get; set; }
public IPet Pet { get; set; }
}
public interface IPet
{
string Name { get; set; }
}
public class DogPet : IPet
{
public string Name { get; set; }
public int Age { get; set; }
}
public class CatPet : IPet
{
public string Name { get; set; }
}
public class DogPetValidator : AbstractValidator<DogPet>
{
public DogPetValidator()
{
RuleFor(x => x.Name).NotEmpty();
RuleFor(x => x.Age).GreaterThan(0);
}
}
public class CatPetValidator : AbstractValidator<CatPet>
{
public CatPetValidator()
{
RuleFor(x => x.Name).NotEmpty();
}
}
這次,我們新增了一個屬性,它是接口類型,也就是說它的實作類是不固定的。這種情況下,我們該如何為其指定驗證器呢?
這時候就輪到
SetInheritanceValidator
上場了,通過它指定多個實作類的驗證器,當進行模型驗證時,可以自動根據模型類型,選擇對應的驗證器:
public class CreateUserDtoValidator : AbstractValidator<CreateUserDto>
{
public CreateUserDtoValidator()
{
RuleFor(x => x.Age).GreaterThan(0);
RuleFor(x => x.Pet).NotEmpty().SetInheritanceValidator(v =>
{
v.Add(new DogPetValidator());
v.Add(new CatPetValidator());
});
}
}
自定義驗證
官方提供的驗證器已經可以覆寫大多數的場景,但是總有一些場景是和我們的業務息息相關的,是以,自定義驗證就不可或缺了,官方為我們提供了
Must
Custom
Must
Must
使用起來最簡單,看例子:
public class CreateUserDto
{
public List<string> Hobbies { get; set; }
}
public class CreateUserDtoValidator : AbstractValidator<CreateUserDto>
{
public CreateUserDtoValidator()
{
RuleFor(x => x.Hobbies).NotEmpty()
.Must((x, hobbies, context) =>
{
var duplicateHobby = hobbies.GroupBy(h => h).FirstOrDefault(g => g.Count() > 1)?.Key;
if(duplicateHobby is not null)
{
// 添加自定義占位符
context.MessageFormatter.AppendArgument("DuplicateHobby", duplicateHobby);
return false;
}
return true;
}).WithMessage("愛好不能重複,重複項:{DuplicateHobby}");
}
}
在該示例中,我們使用自定義驗證來驗證
Hobbies
清單中是否存在重複項,并将重複項寫入錯誤消息。
Must
的重載中,可以最多接收三個入參,分别是驗證屬性所在的對象執行個體、驗證屬性和驗證上下文。另外,還通過驗證上下文的
MessageFormatter
添加了自定義的占位符。
Custom
如果
Must
無法滿足需求,可以考慮使用
Custom
。相比
Must
,它可以手動建立
ValidationFailure
執行個體,并且可以針對同一個驗證規則建立多個錯誤消息。
public class CreateUserDtoValidator : AbstractValidator<CreateUserDto>
{
public CreateUserDtoValidator()
{
RuleFor(x => x.Hobbies).NotEmpty()
.Custom((hobbies, context) =>
{
var duplicateHobby = hobbies.GroupBy(h => h).FirstOrDefault(g => g.Count() > 1)?.Key;
if (duplicateHobby is not null)
{
// 當驗證失敗時,會同時輸出這兩條消息
context.AddFailure($"愛好不能重複,重複項:{duplicateHobby}");
context.AddFailure($"再說一次,愛好不能重複");
}
});
}
}
當存在重複項時,會同時輸出兩條錯誤消息(即使設定了
CascadeMode.Stop
,這就是所期望的)。
驗證配置
現在,模型驗證方式你已經全部掌握了。現在的你,是否想要驗證消息重寫、屬性重命名、條件驗證等功能呢?
驗證消息重寫和屬性重命名
預設的驗證消息可以滿足一部分需求,但是無法滿足所有需求,是以,重寫驗證消息,是不可或缺的一項功能,這可以通過
WithMessage
來實作。
public class CreateUserDtoValidator : AbstractValidator<CreateUserDto>
{
public CreateUserDtoValidator()
{
RuleFor(x => x.Name)
.NotNull().WithMessage("{PropertyName} 不能為 null")
.WithName("姓名");
RuleFor(x => x.Age)
.GreaterThan(0).WithMessage(x => $"姓名為“{x.Name}”的年齡“{x.Age}”不正确");
}
}
在
WithMessage
内,除了自定義驗證消息外,還有一個占位符
{PropertyName}
,它可以将屬性名
Name
填充進去。如果你想展示
姓名
而不是
Name
,可以通過
WithName
來更改屬性的展示名稱。
僅用于重寫屬性用于展示的名稱,如果想要将屬性本身重命名,可以使用
WithName
OverridePropertyName
這就很容易了解了,當驗證發現
Name
為
null
時,就會提示消息“姓名 不能為 null”。
另外,
WithMessage
還可以接收Lambda表達式,允許你自由的使用模型的其他屬性。
條件驗證
有時,隻有當滿足特定條件時,才驗證某個屬性,這可以通過
When
public class CreateUserDto
{
public string Name { get; set; }
public int Age { get; set; }
public bool? HasGirlfriend { get; set; }
public bool HardWorking { get; set; }
public bool Healthy { get; set; }
}
public class CreateUserDtoValidator : AbstractValidator<CreateUserDto>
{
public CreateUserDtoValidator()
{
RuleFor(x => x.HasGirlfriend)
.NotNull()
.Equal(false).When(x => x.Age < 18, ApplyConditionTo.CurrentValidator)
.Equal(true).When(x => x.Age >= 18, ApplyConditionTo.CurrentValidator);
When(x => x.HasGirlfriend == true, () =>
{
RuleFor(x => x.HardWorking).Equal(true);
RuleFor(x => x.Healthy).Equal(true);
}).Otherwise(() =>
{
RuleFor(x => x.Healthy).Equal(true);
});
}
}
When
有兩種使用方式:
1.第一種是在規則後緊跟
When
設定條件,那麼隻有當滿足該條件時,才會執行前面的驗證規則。
需要注意的是,預設情況下,
When
會作用于它之前的所有規則上。例如,對于條件
x.Age >= 18
,他預設會作用于
NotNull
Equal(false)
Equal(true)
上面,隻有當
Age >= 18
時,才會執行這些規則,然而,
NotNull
Equal(false)
又受限于條件
x.Age < 18
如果我們想要讓
When
僅僅作用于緊跟它之前的那一條驗證規則上,可以通過指定
ApplyConditionTo.CurrentValidator
來達到目的。例如示例中的
x.Age < 18
僅會作用于
Equal(false)
,而
x.Age >= 18
Equal(true)
可見,第一種比較适合用于對某一條驗證規則設定條件。
2.第二種則是直接使用
When
來指定達到某個條件時要執行的驗證規則。相比第一種,它的好處是更加适合針對多條驗證規則添加同一條件,還可以結合
Otherwise
來添加反向條件達成時的驗證規則。
其他驗證配置
一起來看以下其他常用的配置項。
請注意,以下部配置設定置項,可以在每個驗證器内進行配置覆寫。
public class FluentValidationMvcConfiguration
{
public bool ImplicitlyValidateChildProperties { get; set; }
public bool LocalizationEnabled { get; set; }
public bool AutomaticValidationEnabled { get; set; }
public bool DisableDataAnnotationsValidation { get; set; }
public IValidatorFactory ValidatorFactory { get; set; }
public Type ValidatorFactoryType { get; set; }
public bool ImplicitlyValidateRootCollectionElements { get; set; }
public ValidatorConfiguration ValidatorOptions { get; }
}
public class ValidatorConfiguration
{
public CascadeMode CascadeMode { get; set; }
public Severity Severity { get; set; }
public string PropertyChainSeparator { get; set; }
public ILanguageManager LanguageManager { get; set; }
public ValidatorSelectorOptions ValidatorSelectors { get; }
public Func<MessageFormatter> MessageFormatterFactory { get; set; }
public Func<Type, MemberInfo, LambdaExpression, string> PropertyNameResolver { get; set; }
public Func<Type, MemberInfo, LambdaExpression, string> DisplayNameResolver { get; set; }
public bool DisableAccessorCache { get; set; }
public Func<IPropertyValidator, string> ErrorCodeResolver { get; set; }
}
ImplicitlyValidateChildProperties
預設 false。當設定為 true 時,你就可以不用通過
SetValidator
為複雜屬性設定驗證器了,它會自動尋找。注意,當其設定為 true 時,如果你又使用了
SetValidator
,會導緻驗證兩次。
不過,當設定為 true 時,可能會行為不一緻,比如當設定
ValidatorOptions.CascadeMode
Stop
時(下面會介紹),若多個驗證器中有驗證失敗的規則,那麼這些驗證器都會傳回1條驗證失敗消息。這并不是Bug,可以參考此Issue了解原因。
LocalizationEnabled
預設 true。當設定為 true 時,會啟用本地化支援,提示的錯誤消息文本與目前文化(
CultureInfo.CurrentUICulture
) 有關。
AutomaticValidationEnabled
預設 true。當設定為 true 時,ASP.NET在模型綁定時會嘗試使用FluentValidation進行模型驗證。如果設定為 false,則不會自動使用FluentValidation進行模型驗證。
寫這篇文章時,用的 FluentValidation 版本是10.3.5,當時有一個bug,可能你在用的過程中也會很疑惑,我已經提了Issue。現在作者已經修複了,将在新版本中釋出。
DisableDataAnnotationsValidation
預設 false。預設情況下,FluentValidation 執行完時,還會執行 DataAnnotations。通過将其設定為 true,來禁用 DataAnnotations。
注意:僅當
AutomaticValidationEnabled
true
時,才會生效。
ImplicitlyValidateRootCollectionElements
當接口入參為集合類型時,如:
public string Post([FromBody] List<CreateUserDto> input)
若要驗證該集合,則需要實作繼承自
AbstractValidator<List<CreateUserDto>>
的驗證器,或者指定
ImplicitlyValidateChildProperties = true
如果,你想僅僅驗證
CreateUserDto
的屬性,而不驗證其子屬性
CreateUserNameDto
的屬性,則必須設定
ImplicitlyValidateChildProperties = false
,并設定
ImplicitlyValidateRootCollectionElements = true
(當
ImplicitlyValidateChildProperties = true
時,會忽略該配置)。
ValidatorOptions.CascadeMode
指定驗證失敗時的級聯模式,共兩種(外加一個已過時的):
-
:預設的。即使驗證失敗了,也會執行全部驗證規則。Continue
-
:當一個驗證器中出現驗證失敗時,立即停止目前驗證器的繼續執行。如果在目前驗證器中通過Stop
為複雜屬性設定另一個驗證器,那麼會将其視為一個驗證器。不過,如果設定SetValidator
,那麼這将會被視為不同的驗證器。ImplicitlyValidateChildProperties = true
-
:官方建議,如果可以使用[Obsolete]StopOnFirstFailure
,就不要使用該模式。注意該模式和Stop
模式行為并非完全一緻,具體要不要用,自己決定。點選此處檢視他倆的差別。Stop
ValidatorOptions.Severity
設定驗證錯誤的嚴重級别,可以配置的項有
Error
(預設)、
Warning
Info
即使你講嚴重級别設定為了
Warning
或者
Info
ValidationResult.IsValid
仍是
false
。不同的是,
ValidationResult.Errors
中的嚴重級别是
Warning
Info
ValidatorOptions.LanguageManager
可以忽略目前文化,強制設定指定文化,如強制設定為美國:
ValidatorOptions.LanguageManager.Culture = new CultureInfo("en-US");
ValidatorOptions.DisplayNameResolver
驗證屬性展示名稱的解析器。通過該配置,可以自定義驗證屬性展示名稱,如加字首“xiaoxiaotank_”:
ValidatorOptions.DisplayNameResolver = (type, member, expression) =>
{
if (member is not null)
{
return "xiaoxiaotank_" + member.Name;
}
return null;
};
錯誤消息類似如下:
'xiaoxiaotank_FirstName' 不能為Null。
占位符
上面我們已經接觸了
{PropertyName}
占位符,除了它之外,還有很多。下面就介紹一些:
-
:正在驗證的屬性的名稱{PropertyName}
-
:正在驗證的屬性的值{PropertyValue}
-
:比較驗證器中要比較的值{ComparisonValue}
-
:字元串最小長度{MinLength}
-
:字元串最大長度{MaxLength}
-
:字元串長度{TotalLength}
-
:正規表達式驗證器的正規表達式{RegularExpression}
-
:範圍驗證器的範圍下限{From}
-
:範圍驗證器的範圍上限{To}
-
:decimal精度驗證器的數字總位數{ExpectedPrecision}
-
:decimal精度驗證器的小數位數{ExpectedScale}
-
:decimal精度驗證器正在驗證的數字實際整數位數{Digits}
-
:decimal精度驗證器正在驗證的數字實際小數位數{ActualScale}
這些占位符,隻能運用在特定的驗證器中。更多占位符的詳細介紹,請檢視官方文檔Built-in Validators
Web Api中的模型驗證
對于Web Api應用,由于标記了
[ApiController]
ModelState.IsValid
進行檢查,若發現模型狀态無效,會傳回包含錯誤資訊的指定格式的HTTP 400響應。
該格式預設類型為
ValidationProblemDetails
,在
Action
中可以通過調用
ValidationProblem
方法傳回該類型。類似如下:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-16fd10e48fa5d545ae2e5f3fee05dc84-d23c49c9a5e35d49-00",
"errors": {
"Hobbies[0].LastName": [
"'xiaoxiaotank_LastName' 不能為Null。",
"'xiaoxiaotank_LastName' 不能為空。"
],
"Hobbies[0].FirstName": [
"'xiaoxiaotank_FirstName' 不能為Null。",
"'xiaoxiaotank_FirstName' 不能為空。"
]
}
}
其實作的根本原理是使用了ModelStateInvalidFilter過濾器,該過濾器會附加在所有被标注了
ApiControllerAttribute
的類型上。
public class ModelStateInvalidFilter : IActionFilter, IOrderedFilter
{
internal const int FilterOrder = -2000;
private readonly ApiBehaviorOptions _apiBehaviorOptions;
private readonly ILogger _logger;
public ModelStateInvalidFilter(ApiBehaviorOptions apiBehaviorOptions, ILogger logger)
{
// ...
}
// 預設 -2000
public int Order => FilterOrder;
public bool IsReusable => true;
public void OnActionExecuted(ActionExecutedContext context) { }
public void OnActionExecuting(ActionExecutingContext context)
{
if (context.Result == null && !context.ModelState.IsValid)
{
_logger.ModelStateInvalidFilterExecuting();
context.Result = _apiBehaviorOptions.InvalidModelStateResponseFactory(context);
}
}
}
internal class ApiBehaviorOptionsSetup : IConfigureOptions<ApiBehaviorOptions>
{
private ProblemDetailsFactory _problemDetailsFactory;
public void Configure(ApiBehaviorOptions options)
{
// 看這裡
options.InvalidModelStateResponseFactory = context =>
{
// ProblemDetailsFactory 中依賴 ApiBehaviorOptionsSetup,是以這裡未使用構造函數注入,以避免DI循環
_problemDetailsFactory ??= context.HttpContext.RequestServices.GetRequiredService<ProblemDetailsFactory>();
return ProblemDetailsInvalidModelStateResponse(_problemDetailsFactory, context);
};
ConfigureClientErrorMapping(options);
}
internal static IActionResult ProblemDetailsInvalidModelStateResponse(ProblemDetailsFactory problemDetailsFactory, ActionContext context)
{
var problemDetails = problemDetailsFactory.CreateValidationProblemDetails(context.HttpContext, context.ModelState);
ObjectResult result;
if (problemDetails.Status == 400)
{
// 相容 2.x
result = new BadRequestObjectResult(problemDetails);
}
else
{
result = new ObjectResult(problemDetails)
{
StatusCode = problemDetails.Status,
};
}
result.ContentTypes.Add("application/problem+json");
result.ContentTypes.Add("application/problem+xml");
return result;
}
internal static void ConfigureClientErrorMapping(ApiBehaviorOptions options)
{
options.ClientErrorMapping[400] = new ClientErrorData
{
Link = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
Title = Resources.ApiConventions_Title_400,
};
// ...還有很多,省略了
}
}
全局模型驗證
Web Api中有全局的自動模型驗證,那Web中你是否也想整一個呢(你該不會想總在方法内寫
ModelState.IsValid
吧)?以下給出一個簡單的示例:
public class ModelStateValidationFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
if (context.HttpContext.Request.AcceptJson())
{
var errorMsg = string.Join(Environment.NewLine, context.ModelState.Values.SelectMany(v => v.Errors.Select(e => e.ErrorMessage)));
context.Result = new BadRequestObjectResult(AjaxResponse.Failed(errorMsg));
}
else
{
context.Result = new ViewResult();
}
}
}
}
public static class HttpRequestExtensions
{
public static bool AcceptJson(this HttpRequest request)
{
if (request == null) throw new ArgumentNullException(nameof(request));
var regex = new Regex(@"^(\*|application)/(\*|json)$");
return request.Headers[HeaderNames.Accept].ToString()
.Split(',')
.Any(type => regex.IsMatch(type));
}
}
AjaxResponse.Failed(errorMsg)
隻是自定義的json資料結構,你可以按照自己的方式來。