您的位置:首页 > 编程语言 > ASP

理解ASP.NET Core - 模型绑定&验证(Model Binding and Validation)

2021-12-08 09:49 1446 查看

注:本文隶属于《理解ASP.NET Core》系列文章,请查看置顶博客或点击此处查看全文目录

模型绑定

什么是模型绑定?简单说就是将HTTP请求参数绑定到程序方法入参上,该变量可以是简单类型,也可以是复杂类。

绑定源

所谓绑定源,是指用于模型绑定的值来源。

先举个例子:

[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容器。

建议大家在编写接口时,尽量显式指明绑定源。

在绑定的时候,可能会遇到以下两种情况:

情况一:模型属性在绑定源中不存在

什么是模型属性在绑定源中不存在?给大家举个例子:

[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
上,你可以通过表单或查询字符串传入,可以采用以下格式之一:

  • 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
方法:

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; }
}
  • 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
    方法重新验证:

    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]
    :验证数值属性是否在指定范围内
  • github" target=_blank>[/code]:验证属性的格式是否为URL
  • [Phone]
    :验证属性的格式是否为电话号码
  • [EmailAddress]
    :验证属性的格式是否为邮箱地址
  • [Compare]
    :验证当前属性和指定的属性是否匹配
  • [RegularExpression]
    :验证属性是否和正则表达式匹配
  • 大家一定或多或少都接触过这些特性。不过,我并不打算详细介绍这些特性的使用,因为这些特性的局限性较高,不够灵活。

    那有没有更好用的呢?当然有,接下来就给大家介绍一款验证库——

    FluentValidation

    FluentValidation

    FluentValidation
    是一款免费开源的模型验证库,通过它,你可以使用Fluent接口和Lambda表达式来构建强类型的验证规则。

    接下来,跟我一起感受

    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
    必须在
    AddMvc
    之后注册,因为其需要使用Mvc的服务。

    通过

    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}
      :范围验证器的范围上限
    • {ExpectedPrecision}
      :decimal精度验证器的数字总位数
    • {ExpectedScale}
      :decimal精度验证器的小数位数
    • {Digits}
      :decimal精度验证器正在验证的数字实际整数位数
    • {ActualScale}
      :decimal精度验证器正在验证的数字实际小数位数

    这些占位符,只能运用在特定的验证器中。更多占位符的详细介绍,请查看官方文档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数据结构,你可以按照自己的方式来。

    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: