8. 数据校验

8.1 关于数据校验#

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

8.2 数据校验的好处#

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

8.3 数据校验方式#

  • 传统方式,在业务代码之前手动验证
  • Mvc 特性方式,Mvc 内置的 DataAnnotations 方式
  • 推荐方式Furion 框架内置的 DataValidation 验证
  • 其他方式,使用第三方验证库,如 FluentValidation

8.3.1 传统方式#

在很多老项目中,我们经常看到这样的代码:

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("两次密码不一致");
}
// 业务代码
_repository.Insert(person.Adapt<PersonEntity>());
// ...
}

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

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

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

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

8.3.2 Mvc 特性方式#

ASP.NET Core 中,微软为我们提供了全新的 特性 验证方式,可通过对对象贴特性实现数据验证。这种方式有效的将数据校验和业务代码剥离开来,而且容易使用和拓展。

  • 在模型中验证
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; }
}
}
  • 在参数中验证
public void CheckMethodParameterValid(
[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 个,建议抽离出模型类,也就是不建议上面的方式。

  • 自定义特性验证
public class ClassicMovieAttribute : ValidationAttribute
{
public ClassicMovieAttribute(int year)
{
Year = year;
}
public int Year { get; }
public string GetErrorMessage() =>
$"Classic movies must have a release year no later than {Year}.";
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var movie = (Movie)validationContext.ObjectInstance;
var releaseYear = ((DateTime)value).Year;
if (movie.Genre == Genre.Classic && releaseYear > Year)
{
return new ValidationResult(GetErrorMessage());
}
return ValidationResult.Success;
}
}
  • IValidatableObject 复杂验证
using System.Collections.Generic;
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) } // 验证失败的属性
);
}
}
}

Mvc 特性方式极大的将业务逻辑和验证进行了剥离和解耦,而且还能实现自定义复杂验证。

但是 Mvc 特性验证方式有几个明显的缺点

  • 只能在 控制器 中的 Action(动作方法)中使用
  • 无法在任意类、任意方法中使用
  • 内置的验证类型非常有限,且不易拓展
  • 不支持验证消息后期配置

所以,Furion 提供了新的验证引擎 DataValidation,在完全兼容 Mvc 内置验证的同时提供了大量常见验证、复杂验证、自定义验证等能力。

8.4 DataValidation 验证 🤗#

DataValidationFurion 框架提供了全新的验证方式,完全兼容 Mvc 内置验证,并且赋予了超能。

8.4.1 DataValidation 优点#

  • 完全兼容 Mvc 内置验证引擎
  • 内置常见验证类型及可自定义验证类型功能
  • 提供全局对象拓展验证方式
  • 支持验证消息后期配置,支持实时更新
  • 支持在任何类,任何方法、任何位置实现手动验证、特性方式验证等
  • 支持设置验证结果模型

8.5 DataValidation 使用#

备注

.AddDataValidation() 默认已经继承在 AddInject() 中了,无需再次注册。也就是 8.5.1 章节可不配置。

8.5.1 注册验证服务#

Furion.Web.Core\FurWebCoreStartup.cs
using Microsoft.Extensions.DependencyInjection;
namespace Furion.Web.Core
{
[AppStartup(800)]
public sealed class FurWebCoreStartup : AppStartup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddDataValidation();
}
}
}
特别注意

.AddDataValidation() 需在 services.AddControllers() 之后注册。

8.5.2 兼容 Mvc 特性验证#

using System.ComponentModel.DataAnnotations;
namespace Furion.Application
{
public class TestDto
{
[Range(10, 20, ErrorMessage = "Id 只能在 10-20 区间取值")]
public int Id { get; set; }
[Required(ErrorMessage = "必填"), MinLength(3, ErrorMessage = "字符串长度不能少于3位")]
public string Name { get; set; }
}
}

如下图所示:

8.5.3 兼容 Mvc 复杂验证#

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Furion.Application
{
public class TestDto : IValidatableObject
{
[Range(10, 20, ErrorMessage = "Id 只能在 10-20 区间取值")]
public int Id { get; set; }
[Required(ErrorMessage = "必填"), MinLength(3, ErrorMessage = "字符串长度不能少于3位")]
public string Name { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Name.StartsWith("Furion"))
{
yield return new ValidationResult(
"不能以 Furion 开头"
, new[] { nameof(Name) }
);
}
}
}
}

如下图所示:

8.6 手动验证#

8.6.1 验证模型#

using Furion.DataValidation;
using Furion.DynamicApiController;
namespace Furion.Application
{
public class FurAppService : IDynamicApiController
{
[NonValidation] // 跳过全局验证
public DataValidationResult Post(TestDto testDto)
{
return testDto.TryValidate();
}
}
}

如下图所示:

note

支持 Mvc 内置的特性验证、属性验证及复杂的 IValidatableObject 验证。

8.6.2 TryValidateValidate#

Furion 提供了 TryValidate()Validate() 两个验证拓展方法,唯一的区别就是后者验证失败将自动抛出异常消息。

8.6.3 ValidationTypes 常见验证#

