CodeSnippet.Cn
代码片段
Csharp
架构设计
.NetCore
西班牙语
kubernetes
MySql
Redis
Algorithm
Ubuntu
Linux
Other
.NetMvc
VisualStudio
Git
pm
Python
WPF
java
Plug-In
分布式
CSS
微服务架构
JavaScript
DataStructure
Shared
理解ASP.NET Core - 模型绑定&验证(Model Binding and Validation)
0
.NetCore
小笨蛋
发布于:2022年12月25日
更新于:2022年12月25日
195
#custom-toc-container
### 模型绑定 什么是模型绑定?简单说就是将HTTP请求参数绑定到程序方法入参上,该变量可以是简单类型,也可以是复杂类。 #### 绑定源 所谓绑定源,是指用于模型绑定的值来源。 先举个例子: ```csharp [Route("api/[controller]")] public class UserController : ControllerBase { [Route("{id}")] public string Get([FromRoute] string id) { return id; } } ``` 就拿上面的例子来说,Get方法的参数id,被[FromRoute]标注,表示其绑定源是路由。当然,绑定源不仅仅只有这一种: - [FromQuery]:从Url的查询字符串中获取值。查询字符串就是Url中问号(?)后面拼接的参数 - [FromRoute]:从路由数据中获取值。例如上例中的{id} - [FromForm]:从表单中获取值。 - [FromBody]:从请求正文中获取值。 - [FromHeader]:从请求标头中获取值。 - [FromServices]:从DI容器中获取服务。相比其他源,它特殊在值不是来源于HTTP请求,而是DI容器。 > 建议大家在编写接口时,尽量显式指明绑定源。 在绑定的时候,可能会遇到以下两种情况: ##### 情况一:模型属性在绑定源中不存在 什么是模型属性在绑定源中不存在?给大家举个例子: ```csharp [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
(),不过byte[]数组设置为null。如例子中的Post2,你会得到一个空数组。 ##### 情况二:绑定源无法转换为模型中的目标类型 比如,当尝试将绑定源中的字符串abc转换为模型中的值类型int时,会发生类型转换错误,此时,会将该模型状态标记为无效。 #### 绑定格式 int、string、模型类等绑定格式大家已经很熟悉了,我就不再赘述了。这次,只给大家介绍一些比较特殊的绑定格式。 ##### 集合 假设存在以下接口,接口参数是一个数组: ```csharp 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的长度是有限制的,若传递的集合过长,超过了长度限制,就会有截断的风险。所以,建议将该集合放到一个模型类里面,该模型类作为接口参数。 ##### 字典 假设存在以下接口,接口参数是一个字典: ```csharp public Dictionary
Post([FromQuery] Dictionary
idNames) ``` 参数为:{ [1] = "j", [2] = "k" } 为了将参数绑定到字典idNames上,你可以通过表单或查询字符串传入,可以采用以下格式之一: - idNames[1]=j&idNames[2]=k,注意:方括号中的数字是字典的key - [1]=j&[2]=k - idNames[0].key=1&idNames[0].value=j&idNames[1].key=2&idNames[1].value=k,注意:方括号中的数字是索引,不是字典的key - [0].key=1&[0].value=j&[1].key=2&[1].value=k 同样,请注意Url长度限制问题。 ### 模型验证 聊完了模型绑定,那接下来就是要验证绑定的模型是否有效。 假设UserController中存在一个Post方法: ```csharp 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,传入以下参数: ```csharp { "age":"abc" } ``` 会得到如下响应: ```csharp 模型状态无效: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的核心类结构: ```csharp public class ModelStateDictionary : IReadOnlyDictionary
{ 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
IReadOnlyDictionary
.Keys => Keys; public ValueEnumerable Values { get; } IEnumerable
IReadOnlyDictionary
.Values => Values; // 枚举,模型验证状态,有 Unvalidated、Invalid、Valid、Skipped 共4种 public ModelValidationState ValidationState { get; } // 指示模型状态是否有效,当验证状态为 Valid 和 Skipped 有效 public bool IsValid { get; } public ModelStateEntry this[string key] { get; } } ``` - MaxAllowedErrors:允许的模型状态错误数量,默认是 200。 - 当错误数量达到MaxAllowedErrors - 1 时,若还要添加错误,则该错误不会被添加,而是添加一个 TooManyModelErrorsException错误 - 可以通过AddModelError或TryAddModelError方法添加错误 - 另外,若是直接修改ModelStateEntry,那错误数量不会受该属性限制 - ValidationState:模型验证状态 - Unvalidated:未验证。当模型尚未进行验证或任意一个ModelStateEntry验证状态为Unvalidated时,该值为未验证。 - Invalid:无效。当模型已验证完毕(即没有ModelStateEntry验证状态为Unvalidated)并且任意一个ModelStateEntry验证状态为Invalid,该值为无效。 - Valid:有效。当模型已验证完毕,且所有ModelStateEntry验证状态仅包含Valid和Skipped时,该值为有效。 - Skipped:跳过。整个模型跳过验证时,该值为跳过。 #### 重新验证 默认情况下,模型验证是自动进行的。不过有时,需要为模型进行一番自定义操作后,重新进行模型验证。可以先通过ModelStateDictionary.ClearValidationState方法清除验证状态,然后调用ControllerBase.TryValidateModel方法重新验证: ```csharp 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](https://github.com/FluentValidation/FluentValidation "github") 查看[官网](https://fluentvalidation.net/ "官网") 接下来,跟我一起感受FluentValidation的魅力吧! 为了更好的展示,我们先丰富一下CreateUserDto: ```csharp public class CreateUserDto { public string Name { get; set; } public int Age { get; set; } } ``` ##### 安装 今天,我们要安装两个包,分别是FluentValidation和FluentValidation.AspNetCore(后者依赖前者): - FluentValidation:是整个验证库的核心 - FluentValidation.AspNetCore:用于与ASP.NET Core集成 选择你喜欢的安装方式: - 方式1:通过NuGet安装: ```csharp Install-Package FluentValidation Install-Package FluentValidation.AspNetCore ``` - 方式2:通过CLI安装 ```csharp dotnet add package FluentValidation dotnet add package FluentValidation.AspNetCore ``` ##### 创建 CreateUserDto 的验证器 为了配置CreateUserDto各个属性的验证规则,我们需要为它创建一个验证器(validator),该验证器继承自抽象类AbstractValidator
,T就是你要验证的类型,这里就是CreateUserDto。 ```csharp public class CreateUserDtoValidator : AbstractValidator
{ public CreateUserDtoValidator() { RuleFor(x => x.Name).NotEmpty(); RuleFor(x => x.Age).GreaterThan(0); } } ``` 验证器很简单,只有一个构造函数,所有的验证规则,都将写入到该构造函数中。 通过RuleFor并传入Lambda表达式为指定属性设定验证规则,然后,就可以以Fluent的方式添加验证规则。这里我添加了两个验证规则:Name 不能为空、Age 必须大于 0 现在,改写一下Post方法: ```csharp [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对象时,会得到以下响应: ```csharp 模型状态无效:Name' 不能为空。 'Age' 必须大于 '0'。 ``` 虽然我们已经基本实现了验证功能,但是不免有人会吐槽:验证代码也太多了吧,而且还要手动 new 一个指定类型的验证器对象,太麻烦了,我还是喜欢用ModelState。 下面就满足你的要求。 ##### 与ASP.NET Core集成 首先,通过AddFluentValidation扩展方法注册相关服务,并注册验证器CreateUserDtoValidator。 注册验证器的方式有两种: - 一种是手动注册,如services.AddTransient
, CreateUserDtoValidator>(); - 另一种是通过指定程序集,程序集内的所有(public、非抽象、继承自AbstractValidator
)验证器将会被自动注册 我们使用第二种方式: ```csharp public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews() .AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining
()); } ``` > 注意:AddFluentValidation必须在AddMvc之后注册,因为其需要使用Mvc的服务。 通过RegisterValidatorsFromAssemblyContaining
方法,可以自动查找指定类型所属的程序集。 该方法可以指定一个filter,可以对要注册的验证器进行筛选。 需要注意的是,这些验证器默认注册的生命周期是Scoped,你也可以修改成其他的: ```csharp fv.RegisterValidatorsFromAssemblyContaining
(lifetime: ServiceLifetime.Transient) ``` 不过,不建议将其注册为Singleton,因为开发时很容易就在不经意间,在单例的验证器中依赖了Transient或Scoped的服务,这会导致生命周期提升。 另外,如果你想将internal的验证器也自动注册到DI容器中,可以通过指定参数includeInternalTypes来实现: ```csharp fv.RegisterValidatorsFromAssemblyContaining
(includeInternalTypes: true) ``` 好了,现在将Post方法改回我们熟悉的样子: ```csharp [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: ```csharp 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
{ public CreateUserNameDtoValidator() { RuleFor(x => x.FirstName).NotEmpty(); RuleFor(x => x.LastName).NotEmpty(); } } ``` 现在,我们的Name重新封装为了一个类CreateUserNameDto,该类包含了FirstName和LastName两个属性,并为其创建了一个验证器。很显然,我们希望在验证CreateUserDtoValidator中,可以使用CreateUserNameDtoValidator来验证Name。这可以通过SetValidator来实现: ```csharp public class CreateUserDtoValidator : AbstractValidator
{ 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: ```csharp public class CreateUserDto { public int Age { get; set; } public List
Hobbies { get; set; } public List
Names { get; set; } } ``` 可以看到,新增了两个集合:简单集合Hobbies和复杂集合Names。如果仅使用RuleFor设定验证规则,那么其验证的是集合整体,而不是集合中的每个项。 为了验证集合中的每个项,需要使用RuleForEach或在RuleFor后跟ForEach来实现: ```csharp public class CreateUserDtoValidator : AbstractValidator
{ 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合并到一个验证器中。 ```csharp public class CreateUserDtoNameValidator : AbstractValidator
{ public CreateUserDtoNameValidator() { RuleFor(x => x.Name).NotEmpty(); } } public class CreateUserDtoAgeValidator : AbstractValidator
{ public CreateUserDtoAgeValidator() { RuleFor(x => x.Age).GreaterThan(0); } } public class CreateUserDtoValidator : AbstractValidator
{ public CreateUserDtoValidator() { Include(new CreateUserDtoNameValidator()); Include(new CreateUserDtoAgeValidator()); } } ``` ###### 继承验证 虽然模型绑定不支持反序列化接口类型,但是它在其他场景中还是有用途的。 首先,改造一下CreateUserDto: ```csharp 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
{ public DogPetValidator() { RuleFor(x => x.Name).NotEmpty(); RuleFor(x => x.Age).GreaterThan(0); } } public class CatPetValidator : AbstractValidator
{ public CatPetValidator() { RuleFor(x => x.Name).NotEmpty(); } } ``` 这次,我们新增了一个属性,它是接口类型,也就是说它的实现类是不固定的。这种情况下,我们该如何为其指定验证器呢? 这时候就轮到SetInheritanceValidator上场了,通过它指定多个实现类的验证器,当进行模型验证时,可以自动根据模型类型,选择对应的验证器: ```csharp public class CreateUserDtoValidator : AbstractValidator
{ 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使用起来最简单,看例子: ```csharp public class CreateUserDto { public List
Hobbies { get; set; } } public class CreateUserDtoValidator : AbstractValidator
{ 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实例,并且可以针对同一个验证规则创建多个错误消息。 ```csharp public class CreateUserDtoValidator : AbstractValidator
{ 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来实现。 ```csharp public class CreateUserDtoValidator : AbstractValidator
{ 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来实现: ```csharp 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
{ 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来添加反向条件达成时的验证规则。 ###### 其他验证配置 一起来看以下其他常用的配置项。 请注意,以下部分配置项,可以在每个验证器内进行配置覆盖。 ```csharp 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
MessageFormatterFactory { get; set; } public Func
PropertyNameResolver { get; set; } public Func
DisplayNameResolver { get; set; } public bool DisableAccessorCache { get; set; } public Func
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](https://docs.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations?view=net-6.0 "DataAnnotations")。通过将其设置为 true,来禁用 DataAnnotations。 注意:仅当AutomaticValidationEnabled为true时,才会生效。 ###### ImplicitlyValidateRootCollectionElements 当接口入参为集合类型时,如: ```csharp public string Post([FromBody] List
input) ``` 若要验证该集合,则需要实现继承自AbstractValidator
>的验证器,或者指定ImplicitlyValidateChildProperties = true。 如果,你想仅仅验证CreateUserDto的属性,而不验证其子属性CreateUserNameDto的属性,则必须设置ImplicitlyValidateChildProperties = false,并设置ImplicitlyValidateRootCollectionElements = true(当ImplicitlyValidateChildProperties = true时,会忽略该配置)。 ###### ValidatorOptions.CascadeMode 指定验证失败时的级联模式,共两种(外加一个已过时的): - Continue:默认的。即使验证失败了,也会执行全部验证规则。 - Stop:当一个验证器中出现验证失败时,立即停止当前验证器的继续执行。如果在当前验证器中通过SetValidator为复杂属性设置另一个验证器,那么会将其视为一个验证器。不过,如果设置ImplicitlyValidateChildProperties = true,那么这将会被视为不同的验证器。 - [Obsolete]StopOnFirstFailure:官方建议,如果可以使用Stop,就不要使用该模式。注意该模式和Stop模式行为并非完全一致,具体要不要用,自己决定。点击[此处](https://docs.fluentvalidation.net/en/latest/conditions.html#stop-vs-stoponfirstfailure "此处")查看他俩的区别。 ###### ValidatorOptions.Severity 设置验证错误的严重级别,可以配置的项有Error(默认)、Warning、Info。 即使你讲严重级别设置为了Warning或者Info,ValidationResult.IsValid仍是false。不同的是,ValidationResult.Errors中的严重级别是Warning或者Info。 ###### ValidatorOptions.LanguageManager 可以忽略当前文化,强制设置指定文化,如强制设置为美国: ```csharp ValidatorOptions.LanguageManager.Culture = new CultureInfo("en-US"); ``` ###### ValidatorOptions.DisplayNameResolver 验证属性展示名称的解析器。通过该配置,可以自定义验证属性展示名称,如加前缀“xiaoxiaotank_”: ```csharp ValidatorOptions.DisplayNameResolver = (type, member, expression) => { if (member is not null) { return "xiaoxiaotank_" + member.Name; } return null; }; ``` 错误消息类似如下: ```csharp 'xiaoxiaotank_FirstName' 不能为Null。 ``` ##### 占位符 上面我们已经接触了{PropertyName}占位符,除了它之外,还有很多。下面就介绍一些: - {PropertyName}:正在验证的属性的名称 - {PropertyValue}:正在验证的属性的值 - {ComparisonValue}:比较验证器中要比较的值 - {MinLength}:字符串最小长度 - {MaxLength}:字符串最大长度 - {TotalLength}:字符串长度 - {RegularExpression}:正则表达式验证器的正则表达式 - {From}:范围验证器的范围下限 - {To}:范围验证器的范围上限 - {ExpectedPrecision}:decimal精度验证器的数字总位数 - {ExpectedScale}:decimal精度验证器的小数位数 - {Digits}:decimal精度验证器正在验证的数字实际整数位数 - {ActualScale}:decimal精度验证器正在验证的数字实际小数位数 > 这些占位符,只能运用在特定的验证器中。更多占位符的详细介绍,请查看官方文档[Built-in Validators](https://docs.fluentvalidation.net/en/latest/built-in-validators.html# "Built-in Validators") ### Web Api中的模型验证 对于Web Api应用,由于标记了[ApiController]特性,其会自动执行ModelState.IsValid进行检查,若发现模型状态无效,会返回包含错误信息的指定格式的HTTP 400响应。 该格式默认类型为ValidationProblemDetails,在Action中可以通过调用ValidationProblem方法返回该类型。类似如下: ```csharp { "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](https://docs.microsoft.com/zh-cn/dotnet/api/microsoft.aspnetcore.mvc.infrastructure.modelstateinvalidfilter?view=aspnetcore-6.0 "ModelStateInvalidFilter")过滤器,该过滤器会附加在所有被标注了ApiControllerAttribute的类型上。 ```csharp 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
{ private ProblemDetailsFactory _problemDetailsFactory; public void Configure(ApiBehaviorOptions options) { // 看这里 options.InvalidModelStateResponseFactory = context => { // ProblemDetailsFactory 中依赖 ApiBehaviorOptionsSetup,所以这里未使用构造函数注入,以避免DI循环 _problemDetailsFactory ??= context.HttpContext.RequestServices.GetRequiredService
(); 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吧)?以下给出一个简单的示例: ```csharp 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数据结构,你可以按照自己的方式来。
这里⇓感觉得写点什么,要不显得有点空,但还没想好写什么...
返回顶部
About
京ICP备13038605号
© 代码片段 2024