# 十一、数据校验

## 数据校验介绍

数据校验字面上的意思就是对使用者提交过来的数据进行合法性验证。在一套完善的应用系统中，数据有效性校验是必不可少的第一道关卡。

## 数据校验好处

1. 过滤不安全数据，提高系统的安全性
2. 减少不必要的业务异常处理，提高系统的响应速度
3. 大大提高系统稳定性
4. 大数据并发时起着一定的缓冲作用

## 数据校验三种方式

* 常见方式，在业务代码中编写验证逻辑
* 特性方式，在参数或属性中贴特性
* FluentValidation 方式，支持链式验证

## 常见方式（不推荐）

在过去的开发中，我们常常把数据校验写在业务代码的顶部，比如：

```csharp
public bool Insert(Person person)
{
    // 验证参数
    if(string.IsNullOrEmty(person.Name))
    {
        throw new System.Exception("名字不能为空");
    }
    
    if(person.Age < 18)
    {
        throw new System.Exception("年龄不能小于 18 岁");
    }
    
    if(!person.Password.Equals(person.ConfirmPassword){
        throw new System.Exception("两次密码不一致");
    }
    
    // 业务代码
    _testRepository.Insert(person.Adapt<PersonEntity>());
    
    // ...
}
```

从上面的代码看起来，似乎没有什么不妥，但是从一个程序可维护性来说，这是一个糟糕的代码，因为该业务代码中包含了太多与业务无关的数据验证。

试想一下，如果这个 `Person` 有 几十个参数都需要验证呢？可想而知，这是一个庞大的业务代码。

再者，如果其他地方也需要用到这个 `Person` 类验证呢？那代码好比老鼠啃过的面包屑一样，到处都是。

**如此得知，这样的方式是极其不推荐的，不但污染了业务代码，也破坏了业务职责单一性原理，也让验证逻辑无法实现通用，后续维护难度大大升级。**

所以，我们迫切需要一种新的方式去解决上述问题。

## 特性注解方式（推荐）

微软提供了 `System.ComponentModel.DataAnnotations` 组件可通过特性方式实现数据模型进行元数据验证。完整的将数据校验和业务代码剥离开来，而且极易使用和自定义拓展。如：

```csharp
using System.ComponentModel.DataAnnotations;

namespace Hoa.Application.Authorization.Dtos
{
    public class SignInInput
    {
        [Required]  // 必填验证
        [MinLength(4)]  // 最小长度验证
        public string Account { get; set; }

        [Required]    // 必填验证
        [MaxLength(32)]    // 最大长度验证
        public string Password { get; set; }
    }
}
```

除了在模型中实现特性方式验证，还可以在方法参数中实现验证，如：

```csharp
public void CheckMethodParameterVaild(
    [Required]    // 必填验证
    [MinLength(4)]    // 最小长度验证
    string name,
    int age,
    [Required]    // 必填验证
    [RegularExpression("[a-zA-Z0-9_]{8,30}")    // 正则表达式验证
    string password,
    [Required]    // 必填验证
    [RegularExpression("[a-zA-Z0-9_]{8,30}")    // 正则表达式验证
    string confirmPassword
)
{
    // TODO
}
```

**特别申明：如果函数的参数大于或等于3个，建议抽离出模型类，也就是不建议上面的方式。**

在 asp.net core 中，微软提供了这种极其灵活的特性验证方式验证数据，大大的减少了业务代码的复杂度，又能实现业务代码和验证解耦。

### asp.net core 内置验证特性

* `[Required]`：必填
* `[MinLength(20)]`：最小长度
* `[MaxLength(20)]`：最大长度
* `[StringLength(20)]`：字符串最大长度
* `[EmailAddress]`：验证电子邮件格式
* `[Phone]`：验证手机号码
* `[Range]`：验证是否在指定范围
* `[Compare(nameof(Value))]`：校对属性是否一致
* `[Url]`：判断是否是 Url 地址
* `[RegularExpression]`：正则表达式