Furion 内置了很多常用类型的数据验证,包括:

  • Required:非空、非 Null 类型
  • Numeric:数值类型
  • PositiveNumber:正数类型
  • NegativeNumber:负数类型
  • Integer:整数类型
  • Money:金钱类型
  • Date:日期类型
  • Time:时间类型
  • IDCard:身份证类型
  • PostCode:邮编类型
  • PhoneNumber:手机号类型
  • Telephone:固话类型
  • PhoneOrTelNumber:手机或固话类型
  • EmailAddress:邮件地址类型
  • Url:网址类型
  • Color:颜色值类型
  • Chinese:中文类型
  • IPv4:IPv4 地址类型
  • IPv6:IPv6 地址类型
  • Age:年龄类型
  • ChineseName:中文名类型
  • EnglishName:英文名类型
  • Capital:纯大写英文类型
  • Lowercase:纯小写英文类型
  • Ascii:Ascii 类型
  • Md5:Md5 字符串类型
  • Zip:压缩包格式类型
  • Image:图片格式类型
  • Document:文档格式类型
  • MP3:Mp3 格式类型
  • Flash:Flash 格式类型
  • Video:视频文件格式类型

使用示例

// 验证必填
"".TryValidate(ValidationTypes.Required); // => false
// 验证中文
"我叫 MonK".TryValidate(ValidationTypes.Chinese); // => false
// 验证数值
2.TryValidate(ValidationTypes.Numeric); // => true
// 验证整数
true.TryValidate(ValidationTypes.Integer); // => false
// 验证邮箱
"monksoul@outlook.com".TryValidate(ValidationTypes.EmailAddress); // => true
// 验证负数
2.0m.TryValidate(ValidationTypes.NegativeNumber); // => false
// 自定义正则表达式验证
"Furion".TryValidate("/^Furion$"); // => true
小知识

可通过设置 TryValidate([ValidationOptions], params object[] validationTypes) 方法的 ValidationOptions 参数配置验证逻辑,如:同时成立只要一个成立 即可验证通过

8.6.4 [DataValidation] 特性#

Furion 还提供了 [DataValidation] 特性方便在模型参数中使用 ValidationTypes 常见验证或自定义验证。

using Furion.DataValidation;
namespace Furion.Application
{
public class TestDto
{
[DataValidation(ValidationTypes.Integer)]
public int Id { get; set; }
[DataValidation(ValidationTypes.Numeric, ValidationTypes.Integer)]
public int Cost { get; set; }
[DataValidation(ValidationOptions.AtLeastOne, ValidationTypes.Chinese, ValidationTypes.Date)]
public string Name { get; set; }
// 可以和Mvc特性共存
[Required, DataValidation(ValidationTypes.Age)]
public int Age { get; set; }
[DataValidation(ValidationTypes.IDCard, ErrorMessage = "自定义身份证提示消息")]
public string IDCard { get; set; }
}
}

8.7 [NonValidation] 跳过验证#

Furion 框架提供了对象模型跳过验证特性 [NonValidation],支持在 控制器动作方法 中使用。

一旦贴了此特性,那么将不会执行验证操作。

note

[NonValidation] 只对对象类型有效,值类型无效。

8.8 高级自定义操作#

8.8.1 自定义 ValidationTypes 类型#

除了 Furion 内置的验证类型以外,Furion 还提供了非常灵活的自定义验证类型机制。

实现自定义验证类型必须遵循以下配置:

  • 验证类型必须时公开且是 Enum 枚举类型
  • 枚举类型必须贴有 [ValidationType] 特性
  • 枚举中每一项必须贴有 [ValidationItemMetadata] 特性

using Furion.DataValidation;
using System.Text.RegularExpressions;
namespace Furion.Application
{
[ValidationType]
public enum MyValidationTypes
{
/// <summary>
/// 强密码类型
/// </summary>
[ValidationItemMetadata(@"^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$", "必须须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间")]
StrongPassword,
/// <summary>
/// 以 Furion 字符串开头,忽略大小写
/// </summary>
[ValidationItemMetadata(@"^(furion).*", "默认提示:必须以Fur字符串开头,忽略大小写", RegexOptions.IgnoreCase)]
StartWithFurString
}
}

使用

  • 手动使用
"q1w2e3".TryValidate(MyValidationTypes.StrongPassword); // => false
"furos".TryValidate(MyValidationTypes.StartWithFurString); // => true
  • [DataValidation] 中使用
[DataValidation(MyValidationTypes.StrongPassword)]
public string Password { get; set; }
  • 多个自定义类型混用
"Q1w2e3r4t5!*".TryValidate(MyValidationTypes.StrongPassword, ValidationTypes.Required); // => true
特别注意

自定义的验证类型也要保证名称全局唯一,也就是多个验证类型不能出现一样的名字。

8.8.2 自定义 ValidationTypes 失败消息#

