使用 DataAnnotations(数据注解)实现通用模型数据校验
Posted dotNET跨平台
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用 DataAnnotations(数据注解)实现通用模型数据校验相关的知识,希望对你有一定的参考价值。
参数校验的意义
在实际项目开发中,无论任何方式、任何规模的开发模式,项目中都离不开对接入数据模型参数的合法性校验,目前普片的开发模式基本是前后端分离,当用户在前端页面中输入一些表单数据时,点击提交按钮,触发请求目标服务器的一系列后续操作,在这中间的执行过程中(标准做法推荐)无论是前端代码部分,还是服务端代码部分都应该有针对用户输入数据的合法性校验,典型做法如下:
前端部分
:当用户在页面输入表单数据时,前端监听页面表单事件触发相应的数据合法性校验规则,当数据非法时,合理的提示用户数据错误,只有当所有表单数据都校验通过后,才继续提交数据给目标后端对应的接口;后端部分
:当前端数据合法校验通过后,向目标服务器提交表单数据时,服务端接收到相应的提交数据,在入口源头出就应该触发相关的合法性校验规则,当数据都校验通过后,继续执行后续的相关业务逻辑处理,反之则响应相关非法数据的提示信息;
特别说明:在实际的项目中,无论前端部分还是服务端部分,参数的校验都是很有必要性的。无效的参数,可能会导致应用程序的异常和一些不可预知的错误行为。
常用的参数校验项
这里例举一些项目中比较常用的参数模型校验项,如下所示:
Name:姓名校验,比如需要是纯汉字的姓名;
Password:密码强度验证,比如要求用户输入必须包含大小写字母、数字和特殊符号的强密码;
QQ:QQ 号码验证,是否是有效合法的 QQ 号码;
China Postal Code:中国邮政编码;
IP Address:IPV4 或者 IPV6 地址验证;
Phone:手机号码或者座机号码合法性验证;
ID Card:身份证号码验证,比如:15 位和 18 位数身份证号码;
Email Address:邮箱地址的合法性校验;
String:字符串验证,比如字段是否不为 null、长度是否超限;
URL:验证属性是否具有 URL 格式;
Number:数值型参数校验,数值范围校验,比如非负数,非负整数,正整数等;
File:文件路径及扩展名校验;
对于参数校验,常见的方式有正则匹配校验,通过对目标参数编写合法的正则表达式,实现对参数合法性的校验。
.NET 中内置 DataAnnotations 提供的特性校验
上面我们介绍了一些常用的参数验证项,接下来我们来了解下在 .NET
中内置提供的 DataAnnotations
数据注解,该类提供了一些常用的验证参数特性。
官方解释:
提供用于为
ASP.NET MVC
和ASP.NET
数据控件定义元数据的特性类。该类位于
System.ComponentModel.DataAnnotations
命名空间。
关于 DataAnnotations 中的特性介绍
让我们可以通过这些特性对 API
请求中的参数进行验证,常用的特性一般有:
**[ValidateNever]**:指示应从验证中排除属性或参数。
**[CreditCard]**:验证属性是否具有信用卡格式。
**[Compare]**:验证模型中的两个属性是否匹配。
**[EmailAddress]**:验证属性是否具有电子邮件格式。
**[Phone]**:验证属性是否具有电话号码格式。
**[Range]**:验证属性值是否位于指定范围内。
**[RegularExpression]**:验证属性值是否与指定的正则表达式匹配。
**[Required]**:验证字段是否不为 null。
**[StringLength]**:验证字符串属性值是否不超过指定的长度限制。
**[Url]**:验证属性是否具有 URL 格式。
其中 RegularExpression
特性,基于正则表达式可以扩展实现很多常用的验证类型,下面的( 基于 DataAnnotations 的通用模型校验封装
)环节举例说明。
关于该类更多详细信息请查看,
https://learn.microsoft.com/zh-cn/dotnet/api/system.componentmodel.dataannotations?view=net-7.0
基于 DataAnnotations 的通用模型校验封装
此处主要是使用了 Validator.TryValidateObject()
方法:
Validator.TryValidateObject(object instance, ValidationContext validationContext, ICollection<ValidationResult>? validationResults, bool validateAllProperties);
Validator
类提供如下校验方法:
基于 DataAnnotations 的特性校验助手实现步骤
错误成员对象类
ErrorMember
namespace Jeff.Common.Validatetion;
/// <summary>
/// 错误成员对象
/// </summary>
public class ErrorMember
/// <summary>
/// 错误信息
/// </summary>
public string? ErrorMessage get; set;
/// <summary>
/// 错误成员名称
/// </summary>
public string? ErrorMemberName get; set;
验证结果类
ValidResult
namespace Jeff.Common.Validatetion;
/// <summary>
/// 验证结果类
/// </summary>
public class ValidResult
public ValidResult()
ErrorMembers = new List<ErrorMember>();
/// <summary>
/// 错误成员列表
/// </summary>
public List<ErrorMember> ErrorMembers get; set;
/// <summary>
/// 验证结果
/// </summary>
public bool IsVaild get; set;
定义操作正则表达式的公共类
RegexHelper
(基于RegularExpression
特性扩展)
using System;
using System.Net;
using System.Text.RegularExpressions;
namespace Jeff.Common.Validatetion;
/// <summary>
/// 操作正则表达式的公共类
/// Regex 用法参考:https://learn.microsoft.com/zh-cn/dotnet/api/system.text.regularexpressions.regex.-ctor?redirectedfrom=MSDN&view=net-7.0
/// </summary>
public class RegexHelper
#region 常用正则验证模式字符串
public enum ValidateType
Email, // 邮箱
TelePhoneNumber, // 固定电话(座机)
MobilePhoneNumber, // 移动电话
Age, // 年龄(1-120 之间有效)
Birthday, // 出生日期
Timespan, // 时间戳
IdentityCardNumber, // 身份证
IpV4, // IPv4 地址
IpV6, // IPV6 地址
Domain, // 域名
English, // 英文字母
Chinese, // 汉字
MacAddress, // MAC 地址
Url, // URL
private static readonly Dictionary<ValidateType, string> keyValuePairs = new Dictionary<ValidateType, string>
ValidateType.Email, _Email ,
ValidateType.TelePhoneNumber,_TelephoneNumber ,
ValidateType.MobilePhoneNumber,_MobilePhoneNumber ,
ValidateType.Age,_Age ,
ValidateType.Birthday,_Birthday ,
ValidateType.Timespan,_Timespan ,
ValidateType.IdentityCardNumber,_IdentityCardNumber ,
ValidateType.IpV4,_IpV4 ,
ValidateType.IpV6,_IpV6 ,
ValidateType.Domain,_Domain ,
ValidateType.English,_English ,
ValidateType.Chinese,_Chinese ,
ValidateType.MacAddress,_MacAddress ,
ValidateType.Url,_Url ,
;
public const string _Email = @"^(\\w)+(\\.\\w)*@(\\w)+((\\.\\w+)+)$"; // ^[\\w-]+(\\.[\\w-]+)*@[\\w-]+(\\.[\\w-]+)+$ , [A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]2,4
public const string _TelephoneNumber = @"(d+-)?(d4-?d7|d3-?d8|^d7,8)(-d+)?"; //座机号码(中国大陆)
public const string _MobilePhoneNumber = @"^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\\d8$"; //移动电话
public const string _Age = @"^(?:[1-9][0-9]?|1[01][0-9]|120)$"; // 年龄 1-120 之间有效
public const string _Birthday = @"^((?:19[2-9]\\d1)|(?:20(?:(?:0[0-9])|(?:1[0-8]))))((?:0?[1-9])|(?:1[0-2]))((?:0?[1-9])|(?:[1-2][0-9])|30|31)$";
public const string _Timespan = @"^15|16|17\\d8,11$"; // 目前时间戳是15开头,以后16、17等开头,长度 10 位是秒级时间戳的正则,13 位时间戳是到毫秒级的。
public const string _IdentityCardNumber = @"^[1-9]\\d7((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d3$|^[1-9]\\d5[1-9]\\d3((0\\d)|(1[0-2]))(([0|1|2]\\d)|3[0-1])\\d3([0-9]|X)$";
public const string _IpV4 = @"^((2(5[0-5]|[0-4]\\d))|[0-1]?\\d1,2)(\\.((2(5[0-5]|[0-4]\\d))|[0-1]?\\d1,2))3$";
public const string _IpV6 = @"^\\s*((([0-9A-Fa-f]1,4:)7([0-9A-Fa-f]1,4|:))|(([0-9A-Fa-f]1,4:)6(:[0-9A-Fa-f]1,4|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d))3)|:))|(([0-9A-Fa-f]1,4:)5(((:[0-9A-Fa-f]1,4)1,2)|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d))3)|:))|(([0-9A-Fa-f]1,4:)4(((:[0-9A-Fa-f]1,4)1,3)|((:[0-9A-Fa-f]1,4)?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d))3))|:))|(([0-9A-Fa-f]1,4:)3(((:[0-9A-Fa-f]1,4)1,4)|((:[0-9A-Fa-f]1,4)0,2:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d))3))|:))|(([0-9A-Fa-f]1,4:)2(((:[0-9A-Fa-f]1,4)1,5)|((:[0-9A-Fa-f]1,4)0,3:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d))3))|:))|(([0-9A-Fa-f]1,4:)1(((:[0-9A-Fa-f]1,4)1,6)|((:[0-9A-Fa-f]1,4)0,4:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d))3))|:))|(:(((:[0-9A-Fa-f]1,4)1,7)|((:[0-9A-Fa-f]1,4)0,5:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d))3))|:)))(%.+)?\\s*$";
public const string _Domain = @"^[a-zA-Z0-9][-a-zA-Z0-9]0,62(\\.[a-zA-Z0-9][-a-zA-Z0-9]0,62)+\\.?$";
public const string _English = @"^[A-Za-z]+$";
public const string _Chinese = @"^[\\u4e00-\\u9fa5]0,$";
public const string _MacAddress = @"^([0-9A-F]2)(-[0-9A-F]2)5$";
public const string _Url = @"^[a-zA-z]+://(\\w+(-\\w+)*)(\\.(\\w+(-\\w+)*))*(\\?\\S*)?$";
#endregion
/// <summary>
/// 获取验证模式字符串
/// </summary>
/// <param name="validateType"></param>
/// <returns></returns>
public static (bool hasPattern, string pattern) GetValidatePattern(ValidateType validateType)
bool hasPattern = keyValuePairs.TryGetValue(validateType, out string? pattern);
return (hasPattern, pattern ?? string.Empty);
#region 验证输入字符串是否与模式字符串匹配
/// <summary>
/// 验证输入字符串是否与模式字符串匹配
/// </summary>
/// <param name="input">输入的字符串</param>
/// <param name="validateType">模式字符串类型</param>
/// <param name="matchTimeout">超时间隔</param>
/// <param name="options">筛选条件</param>
/// <returns></returns>
public static (bool isMatch, string info) IsMatch(string input, ValidateType validateType, TimeSpan matchTimeout, RegexOptions options = RegexOptions.None)
var (hasPattern, pattern) = GetValidatePattern(validateType);
if (hasPattern && !string.IsNullOrWhiteSpace(pattern))
bool isMatch = IsMatch(input, pattern, matchTimeout, options);
if (isMatch) return (true, "Format validation passed."); // 格式验证通过。
else return (false, "Format validation failed."); // 格式验证未通过。
return (false, "Unknown ValidatePattern."); // 未知验证模式
/// <summary>
/// 验证输入字符串是否与模式字符串匹配,匹配返回true
/// </summary>
/// <param name="input">输入字符串</param>
/// <param name="pattern">模式字符串</param>
/// <returns></returns>
public static bool IsMatch(string input, string pattern)
return IsMatch(input, pattern, TimeSpan.Zero, RegexOptions.IgnoreCase);
/// <summary>
/// 验证输入字符串是否与模式字符串匹配,匹配返回true
/// </summary>
/// <param name="input">输入的字符串</param>
/// <param name="pattern">模式字符串</param>
/// <param name="matchTimeout">超时间隔</param>
/// <param name="options">筛选条件</param>
/// <returns></returns>
public static bool IsMatch(string input, string pattern, TimeSpan matchTimeout, RegexOptions options = RegexOptions.None)
return Regex.IsMatch(input, pattern, options, matchTimeout);
#endregion
定义验证结果统一模型格式类
ResponseInfo
(此类通常也是通用的数据响应模型类)
namespace Jeff.Common.Model;
public sealed class ResponseInfo<T> where T : class
/*
Microsoft.AspNetCore.Http.StatusCodes
System.Net.HttpStatusCode
*/
/// <summary>
/// 响应代码(自定义)
/// </summary>
public int Code get; set;
/// <summary>
/// 接口状态
/// </summary>
public bool Success get; set;
#region 此处可以考虑多语言国际化设计(语言提示代号对照表)
/// <summary>
/// 语言对照码,参考:https://blog.csdn.net/shenenhua/article/details/79150053
/// </summary>
public string Lang get; set; = "zh-cn";
/// <summary>
/// 提示信息
/// </summary>
public string Message get; set; = string.Empty;
#endregion
/// <summary>
/// 数据体
/// </summary>
public T? Data get; set;
实现验证助手类
ValidatetionHelper
,配合System.ComponentModel.DataAnnotations
类使用
// 数据注解,https://learn.microsoft.com/zh-cn/dotnet/api/system.componentmodel.dataannotations?view=net-7.0
using System.ComponentModel.DataAnnotations;
using Jeff.Common.Model;
namespace Jeff.Common.Validatetion;
/// <summary>
/// 验证助手类
/// </summary>
public sealed class ValidatetionHelper
/// <summary>
/// DTO 模型校验
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static ValidResult IsValid(object value)
var result = new ValidResult();
try
var validationContext = new ValidationContext(value);
var results = new List<ValidationResult>();
bool isValid = Validator.TryValidateObject(value, validationContext, results, true);
result.IsVaild = isValid;
if (!isValid)
foreach (ValidationResult? item in results)
result.ErrorMembers.Add(new ErrorMember()
ErrorMessage = item.ErrorMessage,
ErrorMemberName = item.MemberNames.FirstOrDefault()
);
catch (ValidationException ex)
result.IsVaild = false;
result.ErrorMembers = new List<ErrorMember>
new ErrorMember()
ErrorMessage = ex.Message,
ErrorMemberName = "Internal error"
;
return result;
/// <summary>
/// DTO 模型校验统一响应信息
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="model"></param>
/// <returns></returns>
public static ResponseInfo<ValidResult> GetValidInfo<T>(T model) where T : class
var result = new ResponseInfo<ValidResult>();
var validResult = IsValid(model);
if (!validResult.IsVaild)
result.Code = 420;
result.Message = "DTO 模型参数值异常";
result.Success = false;
result.Data = validResult;
else
result.Code = 200;
result.Success = true;
result.Message = "DTO 模型参数值合法";
return result;
如何使用 DataAnnotations 封装的特性校验助手?
首先定义一个数据模型类(
DTO
),添加校验特性ValidationAttribute
using System.ComponentModel.DataAnnotations;
using Jeff.Common.Validatetion;
namespace Jeff.Comm.Test;
public class Person
[Display(Name = "姓名"), Required(ErrorMessage = "0必须填写")]
public string Name get; set;
[Display(Name = "邮箱")]
[Required(ErrorMessage = "0必须填写")]
[RegularExpression(RegexHelper._Email, ErrorMessage = "RegularExpression: 0格式非法")]
[EmailAddress(ErrorMessage = "EmailAddress: 0格式非法")]
public string Email get; set;
[Display(Name = "Age年龄")]
[Required(ErrorMessage = "0必须填写")]
[Range(1, 120, ErrorMessage = "超出范围")]
[RegularExpression(RegexHelper._Age, ErrorMessage = "0超出合理范围")]
public int Age get; set;
[Display(Name = "Birthday出生日期")]
[Required(ErrorMessage = "0必须填写")]
[RegularExpression(RegexHelper._Timespan, ErrorMessage = "0超出合理范围")]
public TimeSpan Birthday get; set;
[Display(Name = "Address住址")]
[Required(ErrorMessage = "0必须填写")]
[StringLength(200, MinimumLength = 10, ErrorMessage = "0输入长度不正确")]
public string Address get; set;
[Display(Name = "Mobile手机号码")]
[Required(ErrorMessage = "0必须填写")]
[RegularExpression(RegexHelper._MobilePhoneNumber, ErrorMessage = "0格式非法")]
public string Mobile get; set;
[Display(Name = "Salary薪水")]
[Required(ErrorMessage = "0必须填写")]
[Range(typeof(decimal), "1000.00", "3000.99")]
public decimal Salary get; set;
[Display(Name = "MyUrl连接")]
[Required(ErrorMessage = "0必须填写")]
[Url(ErrorMessage = "Url:0格式非法")]
[RegularExpression(RegexHelper._Url, ErrorMessage = "RegularExpression:0格式非法")]
public string MyUrl get; set;
控制台调用通用校验助手验证方法
ValidatetionHelper.IsValid()
或ValidatetionHelper.GetValidInfo()
// 通用模型数据验证测试
static void ValidatetionTest()
var p = new Person
Name = "",
Age = -10,
Email = "www.baidu.com",
MobilePhoneNumber = "12345",
Salary = 4000,
MyUrl = "aaa"
;
// 调用通用模型校验
var result = ValidatetionHelper.IsValid(p);
if (!result.IsVaild)
foreach (ErrorMember errorMember in result.ErrorMembers)
// 控制台打印字段验证信息
Console.WriteLine($"errorMember.ErrorMemberName:errorMember.ErrorMessage");
Console.WriteLine();
// 调用通用模型校验,返回统一数据格式
var validInfo = ValidatetionHelper.GetValidInfo(p);
var options = new JsonSerializerOptions
Encoder = javascriptEncoder.UnsafeRelaxedJsonEscaping, // 设置中文编码乱码
WriteIndented = false
;
string jsonStr = JsonSerializer.Serialize(validInfo, options);
Console.WriteLine($"校验结果返回统一数据格式:jsonStr");
在控制台Program.Main
方法中调用 ValidatetionTest()
方法:
internal class Program
static void Main(string[] args)
Console.WriteLine("Hello, DataAnnotations!");
#region 数据注解(DataAnnotations)模型验证
ValidatetionTest();
#endregion
Console.ReadKey();
启动控制台,输出如下信息:
如何实现自定义的验证特性?
当我们碰到这些参数需要验证的时候,而上面内置类提供的特性又不能满足需求时,此时我们可以实现自定义的验证特性来满足校验需求,按照微软给出的编码规则,我们只需继承 ValidationAttribute
类,并重写 IsValid()
方法即可。
自定义校验特性案例
比如实现一个密码强度的验证,实现步骤如下:
定义密码强度规则,只包含英文字母、数字和特殊字符的组合,并且组合长度至少 8 位数;
/// <summary>
/// 只包含英文字母、数字和特殊字符的组合
/// </summary>
/// <returns></returns>
public static bool IsCombinationOfEnglishNumberSymbol(string input, int? minLength = null, int? maxLength = null)
var pattern = @"(?=.*\\d)(?=.*[a-zA-Z])(?=.*[^a-zA-Z\\d]).";
if (minLength is null && maxLength is null)
pattern = $@"^pattern+$";
else if (minLength is not null && maxLength is null)
pattern = $@"^patternminLength,$";
else if (minLength is null && maxLength is not null)
pattern = $@"^pattern1,maxLength$";
else
pattern = $@"^patternminLength,maxLength$";
return Regex.IsMatch(input, pattern);
实现自定义特性
EnglishNumberSymbolCombinationAttribute
,继承自ValidationAttribute
;
using System.ComponentModel.DataAnnotations;
namespace Jeff.Common.Validatetion.CustomAttributes;
/// <summary>
/// 是否是英文字母、数字和特殊字符的组合
/// </summary>
public class EnglishNumberSymbolCombinationAttribute : ValidationAttribute
/// <summary>
/// 默认的错误提示信息
/// </summary>
private const string error = "无效的英文字母、数字和特殊字符的组合";
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
if (value is null) return new ValidationResult("参数值为 null");
//if (value is null)
//
// throw new ArgumentNullException(nameof(attribute));
//
// 验证参数逻辑 value 是需要验证的值,而 validationContext 中包含了验证相关的上下文信息,这里可自己封装一个验证格式的 FormatValidation 类
if (FormatValidation.IsCombinationOfEnglishNumberSymbol(value as string, 8))
//验证成功返回 success
return ValidationResult.Success;
//不成功 提示验证错误的信息
else return new ValidationResult(ErrorMessage ?? error);
以上就实现了一个自定义规则的 自定义验证特性
,使用方式很简单,可以把它附属在我们 请求的参数
上或者 DTO 里的属性
,也可以是 Action 上的形参
,如下所示:
public class CreateDTO
[Required]
public string StoreName get; init;
[Required]
// 附属在 DTO 里的属性
[EnglishNumberSymbolCombination(ErrorMessage = "UserId 必须是英文字母、数字和特殊符号的组合")]
public string UserId get; init;
...
// 附属在 Action 上的形参
[HttpGet]
public async ValueTask<ActionResult> Delete([EnglishNumberSymbolCombination]string userId, string storeName)
该自定义验证特性还可以结合 DataAnnotations
内置的 [Compare]
特性,可以实现账号注册的密码确认验证(输入密码和确认密码是否一致性
)。关于更多自定义参数校验特性,感兴趣的小伙伴可参照上面案例的实现思路,自行扩展实现哟。
总结
对于模型参数的校验,在实际项目系统中是非常有必要性的(通常在数据源头提供验证),利用 .NET
内置的 DataAnnotations
(数据注解)提供的特性校验,可以很方便的实现通用的模型校验助手,关于其他特性的用法,请自行参考微软官方文档,这里注意下RegularExpressionAttribute
(指定 ASP.NET
动态数据中的数据字段值必须与指定的正则表达式匹配),该特性可以方便的接入正则匹配验证,当遇到复杂的参数校验时,可以快速方便的扩展自定义校验特性,从此告别传统编码中各种 if(xxx != yyyy)
判断的验证,让整体代码编写更佳简练干净。
以上是关于使用 DataAnnotations(数据注解)实现通用模型数据校验的主要内容,如果未能解决你的问题,请参考以下文章