上一章介绍了什么是特性以及.net框架内的三种预定义特性,下面来看下如何自定义特性:
自定义特性
.Net 框架允许创建自定义特性,用于存储声明性的信息,且可在运行时被检索。该信息根据设计标准和应用程序需要,可与任何目标元素相关。
创建并使用自定义特性包含四个步骤:
- 声明自定义特性
- 构建自定义特性
- 在目标程序元素上应用自定义特性
- 通过反射访问特性
最后一个步骤包含编写一个简单的程序来读取元数据以便查找各种符号。元数据是用于描述其他数据的数据和信息。该程序应使用反射来在运行时访问特性。
声明自定义特性
一个新的自定义特性应派生自 System.Attribute 类。例如:
/// <summary>/// 自定义日志打印/// </summary>[AttributeUsage(AttributeTargets.Method)]public class PrintLogAttribute: Attribute{private string _userName;private string _msg;public PrintLogAttribute(string userNaame, string msg){this._userName = userNaame;this._msg = msg;Console.WriteLine($"{userNaame}于【{DateTime.Now.ToString("yyyy-MM-dd")}】{msg}");}public string GetMsg(){return $"{this._userName}于【{DateTime.Now.ToString("yyyy-MM-dd")}】{this._msg}";}}public class PrintLogTest{[PrintLog("张三","学习Attribute")]public void Study(){Console.WriteLine("张三在学习....");}[PrintLog("张三", "SayHello")]public string SayHello(){return "hello";}}class Program{static void Main(string[] args){PrintLogTest test = new PrintLogTest();test.SayHello();test.Study();Console.ReadKey();}}
执行Main方法,仅仅打印了特性里面的信息并没有被打印出来,这是为啥?想要获取标记的内容就需要用到反射,获取方法如下:
class Program{static void Main(string[] args){PrintLogTest test=new PrintLogTest();test.Study();Type type = test.GetType();var methods = type.GetMethods();//获取所有公开方法foreach (MemberInfo item in methods){if (item.IsDefined(typeof(PrintLogAttribute), true))//判断该方法是否被PrintLogAttribute标记{PrintLogAttribute attribute = item.GetCustomAttribute(typeof(PrintLogAttribute)) as PrintLogAttribute;//实例化PrintLogAttributevar msg = attribute.GetMsg();Console.WriteLine($"得到标记信息:{msg}");}}Console.ReadKey();}}
执行Main方法,执行如下:
从执行结果发现,我们拿到了我们想要信息。那么在实际过程中有哪些用途呢?接下来就进入文章主题。
Attribute特性实际妙用?
在实际开发中,我们经常看到如MVC中标记在方法上的 [HttpGet] [HttpPost][HttpDelete][HttpPut] ,序列化时标记在类上的 [Serializable] ,使用EF标记属性的 [Key] ,使用特性的地方随处可见。那么特性到底有什么妙用?接下来通过一个实例来体现出Attribute特性的妙用。
众所周知,在开发中参数校验是必不可少的一个环节,什么参数不能为空、必须是手机格式、必须是邮箱格式,长度不能小于xx等等。这种校验在前端和后端都可以校验,对于一个后端开发者来说,有些校验放在前端有种把银行卡放到别人身上一样,总感觉不安全。所有后端进行校验总会让人很放心。
之前没有特性是后端校验代码是这样写的,如下:
/// <summary>/// 建一个用户实体/// </summary>public class UserEntity{/// <summary>/// 姓名/// </summary>public string Name { get; set; }/// <summary>/// 年龄/// </summary>public int Age { get; set; }/// <summary>/// 家庭地址/// </summary>public string Address { get; set; }/// <summary>/// 性别/// </summary>public string Sex { get; set; }/// <summary>/// 手机号码/// </summary>public string PhoneNum { get; set; }/// <summary>/// 电子邮箱/// </summary>public string Email { get; set; }}
假如后台处理的时候传一个UserEntity过来,里面的参数都是必填,那么就需要进行校验了,普通的做法就是
UserEntity entity=new UserEntity();if (entity != null){if (string.IsNullOrWhiteSpace(entity.Name)){throw new Exception("姓名不能为空");}if (entity.Age<=0||entity.Age>120){throw new Exception("年龄不合法");}if (string.IsNullOrWhiteSpace(entity.Address)){throw new Exception("家庭地址不能为空");}.....}
字段多了后这种代码看着就繁琐,这还不包括手机格式验证、电子邮件验证等等,看着就不想写了,当然还有一种是在实体里面进行验证,验证实现就不一一列出,效果都是差不多。
看着以上即繁琐又恶心的代码,有什么方法可以解决呢?这下特性的用途就可以体现得淋漓尽致了。
使用特性后的验证写法如下:
先添加RequiredAttribute、StringLengthAttribute两个自定义验证特性
/// <summary>/// 自定义验证,验证不为空/// </summary>[AttributeUsage(AttributeTargets.Property)]public class RequiredAttribute:Attribute{}/// <summary>/// 自定义验证,验证字符长度/// </summary>[AttributeUsage(AttributeTargets.Property)]public class StringLengthAttribute: Attribute{public int _MaxLength;public int _MinLength;/// <summary>/// /// </summary>/// <param name="MinLength">最小长度</param>/// <param name="MaxLength">最大长度</param>public StringLengthAttribute(int MinLength,int MaxLength){this._MaxLength = MaxLength;this._MinLength = MinLength;}}
添加一个用于校验的CustomValidateExtend类
public class CustomValidateExtend{/// <summary>/// 校验/// </summary>/// <typeparam name="T"></typeparam>/// <returns></returns>public static bool Validate<T>(T entity) where T:class{Type type = entity.GetType();PropertyInfo[] properties = type.GetProperties();//通过反射获取所有属性foreach (var item in properties){if (item.IsDefined(typeof(RequiredAttribute), true))//判断该属性是否被RequiredAttribute特性进行标识{//字段被RequiredAttribute标识了var value=item.GetValue(entity);//反射获取属性值if (value == null || string.IsNullOrWhiteSpace(value.ToString()))//如果字段值为null 或"" " ",则验证不通过{return false;}}if (item.IsDefined(typeof(StringLengthAttribute), true))//判断该属性是否被StringLengthAttribute特性进行标识{//字段被StringLengthAttribute标识了var value = item.GetValue(entity);//反射获取属性值//反射实例化StringLengthAttributeStringLengthAttribute attribute =item.GetCustomAttribute(typeof(StringLengthAttribute), true) as StringLengthAttribute;if (attribute == null){throw new Exception("StringLengthAttribute not instantiate");}if (value == null || value.ToString().Length < attribute._MinLength ||value.ToString().Length > attribute._MaxLength){return false;}}}return true;}}
在用户实体类中我们给Name、PhoneNum分别添加Required、StringLength特性标记
public class UserEntity{/// <summary>/// 姓名/// </summary>[Required]public string Name { get; set; }/// <summary>/// 年龄/// </summary>public int Age { get; set; }/// <summary>/// 家庭地址/// </summary>public string Address { get; set; }/// <summary>/// 性别/// </summary>public string Sex { get; set; }/// <summary>/// 手机号码/// </summary>[Required][StringLength(11, 11)]public string PhoneNum { get; set; }/// <summary>/// 电子邮箱/// </summary>public string Email { get; set; }}
调用 CustomValidateExtend 中的 Validate 校验方法
class Program{static void Main(string[] args){UserEntity entity=new UserEntity();entity.Name = "张三";entity.PhoneNum = "18865245328";var validateResult =CustomValidateExtend.Validate(entity);if (validateResult){Console.WriteLine("验证通过");}else{Console.WriteLine("验证不通过");}Console.ReadKey();}}
执行结果验证通过,把Name赋值为空或PhoneNum的长度小于或大于11,结果为验证不通过,目前为止,基于特性校验已经初步实现,对于追求完美的开发人员来说以下代码看着就不是很舒服。
代码再次升级,我们就使用面向抽象编程的思想进行优化,添加一个AbstractCustomAttribute抽象类,所有的校验类都继承AbstractCustomAttribute
/// <summary>/// /// </summary>public abstract class AbstractCustomAttribute: Attribute//继承Attribute特性类{/// <summary>/// 定义校验抽象方法/// </summary>/// <param name="value">需要校验的值</param>/// <returns></returns>public abstract bool Validate(object value);}
升级之后的RequiredAttribute、StringLengthAttribute自定义验证特性代码如下:
/// <summary>/// 自定义验证,验证不为空/// </summary>[AttributeUsage(AttributeTargets.Property)]public class RequiredAttribute : AbstractCustomAttribute{public override bool Validate(object value){return value != null && !string.IsNullOrWhiteSpace(value.ToString());}}/// <summary>/// 自定义验证,验证字符长度/// </summary>[AttributeUsage(AttributeTargets.Property)]public class StringLengthAttribute : AbstractCustomAttribute{public int _MaxLength;public int _MinLength;/// <summary>/// /// </summary>/// <param name="MinLength">最小长度</param>/// <param name="MaxLength">最大长度</param>public StringLengthAttribute(int MinLength, int MaxLength){this._MaxLength = MaxLength;this._MinLength = MinLength;}public override bool Validate(object value){return value != null && value.ToString().Length >= _MinLength && value.ToString().Length <= _MaxLength;}}
升级后CustomValidateExtend类,重点
public static class CustomValidateExtend{/// <summary>/// 校验/// </summary>/// <typeparam name="T"></typeparam>/// <returns></returns>public static bool Validate<T>(this T entity) where T : class{Type type = entity.GetType();foreach (var item in type.GetProperties()){if (item.IsDefined(typeof(AbstractCustomAttribute), true))//此处是重点{//此处是重点foreach (AbstractCustomAttribute attribute in item.GetCustomAttributes(typeof(AbstractCustomAttribute), true)){if (attribute == null){throw new Exception("StringLengthAttribute not instantiate");}if (!attribute.Validate(item.GetValue(entity))){return false;} }}}return true;}}
执行校验方法
二次升级已完成,看看代码,瞬间心情舒畅。细心的朋友会发现,校验返回的都是true跟false,每次遇到校验不通过的字段后下面的都不再校验了,想要返回所有未校验通过的字段,并告诉调用者,一次性把所有字段都按照格式填好,这样才是我们想要的效果。
当然这样肯定是可以做到的,不要返回true跟false就行了,再次封装有一下就可以达到效果了。
为了写升级代码,我添加了一个ValidateResultEntity实体类型,代码如下:
/// <summary>/// 校验结果实体类/// </summary>public class ValidateResultEntity{/// <summary>/// 是否校验成功/// </summary>public bool IsValidateSuccess { get; set; }/// <summary>/// 校验不通过的字段信息存储字段/// </summary>public List<FieidEntity> ValidateMessage { get; set; }}/// <summary>/// 字段信息/// </summary>public class FieidEntity{/// <summary>/// 字段名称/// </summary>public string FieidName { get; set; }/// <summary>/// 字段类型/// </summary>public string FieidType { get; set; }/// <summary>/// 验证错误时提示信息/// </summary>public string ErrorMessage { get; set; }}
终极版的RequiredAttribute、StringLengthAttribute自定义验证特性代码如下:
/// <summary>/// 自定义验证,验证不为空/// </summary>[AttributeUsage(AttributeTargets.Property)]public class RequiredAttribute : AbstractCustomAttribute{private string _ErrorMessage = "";public RequiredAttribute(){}public RequiredAttribute(string ErrorMessage){this._ErrorMessage = ErrorMessage;}/// <summary>/// 重写Validate校验方法/// </summary>/// <param name="value">需要校验的参数</param>/// <returns></returns>public override FieidEntity Validate(object value){if (value != null && !string.IsNullOrWhiteSpace(value.ToString())){return null;}return new FieidEntity(){ErrorMessage = string.IsNullOrWhiteSpace(_ErrorMessage) ? "字段不能为空" : _ErrorMessage,};}}/// <summary>/// 自定义验证,验证字符长度/// </summary>[AttributeUsage(AttributeTargets.Property)]public class StringLengthAttribute : AbstractCustomAttribute{private int _MaxLength;private int _MinLength;private string _ErrorMessage;/// <summary>/// /// </summary>/// <param name="MinLength">最小长度</param>/// <param name="MaxLength">最大长度</param>public StringLengthAttribute(int MinLength, int MaxLength, string ErrorMessage = ""){this._MaxLength = MaxLength;this._MinLength = MinLength;this._ErrorMessage = ErrorMessage;}/// <summary>/// 重写Validate校验方法/// </summary>/// <param name="value">需要校验的参数</param>/// <returns></returns>public override FieidEntity Validate(object value){if (value != null && value.ToString().Length >= _MinLength && value.ToString().Length <= _MaxLength){return null;}return new FieidEntity(){ErrorMessage = string.IsNullOrWhiteSpace(_ErrorMessage) ? $"字段长度必须大于等于{_MinLength}并且小于等于{_MaxLength}" : _ErrorMessage,};}}
终极版的CustomValidateExtend类
public static class CustomValidateExtend{/// <summary>/// 校验/// </summary>/// <typeparam name="T"></typeparam>/// <returns></returns>public static ValidateResultEntity Validate<T>(this T entity) where T : class{ValidateResultEntity validate = new ValidateResultEntity();validate.IsValidateSuccess = true;List<FieidEntity> fieidList = new List<FieidEntity>();Type type = entity.GetType();foreach (var item in type.GetProperties()){if (item.IsDefined(typeof(AbstractCustomAttribute), true))//此处是重点{//此处是重点foreach (AbstractCustomAttribute attribute in item.GetCustomAttributes(typeof(AbstractCustomAttribute), true)){if (attribute == null){throw new Exception("AbstractCustomAttribute not instantiate");}var result = attribute.Validate(item.GetValue(entity));if (result != null)//校验不通过{result.FieidName = item.Name;//获取字段名称result.FieidType = item.PropertyType.Name;//获取字段类型fieidList.Add(result);//信息加入集合break;//此处为了防止字段被多个校验特性标注,只输出第一个验证不通过的校验信息}}}}if (fieidList.Count > 0){validate.ValidateMessage = fieidList;validate.IsValidateSuccess = false;}return validate;}}
修改UserEntity实体类,添加自定义验证失败的错误信息
测试代码:
class Program{static void Main(string[] args){UserEntity entity=new UserEntity();//entity.Name = "张三";//entity.PhoneNum = "1887065752";var validateResult = entity.Validate();//校验方法if (validateResult.IsValidateSuccess){Console.WriteLine("验证通过");}else{Console.WriteLine("验证不通过");Console.WriteLine("================================================================");var data=JsonConvert.SerializeObject(validateResult.ValidateMessage);Console.WriteLine(data);//打印验证不通过的字段信息}Console.ReadKey();}}
测试结果如下:
最终我们做到了通过特性进行校验字段数据,不再写那种繁琐又臭又长的判断代码了。以上代码还可以继续优化,还可以使用泛型缓存提高其性能。
参考文献https://www.cnblogs.com/jiangxifanzhouyudu/p/11107734.html