Furion 内置的 ValidationTypes 已有默认的失败消息:

  • RequiredThe Value is required.
  • NumericThe Value is not a numeric type.
  • PositiveNumberThe Value is not a positive number type.
  • NegativeNumberThe Value is not a negative number type.
  • IntegerThe Value is not a integer type.
  • MoneyThe Value is not a money type.
  • DateThe Value is not a date type.
  • TimeThe Value is not a time type.
  • IDCardThe Value is not a idcard type.
  • PostCodeThe Value is not a postcode type.
  • PhoneNumberThe Value is not a phone number type.
  • TelephoneThe Value is not a telephone type.
  • PhoneOrTelNumberThe Value is not a phone number or telephone type.
  • EmailAddressThe Value is not a email address type.
  • UrlThe Value is not a url address type.
  • ColorThe Value is not a color type.
  • ChineseThe Value is not a chinese type.
  • IPv4The Value is not a IPv4 type.
  • IPv6The Value is not a IPv6 type.
  • AgeThe Value is not a age type.
  • ChineseNameThe Value is not a chinese name type.
  • EnglishNameThe Value is not a english name type.
  • CapitalThe Value is not a capital type.
  • LowercaseThe Value is not a lowercase type.
  • AsciiThe Value is not a ascii type.
  • Md5The Value is not a md5 type.
  • ZipThe Value is not a zip type.
  • ImageThe Value is not a image type.
  • DocumentThe Value is not a document type.
  • MP3The Value is not a mp3 type.
  • FlashThe Value is not a flash type.
  • VideoThe Value is not a video type.

我们可以通过创建继承 IValidationMessageTypeProvider 验证消息提供器类型,或通过 appsettings.json 配置。

  • [ValidationMessageType] 方式
using Furion.DataValidation;
namespace Furion.Application
{
[ValidationMessageType]
public enum MyValidationMessageType
{
// 修改内置验证类型验证失败消息
[ValidationMessage("值不能为空或Null")]
Required,
[ValidationMessage("必须是数值类型")]
Numeric,
[ValidationMessage("必须是正数")]
PositiveNumber,
// 修改自定义类型验证失败消息
[ValidationMessage("密码太简单了")]
StrongPassword,
[ValidationMessage("必须以 Furion 开头")]
StartWithFurString
}
}
小知识

除了贴 [ValidationMessageType] 特性外,Furion 框架还提供了 IValidationMessageTypeProvider 方式查找验证消息类型,如下图所示:

using Furion.DataValidation;
using System;
namespace Furion.Application
{
public class MyValidationTypeMessageProvider : IValidationMessageTypeProvider
{
public Type[] Definitions => new[]
{
typeof(MyValidationMessageType),
typeof(MyValidationMessageType2)
};
}
}

注册验证消息提供器

Furion.Web.Core\FurWebCoreStartup.cs
using Microsoft.Extensions.DependencyInjection;
namespace Furion.Web.Core
{
[AppStartup(800)]
public sealed class FurWebCoreStartup : AppStartup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddDataValidation<MyValidationTypeMessageProvider>();
}
}
}

如下图所示:

  • appsettings.json 方式
Furion.Web.Entry/appsettings.json
{
"ValidationTypeMessageSettings": {
"Definitions": [
["Required", "值不能为空或Null"],
["Numeric", "必须是数值类型"],
["StrongPassword", "密码太简单了!!!"]
]
}
}
important

appsettings.json 中相同的 Key 会覆盖 IValidationMessageTypeProvider 提供相同 Key 的值。

错误消息查找优先级#

DefaultErrorMessage -> IValidationMessageTypeProvider -> appsettings.json (低 -> 高)

8.9 模型验证范围#

Furion 提供多种模型验证范围设置:

  • 全局验证(默认)
  • [NonValidation] 跳过验证
  • [TypeFilter(typeof(DataValidationFilter))] 局部验证
  • [ApiController] 控制器范围验证

8.9.1 全局验证#

默认情况下,通过 .AddDataValidation() 注册数据验证服务已经启用了全局验证,如若不想启用全局验证,则传入 false 即可,如:.AddDataValidation(false)

8.9.2 [NonValidation] 跳过验证#

可通过 [NonValidation] 贴在 控制器动作方法 中跳过全局验证或不需要验证

8.9.3 [TypeFilter(typeof(DataValidationFilter))] 局部验证#

我们也可以无需注册 .AddDataValidation() 服务,直接在 动作方法 上贴 [TypeFilter(typeof(DataValidationFilter))] 可启用局部验证。如:

using Furion.DataValidation;
using Furion.DynamicApiController;
using Microsoft.AspNetCore.Mvc;
namespace Furion.Application
{
public class FurAppService : IDynamicApiController
{
[TypeFilter(typeof(DataValidationFilter))]
public TestDto Post(TestDto testDto)
{
return testDto;
}
}
}

8.9.4 [ApiController] 控制器范围验证#

[ApiController]Mvc 提供的控制器范围(含所有动作方法)的验证。

using Microsoft.AspNetCore.Mvc;
namespace Furion.Web.Entry.Controllers
{
[ApiController]
public class MvcController : Controller
{
public IActionResult Index()
{
return View();
}
}
}

8.10 MiniProfiler 查看#

如下图所示:

8.11 多语言支持#

文档整理中...

8.12 反馈与建议#

与我们交流

给 Furion 提 Issue