### 自定义验证特性

除了内置的这些验证特性以外，asp.net core 还提供了一种**自定义验证特性**方式，这样我们就可以抽离出一些通用的验证特性了。如新增 `[Custom]` 验证特性

```csharp
public class CustomAttribute : ValidationAttribute
{
    public CustomAttribute(object param)
    {
        Param = param;
    }

    public object Param { get; }    // 你的额外参数

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var obj =validationContext.ObjectInstance;

        if (你的逻辑)
        {
            return new ValidationResult("错误消息");
        }

        return ValidationResult.Success;
    }
}
```

```csharp
public class MyClass
{
    [Custom]
    public string MyData { get; set; }
}
```

### IValidatableObject 方式

另外，asp.net core 还提供了接口验证模式，**通常这样的模式用于不通用的验证模型，也就是特定类的特定验证。**&#x8FD9;种验证方式只需要在模型类中实现 `IValidatableObject` 接口即可。如：

```csharp
public class DtoModel : IValidatableObject
{

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

    // 你的验证逻辑
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (你的逻辑代码)
        {
            yield return new ValidationResult(
                "错误消息"
                ,new[] { nameof(Title) }  // 验证失败的属性
            );    
        }
    }
}
```

关于更多内置的 asp.net core 模型验证[可查看官方文档](https://docs.microsoft.com/zh-cn/aspnet/core/mvc/models/validation?view=aspnetcore-3.1)。

## FluentValidation（推荐）

在开发一套复杂的应用系统时，我们的数据校验可能非常复杂，比如涉及到场景验证、验证结果多语言控制、能够在验证之前和之后编写逻辑等等。这时，asp.net core 内置的验证方式很难满足我们的需求，所以，这里推荐使用一个非常优秀的第三方组件 [FluentValidation](https://fluentvalidation.net/)。

FluentValidation 内置丰富的链式验证，提供 AOP 面向切面方式验证，不干扰业务代码，不污染业务模型，是数据校验最佳实现方式。如：

```csharp
using FluentValidation;

namespace Hoa.Application.Authorization.Dtos
{
    public class SignInInput
    {
        public string Account { get; set; }
        
        public string Password { get; set; }
    }

    // FluentValidation配置
    public class SignInInputValidator : AbstractValidator<SignInInput>
    {
        public SignInInputValidator()
        {
            RuleFor(u => u.Password).NotNull().NotEmpty().WithMessage("密码不能为空")
                .MaximumLength(32).WithMessage("密码长度不能超过32位");
        }
    }
}
```

另外， [FluentValidation](https://fluentvalidation.net/) 也兼容asp.net core 内置的模型验证方式，也就是可以实现共同作用：

```csharp
using FluentValidation;
using System.ComponentModel.DataAnnotations;

namespace Hoa.Application.Authorization.Dtos
{
    public class SignInInput
    {
        [Required]
        [MinLength(4)]
        public string Account { get; set; }

        [Required]
        [MinLength(6)]
        public string Password { get; set; }
    }

    // FluentValidation配置
    public class SignInInputValidator : AbstractValidator<SignInInput>
    {
        public SignInInputValidator()
        {
            RuleFor(u => u.Password).NotNull().NotEmpty().WithMessage("密码不能为空")
                .MaximumLength(32).WithMessage("密码长度不能超过32位");
        }
    }
}
```

在这里，就不多介绍 [FluentValidation](https://fluentvalidation.net/) 验证方式了，更多 [FluentValidation](https://fluentvalidation.net/)  [可查看官方文档](https://fluentvalidation.net/)。

## 跳过验证

只需要在方法或类上面贴 `[NotVaild]` 特性即可。

## 特别说明

在 Hoa Framework 中，不管是哪一种验证方式，框架内部都实现了自动验证，无需程序员手动干涉验证。也就是无需调用如下代码：

```csharp
if(!ModelState.IsVaild)
{
    // ....
}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://monksoul.gitbook.io/hoa/shujujiaoyan.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
