fulu

背景

.NETCore下的模型验证相信绝大部分的.NET开发者或多或少的都用过,微软官方提供的模型验证相关的类位于System.ComponentModel.DataAnnotations命令空间下,在使用的时候只需要给属性添加不同的特性即可实现对应的模型验证。如下所示:

public class Movie
{
    public int Id { get; set; }

    [Required]
    [StringLength(100)]
    public string Title { get; set; }
}

在WebApi中,当请求接口时,程序会自动对模型进行验证,如无法验证通过,则会直接终止后续的逻辑执行,并响应400状态码,响应内容如下所示:

{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-4b16460fc83d7b4daa4f10d939016982-f823eebede419a4a-00",
"errors": {
"aa": [
"The aa field is required."
]
}
}

当然,你也可以自定义响应的内容,这不是本文的重点。本文的重点是,.NETCore系统默认的模型验证功能并不够强大,仅支持在Controller的Action中使用,不支持非Controller中或者控制台程序的验证,且代码侵入性较强。

而FluentValidation(https://fluentvalidation.net/ )则是功能更为强大的模型验证框架,支持任何场景下的模型验证,且不侵入代码。

下面就来和笔者一起了解下FluentValidation的用法。

接入

FluentValidation支持一下平台:

  • .NET 4.6.1+
  • .NET Core 2.0+
  • .NET Standard 2.0+

各个平台的集成方式大同小异,本文仅讲解.NETCore3.1的集成方式。

首先,使用NuGet安装FluentValidation.AspNetCore依赖。

添加需要验证的模型类,如Student类,代码如下:

public class Student
{
    public int Id { get; set; }

    public int Age { get; set; }

    public string Name { get; set; }
}

然后创建类StudentValidator,并集成类AbstractValidator,代码如下:

public class StudentValidator : AbstractValidator<Student>
{
    public StudentValidator()
    {
        RuleFor(x => x.Age).InclusiveBetween(10, 50);
        RuleFor(x => x.Name).NotEmpty().MaximumLength(5);
    }
}

上述的验证类中,要求Age大于10且小于50,Name不为空,且长度小于5。

最后,还需要将验证类注册到服务中。修改Startup的ConfigureServices,部分代码如下:

services.AddControllers().AddFluentValidation(conf =>
    {
        conf.RegisterValidatorsFromAssemblyContaining<StudentValidator>();
        conf.RunDefaultMvcValidationAfterFluentValidationExecutes = false;
    });

上述代码中,RegisterValidatorsFromAssemblyContaining方法的作用是扫描StudentValidator类所在的程序集中的所有验证类,并注册到服务中。

RunDefaultMvcValidationAfterFluentValidationExecutes为false时,会屏蔽掉系统默认的模型验证,如需兼容系统默认的模型验证,将RunDefaultMvcValidationAfterFluentValidationExecutes的值改为true即可。此参数默认为true。

下面在Controller中,添加一个Action,代码如下:

[HttpPost]
public IActionResult Add([FromBody] Student student)
{
    return Ok(student);
}

打开swagger,访问接口,响应如下所示:

{
  "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "traceId": "00-6331a76578228b4cb9044aa40f514bc9-89fd8547c1921340-00",
  "errors": {
    "Age": [
      "'Age' 必须在 10 (包含)和 25 (包含)之间, 您输入了 0。"
    ],
    "Name": [
      "'Name' 必须小于或等于5个字符。您输入了6个字符。"
    ]
  }
}

至此,在 ASP.NET Core中集成FluentValidation就完成了。但到现在为止,这和系统默认的模型验证并没有区别。 在文章的开头笔者也提到过,FluentValidation不仅支持Controller中对模型进行验证,下面的代码就是非Controller场景下的验证。

public class DemoService
{
    private readonly IValidator<Student> _studentValidator;

    public DemoService(IValidator<Student> studentValidator)
    {
        _studentValidator = studentValidator;
    }

    public bool Run(Student student)
    {
        var valid = _studentValidator.Validate(student);
        return valid.IsValid;
    }
}

在上述代码中,通过构造函数注入的方式,获取到了IValidator实例,在Run方法中只需要调用Validate方法,参数是需要验证的对象,返回的对象就包含了验证的是否通过以及不通过时,具体的错误信息。

基础用法

内置规则

FluentValidation内置了多个常用的验证器,下面简单介绍几个特别常用或容易出错的验证器。

NotNull 和 NotEmpty

NotNull是确保指定的属性不为null,NotEmpty则表示确保指定的属性不为null、空字符串或空白(值类型的默认值,比如int类型的默认值为0),如果int类型属性设置NotEmpty验证器,则当值为0时,验证是无法通过的。

NotEqual 和 Equal

NotEqual 和 Equal分别是不相等和相等验证器,可与指定的值或者指定的属性进行比较。

MaximumLength、MinimumLength和Length

MaximumLength为最大长度验证器,MinimumLength为最小长度验证器,而Length则是二者的结合,需要注意的是,这三种验证器仅对字符串有效,且不会验证null,当值为null时,则不对长度进行验证,所以使用长度验证器时,建议结合NotNull一起使用。

LessThan、LessThanOrEqualTo、GreaterThan、GreaterThanOrEqualTo

上述的几个验证器为比较验证器,仅适用于继承IComparable接口的属性,分别表示的是:小于、小于或等于、大于、大于或等于。

Matches

正则表达式验证器,用于确保指定的属性与给定的正则表达式匹配。

ExclusiveBetween和InclusiveBetween

示例代码如下:

RuleFor(x => x.Id).ExclusiveBetween(1,10);
RuleFor(x => x.Id).InclusiveBetween(1,10);

以上代码均表示输入的Id的值需要在1,10之间,而两者的区别是,InclusiveBetween验证器是包含头和尾的,而ExclusiveBetween是不包含的,例如当Id值为1时,ExclusiveBetween验证失败,但InclusiveBetween则验证成功。

覆盖验证器默认的错误提示

在文章的开头提到了,当验证Student的Age属性不通过时,提示信息是:'Age' 必须在 10 (包含)和 25 (包含)之间, 您输入了 0。

这个提示信息对于开发者来讲,定位问题已经很清晰了,但如果要在WebApi中讲验证的错误信息返回给前端,那么这个提示就会被用户看到,则此错误信息就不太友好,FluentValidation提供了多种覆盖错误提示的方式,下面就来一起看下。

占位符

我们可以将验证Age的代码改为如下所示:

RuleFor(x => x.Age).InclusiveBetween(10, 25).WithMessage("年龄必须在{From}到{To}之间");

当验证不通过时,输出的错误信息则为:年龄必须在10到25之间。

程序自动将{From}和{To}进行了替换。每个验证器的占位符都不一样,有关占位符的完整列表,请查看官方文档 https://docs.fluentvalidation.net/en/latest/built-in-validators.html。

覆盖属性名称

此方法是将属性的名称使用指定的字符串替换,如下所示:

RuleFor(x => x.Age).InclusiveBetween(10, 25).WithName("年龄");

当发生错误时,会自动将系统默认的错误提示信息中的"Age"替换为"年龄"

默认情况下,When或者Otherwise将应用于链式调用的所有前置的验证器,如果只希望条件引用于前面的第一个验证器,则必须使用ApplyConditionTo.CurrentValidator显示指定

 RuleFor(x => x.Age).GreaterThan(10).LessThan(20).When(x => x.Sex == 2,ApplyConditionTo.CurrentValidator);

上述的代码,如果不加ApplyConditionTo.CurrentValidator,则当Sex等于2时,则要求Age大于10且小于20。而Sex不等于2时,则不作任何验证。如果加上ApplyConditionTo.CurrentValidator,则Age大于10的验证跟Sex的值没有任何关系了,程序会始终验证Age是否大于10

带条件的验证规则

使用When方法可控制规则执行的条件。例如,国家的法定结婚年龄为女性20岁,则验证年龄属性时,只有当性别为女时,才对年龄大于等于20进行校验。

RuleFor(x => x.Age).GreaterThan(20).When(x => x.Sex == 2);

相反的,Unless表示的是当指定条件不满足时,才执行校验。

RuleFor(x => x.Age).GreaterThan(20).Unless(x => x.Sex == 2);

上述代码表示当Sex值不为2时,校验Age是否大于等于20

如果需要为多个验证规则指定相同的条件,可以调用When的*方法,而不是在规则末尾调用When方法。

When(x => x.Sex == 2, () =>
{
    RuleFor(x => x.Name).Must(x => !x.EndsWith("国庆"));
    RuleFor(x => x.Age).LessThan(30);
});

上述代码表示是,当Sex等于2时,Age需要小于30,并且名字不能以"国庆"结尾。

将Otherwise方法链接到When调用,表示When条件不满足时,执行的验证规则。

When(x => x.Sex == 2, () =>
{
    RuleFor(x => x.Name).Must(x => x.EndsWith("国庆"));
    RuleFor(x => x.Age).LessThan(30);
}).Otherwise(() =>
{
    RuleFor(x => x.Age).LessThan(50);
});

上述代码中的Otherwise方法表示的是,当Sex不等于2时,则Age需要小于50

链式调用

当一个属性使用多个验证规则时,可将多个验证器链接在一起,比如,Student类的Name属性不能为空,并且,长度需要小于10,则对应的代码为:

public StudentValidator()
{
    RuleFor(x =>x.Name).NotEmpty().MaximumLength(10);
}

CascadeMode

CascadeMode是一个枚举类型的属性,有两个选项:Continue和Stop

如果设置为Stop,则检测到失败的验证,则立即终止,不会继续执行剩余属性的验证。默认值为Continue

CascadeMode = CascadeMode.Stop;
RuleFor(x => x.Name).NotEmpty().MaximumLength(10);
RuleFor(x => x.NickName).NotEmpty().MaximumLength(10);

如上述代码所示,当Name值不满足要求时,则会停止对NickName的校验

依赖规则

默认情况下,FluentValidation 中的所有规则都是独立的,不能彼此影响。这是异步验证工作所必需的,也是必要的。但是,在某些情况下,您可能希望确保某些规则仅在另一个规则完成之后执行。您可以使用DependentRules它来做到这一点。

比如,只有身高超过130的儿童,才需要验证是否购票,则可以通过如下的代码实现:

RuleFor(x => x.Height).GreaterThan(130).DependentRules(() =>
{
    RuleFor(x => x.HasTicket).NotEmpty();
});

高级用法

异步验证

在某些情况下,你可能希望定义异步规则,比如从数据库或者外部api判断。

public StudentValidator(IStudentService studentService)
{
    _studentService = studentService;
    RuleFor(x => x.Name).MustAsync(async (name, token) => await _studentService.CheckExist(name));
}

上述代码中,通过一个异步方法的返回值验证Name属性。
另外,如果在非Controller场景下使用,则必须调用ValidateAsync方法进行验证。

转换值

您可以在对属性值执行验证之前使用 Transform方法转换属性值。

RuleFor(x => x.Weight).Transform(x => int.TryParse(x, out int val)?(int?)val:null).GreaterThan(10);

上述代码先试图将string类型转换成int类型,如果转换成功则对转换后的值做大于验证。如果转换失败,则不做验证。

回调

如果验证失败,可以使用回调做一些操作。

RuleFor(x => x.Weight).NotEmpty().OnFailure(x =>
            {
                Console.WriteLine("验证失败");
            });

预验证

如果需要每次调用验证器前运行特定代码,可以通过重写PreValidate方法来做到这一点。

public class StudentValidator : AbstractValidator<Student>
{
    public StudentValidator()
    {
        RuleFor(x => x.Weight).NotEmpty();
    }

    protected override bool PreValidate(ValidationContext<Student> context,ValidationResult result)
    {
        if (context.InstanceToValidate == null) return true;
        result.Errors.Add(new ValidationFailure("", "实体不能为null"));
        return false;
    }
}

福禄ICH.架构出品

作者:福尔斯

2021年3月

相关文章: