ASP.NET Core通过特性实现参数验证

  微软在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         public EnglishNumberCombinationAttribute()
12         {
13         }
14 
15         /// <param name="errorMessage">自定义的错误信息</param>
16         public EnglishNumberCombinationAttribute(string errorMessage) : base(errorMessage)
17         {
18         }
19 
20         protected override ValidationResult IsValid(object value, ValidationContext validationContext)
21         {
22             //这里是验证的参数的逻辑 value是需要验证的值  而validationContext中包含了验证相关的上下文信息 这里我是有一个自己封装的验证格式的FormatValidation类
23             if (FormatValidation.IsCombinationOfEnglishNumber(value as string))
24                 //验证成功返回 success
25                 return ValidationResult.Success;
26             //不成功 提示验证错误的信息 这里注意不要用ErrorMessage,要用ErrorMessageString
27             else return new ValidationResult(ErrorMessageString ?? error);
28         }
29     }

  这里是实现一个英文字母数字组合的验证特性,这样我们就可以把它附在在我们请求的参数上,可以是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测试结果:

ASP.NET Core通过特性实现参数验证

   至于验证的过程,我看了下源码,具体的过程是当我们在startup中services.AddControllers()或者services.AddMvc()的时候,有一个默认的MvcOptions(这个我们是可以配置的),其中有一个ModelValidatorProviders属性,看名字就知道模型验证提供器。ASP.NET Core实现了默认的提供器:

options.ModelValidatorProviders.Add(new DataAnnotationsModelValidatorProvider(
                _validationAttributeAdapterProvider,
                _dataAnnotationLocalizationOptions,
                _stringLocalizerFactory));

  其中_validationAttributeAdapterProvider,是已经依赖注入的IValidationAttributeAdapterProvider,下面是微软实现的代码,感兴趣的小伙伴可以去看一下,可以学到很多设计模式的运用:

ASP.NET Core通过特性实现参数验证
 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     }

  每天了解多一点,日积月累,基础就会慢慢牢固。

  author:https://www.cnblogs.com/abnerwong/

上一篇:pyCharm 当中使用VirtualEnv


下一篇:ELKF日志学习(十)Logstash解析日志成json