使用 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, ICollectionValidationResult>
? 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 ListErrorMember>
();
}
/// summary>
/// 错误成员列表
/// /summary>
public ListErrorMember>
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 DictionaryValidateType, string>
keyValuePairs = new DictionaryValidateType, 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+-)?(d{
4}
-?d{
7}
|d{
3}
-?d{
8}
|^d{
7,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])\d{
8}
$";
//移动电话
public const string _Age = @"^(?:[1-9][0-9]?|1[01][0-9]|120)$";
// 年龄 1-120 之间有效
public const string _Birthday = @"^((?:19[2-9]\d{
1}
)|(?: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\d{
8,11}
$";
// 目前时间戳是15开头,以后16、17等开头,长度 10 位是秒级时间戳的正则,13 位时间戳是到毫秒级的。
public const string _IdentityCardNumber = @"^[1-9]\d{
7}
((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{
3}
$|^[1-9]\d{
5}
[1-9]\d{
3}
((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{
3}
([0-9]|X)$";
public const string _IpV4 = @"^((2(5[0-5]|[0-4]\d))|[0-1]?\d{
1,2}
)(\.((2(5[0-5]|[0-4]\d))|[0-1]?\d{
1,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 ResponseInfoT>
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 ListValidationResult>
();
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 ListErrorMember>
{
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 ResponseInfoValidResult>
GetValidInfoT>
(T model) where T : class
{
var result = new ResponseInfoValidResult>
();
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, World!");
{
#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 = $@"^{
pattern}
{
{
{
minLength}
,}
}
$";
else if (minLength is null &
&
maxLength is not null)
pattern = $@"^{
pattern}
{
{
1,{
maxLength}
}
}
$";
else
pattern = $@"^{
pattern}
{
{
{
minLength}
,{
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 ValueTaskActionResult>
Delete([EnglishNumberSymbolCombination]string userId, string storeName)
该自定义验证特性还可以结合 DataAnnotations
内置的 [Compare]
特性,可以实现账号注册的密码确认验证(输入密码和确认密码是否一致性
)。关于更多自定义参数校验特性,感兴趣的小伙伴可参照上面案例的实现思路,自行扩展实现哟。
总结
对于模型参数的校验,在实际项目系统中是非常有必要性的(通常在数据源头提供验证),利用 .NET
内置的 DataAnnotations
(数据注解)提供的特性校验,可以很方便的实现通用的模型校验助手,关于其他特性的用法,请自行参考微软官方文档,这里注意下RegularExpressionAttribute
(指定 ASP.NET
动态数据中的数据字段值必须与指定的正则表达式匹配),该特性可以方便的接入正则匹配验证,当遇到复杂的参数校验时,可以快速方便的扩展自定义校验特性,从此告别传统编码中各种 if(xxx != yyyy)
判断的验证,让整体代码编写更佳简练干净。
声明:本文内容由网友自发贡献,本站不承担相应法律责任。对本内容有异议或投诉,请联系2913721942#qq.com核实处理,我们将尽快回复您,谢谢合作!
若转载请注明出处: 使用 DataAnnotations(数据注解)实现模型的通用数据校验
本文地址: https://pptw.com/jishu/954.html