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)
{
// 还可以解析服务
var service = validationContext.GetService(typeof(类型));
if (你的逻辑代码)
{
yield return new ValidationResult(
"错误消息"
,new[] { nameof(Title) } // 验证失败的属性
);
}
}
}
Mvc
特性方式极大的将业务逻辑和验证进行了剥离和解耦,而且还能实现自定义复杂验证。
但是 Mvc
特性验证方式有几个明显的缺点:
- 只能在
控制器
中的Action
(动作方法)中使用 - 无法在任意类、任意方法中使用
- 内置的验证类型非常有限,且不易拓展
- 不支持验证消息后期配置
所以,Furion
提供了新的验证引擎 DataValidation
,在完全兼容 Mvc
内置验证的同时提供了大量常见验证、复杂验证、自定义验证等能力。
8.4 DataValidation
验证 🤗
DataValidation
是 Furion
框架提供了全新的验证方式,完全兼容 Mvc
内置验证,并且赋予了超能。
8.4.1 DataValidation
优点
- 完全兼容
Mvc
内置验证引擎 - 内置常见验证类型及可自定义验证类型功能
- 提供全局对象拓展验证方式
- 支持验证消息后期配置,支持实时更新
- 支持在任何类,任何方法、任何位置实现手动验证、特性方式验证等
- 支持设置验证结果模型
8.5 DataValidation
使用
备注
.AddDataValidation()
默认已经集成在 AddInject()
中了,无需再次注册。也就是 8.5.1
章节可不配置。
8.5.1 注册验证服务
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
特性验证
- TestDto
- FurionAppService
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; }
}
}
using Furion.DynamicApiController;
namespace Furion.Application
{
public class FurionAppService : IDynamicApiController
{
/// <summary>
/// 值类型验证
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public int Get(int id)
{
return id;
}
/// <summary>
/// 对象类型验证
/// </summary>
/// <param name="testDto"></param>
/// <returns></returns>
public TestDto Post(TestDto testDto)
{
return testDto;
}
}
}
如下图所示:
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)
{
// 还可以解析服务
var service = validationContext.GetService(typeof(类型));
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 FurionAppService : IDynamicApiController
{
[NonValidation] // 跳过全局验证
public DataValidationResult Post(TestDto testDto)
{
return testDto.TryValidate();
}
}
}
如下图所示:
note
支持 Mvc
内置的特性验证、属性验证及复杂的 IValidatableObject
验证。
8.6.2 TryValidate
和 Validate
Furion
提供了 TryValidate()
和 Validate()
两个验证拓展方法,唯一的区别就是后者验证失败将自动抛出异常消息。
8.6.3 ValidationTypes
常见验证
Furion
内置了很多常用类型的数据验证,包括:
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
:视频文件格式类型
使用示例
- 单个类型验证
- 多个组合类型验证
// 验证中文
"我叫 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
// 验证数值类型且是整数
"20".TryValidate(ValidationTypes.Numeric, ValidationTypes.Integer); // => true
// 验证时日期或时间格式
"2020-05-20".TryValidate(ValidationPattern.AtLeastOne, ValidationTypes.Date, ValidationTypes.Time); // => true
"23:45:20".TryValidate(ValidationPattern.AtLeastOne, ValidationTypes.Date, ValidationTypes.Time); // => true
小知识
可通过设置 TryValidate([ValidationPattern], params object[] validationTypes)
方法的 ValidationPattern
参数配置验证逻辑,如:同时成立
或 只要一个成立
即可验证通过
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(ValidationPattern.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; }
}
}
[DataValidation]
特性具备 ValidationAttribute
特性的所有配置以外还提供了以下配置:
ValidationTypes
:验证类型,Enum[]
类型,ValidationPattern
:验证逻辑,ValidationPattern
类型,可选AllOfThem(全部验证通过)
和AtleastOne(至少一个验证通过)
AllowNullValue
:是否允许空值,bool
类型,默认false
AllowEmptyStrings
:是否允许空字符串,bool
类型,默认false
8.6.5 [ModelBinder]
特性
默认情况下,验证失败信息会根据属性名进行序列化,但是如果属性序列化自定义了 [JsonPropertyName]
特性,那么验证失败的消息就不匹配了,这时我们需要添加 [ModelBinder(Name = "序列化对应名字")]
进行纠正。如下图所示:
[JsonPropertyName("phone_number"), ModelBinder(Name = "phone_number")]
public string PhoneNumber { 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.EmailAddress); // => true
特别注意
自定义的验证类型也要保证名称全局唯一,也就是多个验证类型不能出现一样的名字。
8.8.2 自定义 ValidationTypes
失败消息
Furion
内置的 ValidationTypes
已有默认的失败消息:
Numeric
:The Value is not a numeric type.PositiveNumber
:The Value is not a positive number type.NegativeNumber
:The Value is not a negative number type.Integer
:The Value is not a integer type.Money
:The Value is not a money type.Date
:The Value is not a date type.Time
:The Value is not a time type.IDCard
:The Value is not a idcard type.PostCode
:The Value is not a postcode type.PhoneNumber
:The Value is not a phone number type.Telephone
:The Value is not a telephone type.PhoneOrTelNumber
:The Value is not a phone number or telephone type.EmailAddress
:The Value is not a email address type.Url
:The Value is not a url address type.Color
:The Value is not a color type.Chinese
:The Value is not a chinese type.IPv4
:The Value is not a IPv4 type.IPv6
:The Value is not a IPv6 type.Age
:The Value is not a age type.ChineseName
:The Value is not a chinese name type.EnglishName
:The Value is not a english name type.Capital
:The Value is not a capital type.Lowercase
:The Value is not a lowercase type.Ascii
:The Value is not a ascii type.Md5
:The Value is not a md5 type.Zip
:The Value is not a zip type.Image
:The Value is not a image type.Document
:The Value is not a document type.MP3
:The Value is not a mp3 type.Flash
:The Value is not a flash type.Video
:The Value is not a video type.
我们可以通过创建继承 IValidationMessageTypeProvider
验证消息提供器类型,或通过 appsettings.json
配置。
[ValidationMessageType]
方式
using Furion.DataValidation;
namespace Furion.Application
{
[ValidationMessageType]
public enum MyValidationMessageType
{
[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)
};
}
}
注册验证消息提供器
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
方式
{
"ValidationTypeMessageSettings": {
"Definitions": [
["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 FurionAppService : 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 集成 FluentValidation
第三方校验
Furion
内置的验证已经可以满足绝大多数校验情况,但是对于 场景
验证目前暂未支持。这里推荐集成 FluentValidation
第三方校验组件。
8.12.1 安装 FluentValidation.AspNetCore
拓展包
dotnet add package FluentValidation.AspNetCore
8.12.2 在 Startup.cs
中注册
services.AddControllers()
.AddFluentValidation(fv => {
fv.RegisterValidatorsFromAssemblies(App.Assemblies);
});
8.12.3 使用例子
public class Person {
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public int Age { get; set; }
}
public class PersonValidator : AbstractValidator<Person> {
public PersonValidator() {
RuleFor(x => x.Id).NotNull();
RuleFor(x => x.Name).Length(0, 10);
RuleFor(x => x.Email).EmailAddress();
RuleFor(x => x.Age).InclusiveBetween(18, 60);
}
}
在控制器中使用无需手动调用 ModelState.IsValid
进行判断,Furion
会自动执行该操作。
如需了解更多 FluentValidation
知识可查阅官方文档:https://fluentvalidation.net/
8.13 反馈与建议
与我们交流
给 Furion 提 Issue。