微软在ASP.NET Core框架中内置了一些验证参数的特性,让我们可以通过这些特性对API请求中的参数进行验证,常用的特性一般有:
-
[ValidateNever]: ValidateNeverAttribute 指示应从验证中排除属性或参数。 -
[CreditCard]:验证属性是否具有信用卡格式。 -
[Compare]:验证模型中的两个属性是否匹配。 -
[EmailAddress]:验证属性是否具有电子邮件格式。 -
[Phone]:验证属性是否具有电话号码格式。 -
[Range]:验证属性值是否位于指定范围内。 -
[RegularExpression]:验证 属性值是否与指定的正则表达式匹配。 -
[Required]:验证字段是否不为 null。 -
[StringLength]:验证字符串属性值是否不超过指定的长度限制。 -
[Url]:验证属性是否具有 URL 格式。
但除了上面这些,还缺少一些我们平时在项目中会经常碰到的验证,例如:需要是纯汉字的姓名、必须包含大小写字母和数字的强密码、QQ号、IPV4或者IPV6地址,以及中国的手机号码和身份证号码等等。
当我们碰到这些参数需要验证的时候,我们需要如何实现自定义的验证特性呢?此时微软已经指出,让我们去继承ValidationAttribute类,并重写IsValid()即可。
1 /// <summary> 2 /// 是否是英文字母、数字组合 3 /// </summary> 4 public class EnglishNumberCombinationAttribute : ValidationAttribute 5 { 6 /// <summary> 7 /// 默认的错误提示信息 8 /// </summary> 9 private const string error = "无效的英文字母加数字组合"; 10 11 protected override ValidationResult IsValid(object value, ValidationContext validationContext) 12 { 13 //这里是验证的参数的逻辑 value是需要验证的值 而validationContext中包含了验证相关的上下文信息 这里我是有一个自己封装的验证格式的FormatValidation类 14 if (FormatValidation.IsCombinationOfEnglishNumber(value as string)) 15 //验证成功返回 success 16 return ValidationResult.Success; 17 //不成功 提示验证错误的信息 18 else return new ValidationResult(ErrorMessage ?? error); 19 } 20 }
这里是实现一个英文字母数字组合的验证特性,这样我们就可以把它附在在我们请求的参数上,可以是DTO里的属性,也可以是Action上的形参。
1 public class CreateDTO 2 { 3 [Required] 4 public string StoreName { get; init; } 5 [Required] 6 [EnglishNumberCombination(ErrorMessage = "UserId必须是英文字母加数字的组合")] 7 public string UserId { get; init; } 8 } 9 10 ...
11
12 [HttpGet] 13 public async ValueTask<ActionResult> Delete([EnglishNumberCombination]string UserId, string StoreName)
Postman测试结果:
至于验证的过程,我看了下源码,具体的过程是当我们在startup中services.AddControllers()或者services.AddMvc()的时候,有一个默认的MvcOptions(这个我们是可以配置的),其中有一个ModelValidatorProviders属性,看名字就知道模型验证提供器。ASP.NET Core实现了默认的提供器:
options.ModelValidatorProviders.Add(new DataAnnotationsModelValidatorProvider( _validationAttributeAdapterProvider, _dataAnnotationLocalizationOptions, _stringLocalizerFactory));
其中_validationAttributeAdapterProvider,是已经依赖注入的IValidationAttributeAdapterProvider,下面是微软实现的代码,感兴趣的小伙伴可以去看一下,可以学到很多设计模式的运用:
1 namespace Microsoft.AspNetCore.Mvc.DataAnnotations 2 { 3 /// <summary> 4 /// Creates an <see cref="IAttributeAdapter"/> for the given attribute. 5 /// </summary> 6 public class ValidationAttributeAdapterProvider : IValidationAttributeAdapterProvider 7 { 8 /// <summary> 9 /// Creates an <see cref="IAttributeAdapter"/> for the given attribute. 10 /// </summary> 11 /// <param name="attribute">The attribute to create an adapter for.</param> 12 /// <param name="stringLocalizer">The localizer to provide to the adapter.</param> 13 /// <returns>An <see cref="IAttributeAdapter"/> for the given attribute.</returns> 14 public IAttributeAdapter? GetAttributeAdapter(ValidationAttribute attribute, IStringLocalizer? stringLocalizer) 15 { 16 if (attribute == null) 17 { 18 throw new ArgumentNullException(nameof(attribute)); 19 } 20 21 var type = attribute.GetType(); 22 23 if (typeof(RegularExpressionAttribute).IsAssignableFrom(type)) 24 { 25 return new RegularExpressionAttributeAdapter((RegularExpressionAttribute)attribute, stringLocalizer); 26 } 27 else if (typeof(MaxLengthAttribute).IsAssignableFrom(type)) 28 { 29 return new MaxLengthAttributeAdapter((MaxLengthAttribute)attribute, stringLocalizer); 30 } 31 else if (typeof(RequiredAttribute).IsAssignableFrom(type)) 32 { 33 return new RequiredAttributeAdapter((RequiredAttribute)attribute, stringLocalizer); 34 } 35 else if (typeof(CompareAttribute).IsAssignableFrom(type)) 36 { 37 return new CompareAttributeAdapter((CompareAttribute)attribute, stringLocalizer); 38 } 39 else if (typeof(MinLengthAttribute).IsAssignableFrom(type)) 40 { 41 return new MinLengthAttributeAdapter((MinLengthAttribute)attribute, stringLocalizer); 42 } 43 else if (typeof(CreditCardAttribute).IsAssignableFrom(type)) 44 { 45 return new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "data-val-creditcard", stringLocalizer); 46 } 47 else if (typeof(StringLengthAttribute).IsAssignableFrom(type)) 48 { 49 return new StringLengthAttributeAdapter((StringLengthAttribute)attribute, stringLocalizer); 50 } 51 else if (typeof(RangeAttribute).IsAssignableFrom(type)) 52 { 53 return new RangeAttributeAdapter((RangeAttribute)attribute, stringLocalizer); 54 } 55 else if (typeof(EmailAddressAttribute).IsAssignableFrom(type)) 56 { 57 return new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "data-val-email", stringLocalizer); 58 } 59 else if (typeof(PhoneAttribute).IsAssignableFrom(type)) 60 { 61 return new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "data-val-phone", stringLocalizer); 62 } 63 else if (typeof(UrlAttribute).IsAssignableFrom(type)) 64 { 65 return new DataTypeAttributeAdapter((DataTypeAttribute)attribute, "data-val-url", stringLocalizer); 66 } 67 else if (typeof(FileExtensionsAttribute).IsAssignableFrom(type)) 68 { 69 return new FileExtensionsAttributeAdapter((FileExtensionsAttribute)attribute, stringLocalizer); 70 } 71 else 72 { 73 return null; 74 } 75 } 76 }; 77 }