微軟在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 }
最后附上自己寫的驗證類,都是一些常用的驗證:
1 /// <summary> 2 /// 格式驗證 3 /// </summary> 4 public static class FormatValidation 5 { 6 private readonly static Regex IPV4Regex = new(@"^((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}$", RegexOptions.Compiled); 7 private readonly static Regex IPV6Regex = new(@"^\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*$", RegexOptions.Compiled); 8 private readonly static Regex DomainRegex = new(@"^[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?$", RegexOptions.Compiled); 9 private readonly static Regex UrlRegex = new(@"^[a-zA-z]+://[^\s]*$", RegexOptions.Compiled); 10 private readonly static Regex PhoneNumberRegex = new(@"^(13[0-9]|14[5|7]|15[0|1|2|3|4|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$", RegexOptions.Compiled); 11 private readonly static Regex EnglishRegex = new(@"^[A-Za-z]+$", RegexOptions.Compiled); 12 private readonly static Regex IdentityNumberRegex = new(@"(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)", RegexOptions.Compiled); 13 private readonly static Regex EmailRegex = new(@"^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$", RegexOptions.Compiled); 14 private readonly static Regex ChineseRegex = new(@"^[\u4e00-\u9fa5]{0,}$", RegexOptions.Compiled); 15 private readonly static Regex LandlineRegex = new(@"^\d{3}-\d{8}|\d{4}-\d{7}|\d{7}$", RegexOptions.Compiled); 16 17 /// <summary> 18 /// 是否是IPV4格式的IP 19 /// </summary> 20 /// <returns></returns> 21 public static bool IsIPV4(string input) 22 { 23 return IPV4Regex.IsMatch(input); 24 } 25 26 /// <summary> 27 /// 是否是IPV6格式的IP 28 /// </summary> 29 /// <returns></returns> 30 public static bool IsIPV6(string input) 31 { 32 return IPV6Regex.IsMatch(input); 33 } 34 35 /// <summary> 36 /// 是否是一個域名 37 /// </summary> 38 /// <returns></returns> 39 public static bool IsDomain(string input) 40 { 41 return DomainRegex.IsMatch(input); 42 } 43 44 /// <summary> 45 /// 是否是一個網址 46 /// </summary> 47 /// <returns></returns> 48 public static bool IsUrl(string input) 49 { 50 return UrlRegex.IsMatch(input); 51 } 52 53 /// <summary> 54 /// 是否是一個手機號碼(中國大陸) 55 /// </summary> 56 /// <returns></returns> 57 public static bool IsPhoneNumber(string input) 58 { 59 return PhoneNumberRegex.IsMatch(input); 60 } 61 62 /// <summary> 63 /// 是否是純英文字母 64 /// </summary> 65 /// <returns></returns> 66 public static bool IsEnglish(string input) 67 { 68 return EnglishRegex.IsMatch(input); 69 } 70 71 /// <summary> 72 /// 只包含英文字母和數字的組合 73 /// </summary> 74 /// <returns></returns> 75 public static bool IsCombinationOfEnglishNumber(string input, int? minLength = null, int? maxLength = null) 76 { 77 var pattern = @"(?=.*\d)(?=.*[a-zA-Z])[a-zA-Z0-9]"; 78 if (minLength is null && maxLength is null) 79 pattern = $@"^{pattern}+$"; 80 else if (minLength is not null && maxLength is null) 81 pattern = $@"^{pattern}{{{minLength},}}$"; 82 else if (minLength is null && maxLength is not null) 83 pattern = $@"^{pattern}{{1,{maxLength}}}$"; 84 else 85 pattern = $@"^{pattern}{{{minLength},{maxLength}}}$"; 86 return Regex.IsMatch(input, pattern); 87 } 88 89 /// <summary> 90 /// 只包含英文字母、數字和特殊字符的組合 91 /// </summary> 92 /// <returns></returns> 93 public static bool IsCombinationOfEnglishNumberSymbol(string input, int? minLength = null, int? maxLength = null) 94 { 95 var pattern = @"(?=.*\d)(?=.*[a-zA-Z])(?=.*[^a-zA-Z\d])."; 96 if (minLength is null && maxLength is null) 97 pattern = $@"^{pattern}+$"; 98 else if (minLength is not null && maxLength is null) 99 pattern = $@"^{pattern}{{{minLength},}}$"; 100 else if (minLength is null && maxLength is not null) 101 pattern = $@"^{pattern}{{1,{maxLength}}}$"; 102 else 103 pattern = $@"^{pattern}{{{minLength},{maxLength}}}$"; 104 return Regex.IsMatch(input, pattern); 105 } 106 107 /// <summary> 108 /// 是否是身份證號碼(中國大陸) 109 /// </summary> 110 /// <returns></returns> 111 public static bool IsIdentityNumber(string input) 112 { 113 return IdentityNumberRegex.IsMatch(input); 114 } 115 116 /// <summary> 117 /// 是否是電子郵箱 118 /// </summary> 119 /// <returns></returns> 120 public static bool IsEmail(string input) 121 { 122 return EmailRegex.IsMatch(input); 123 } 124 125 /// <summary> 126 /// 是否是漢字 127 /// </summary> 128 /// <returns></returns> 129 public static bool IsChinese(string input) 130 { 131 return ChineseRegex.IsMatch(input); 132 } 133 134 /// <summary> 135 /// 是否是座機號碼(中國大陸) 136 /// </summary> 137 /// <returns></returns> 138 public static bool IsLandline(string input) 139 { 140 return LandlineRegex.IsMatch(input); 141 } 142 }
每天了解多一點,日積月累,基礎就會慢慢牢固。