好久沒寫博客了,今天在百忙之中抽空來寫篇文章,記錄一下最近深入學習Attribute特性的筆記及心得。~~
一、什么是特性?
特性(Attribute)是用於在運行時傳遞程序中各種元素(比如類、方法、結構、枚舉、組件等)的行為信息的聲明性標簽。您可以通過使用特性向程序添加聲明性信息。一個聲明性標簽是通過放置在它所應用的元素前面的方括號([ ])來描述的。
特性(Attribute)用於添加元數據,如編譯器指令和注釋、描述、方法、類等其他信息。在.Net 框架提供了兩種類型的特性:預定義特性和自定義特性。
1.預定義特性
在.net框架內提供了三種預定義特性,經常使用特性或對特性有了解的朋友肯定見到過或用過。
- AttributeUsage
- Conditional
- obsolete
AttributeUsage
預定義特性 AttributeUsage 描述了如何使用一個自定義特性類。它規定了特性可應用到的項目的類型。使用AttributeUsage 特性有個前提,該類必須繼承Attribute抽象類。
例如:
[AttributeUsage(AttributeTargets.Property)]//只能標記在屬性上 public class MyCustomAttribute: Attribute { }
AttributeUsage 使用語法詳細如下:
[AttributeUsage(AttributeTargets.Property,AllowMultiple = true,Inherited = true)]
[AttributeUsage( validon, AllowMultiple=allowmultiple, Inherited=inherited )]
其中:
- 參數 validon 規定特性可被放置的語言元素。它是枚舉器 AttributeTargets 的值的組合。默認值是 AttributeTargets.All。
- 參數 allowmultiple(可選的)為該特性的 AllowMultiple 屬性(property)提供一個布爾值。如果為 true,則該特性是多用的。默認值是 false(單用的)。
- 參數 inherited(可選的)為該特性的 Inherited 屬性(property)提供一個布爾值。如果為 true,則該特性可被派生類繼承。默認值是 false(不被繼承)。
例如:
[AttributeUsage(AttributeTargets.Class |//特性只能運用於類上 AttributeTargets.Constructor |//特性只能運用於構造函數上 AttributeTargets.Field |//特性只能運用於字段上 AttributeTargets.Method |//特性只能運用於方法上 AttributeTargets.Property, //特性只能運用於屬性上 AllowMultiple = true)]//true:可以為程序元素指定有多個實例
Conditional
這個預定義特性標記了一個條件方法,其執行依賴於它頂的預處理標識符。
它會引起方法調用的條件編譯,取決於指定的值,比如 Debug 或 Trace。例如,當調試代碼時顯示變量的值。
規定該特性的語法如下:
[Conditional(conditionalSymbol)]
public class MyTest { [Conditional("DEBUG")] public static void Message(string msg) { Console.WriteLine(msg); } } class Program { static void function1() { MyTest.Message("In Function 1."); function2(); } static void function2() { MyTest.Message("In Function 2."); } static void Main(string[] args) { MyTest.Message("In Main function."); function1(); Console.ReadLine(); } }
當上面的代碼被編譯和執行時,它會產生下列結果:
Obsolete
這個預定義特性標記了不應被使用的程序實體。它可以讓您通知編譯器丟棄某個特定的目標元素。例如,當一個新方法被用在一個類中,但是您仍然想要保持類中的舊方法,您可以通過顯示一個應該使用新方法,而不是舊方法的消息,來把它標記為 obsolete(過時的)。
規定該特性的語法如下:
[Obsolete(message)]
[Obsolete(message, iserror)]
其中:
- 參數 message,是一個字符串,描述項目為什么過時的原因以及該替代使用什么。
- 參數 iserror,是一個布爾值。如果該值為 true,編譯器應把該項目的使用當作一個錯誤。默認值是 false(編譯器生成一個警告)。
下面的實例演示了該特性:
public class MyTest { [Obsolete("該方法已過期,你可使用xxx最新方法")] public static void Message(string msg) { Console.WriteLine(msg); } }
當編譯程序時會出現如下效果,通常該特性用於在方法過期上、版本變更等等
public class MyTest { [Obsolete("該方法已經不可使用,請使用最新XXX方法",true)] public static void Message(string msg) { Console.WriteLine(msg); } }
當編譯程序時會出現如下效果,可導致程序無法生成
以上是三種預定義特性的介紹
2.自定義特性
.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(); Console.ReadKey(); } }
執行Main方法,然后你會發現啥事都沒發生,what?那這特性有個錘子用。當然不是,想要獲取標記的內容就需要用到反射,獲取方法如下:
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;//實例化PrintLogAttribute var msg = attribute.GetMsg(); Console.WriteLine($"得到標記信息:{msg}"); } } Console.ReadKey(); } }
執行Main方法,執行如下:
從執行結果發現,我們拿到了我們想要信息。那么在實際過程中有哪些用途呢?接下來就進入文章主題。
二、Attribute特性實際妙用?
在實際開發中,我們經常看到如MVC中標記在方法上的 [HttpGet] [HttpPost][HttpDelete][HttpPut] ,序列化時標記在類上的 [Serializable] ,使用EF是標記屬性的 [Key] ,以及之前wepApi文章中的三大過濾的簡單使用都使用到了特性,具體可查看【WebApi 過濾器的使用,開發接口必備利器】,使用特性的地方隨處可見。那么特性到底有什么妙用?接下來通過一個實例來體現出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);//反射獲取屬性值 //反射實例化StringLengthAttribute StringLengthAttribute 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 { /// <summary> /// 重寫Validate校驗方法 /// </summary> /// <param name="value">需要校驗的參數</param> /// <returns></returns> public override bool Validate(object value) { return value != null && !string.IsNullOrWhiteSpace(value.ToString()); } } /// <summary> /// 自定義驗證,驗證字符長度 /// </summary> [AttributeUsage(AttributeTargets.Property)] public class StringLengthAttribute: AbstractCustomAttribute { private int _MaxLength; private int _MinLength; /// <summary> /// /// </summary> /// <param name="MinLength">最小長度</param> /// <param name="MaxLength">最大長度</param> public StringLengthAttribute(int MinLength,int MaxLength) { this._MaxLength = MaxLength; this._MinLength = MinLength; } /// <summary> /// 重寫Validate校驗方法 /// </summary> /// <param name="value">需要校驗的參數</param> /// <returns></returns> 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; } }
執行校驗方法
class Program { static void Main(string[] args) { UserEntity entity=new UserEntity(); entity.Name = "張三"; entity.PhoneNum = "1887065752"; var validateResult = entity.Validate();//校驗方法 if (validateResult) { Console.WriteLine("驗證通過"); } else { Console.WriteLine("驗證不通過"); } Console.ReadKey(); } }
因為手機號少了一位,所有校驗不通過。二次升級已完成,看看代碼,瞬間心情舒暢。細心的朋友會發現,校驗返回的都是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實體類,添加自定義驗證失敗的錯誤信息
/// <summary> /// /// </summary> 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,"手機號碼必須等於11位")] public string PhoneNum { get; set; } /// <summary> /// 電子郵箱 /// </summary> public string Email { get; set; } }
測試代碼:
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(); } }
測試結果如下:
最終我們做到了通過特性進行校驗字段數據,不再寫那種繁瑣又臭又長的判斷代碼了。以上代碼還可以繼續優化,還可以使用泛型緩存提高其性能。
最后介紹一波微軟的模型驗證,原理類似,最近個人用於WebAPI上,
引用【System.ComponentModel.DataAnnotations】
里面有:
- [Required]
- [Range]
- ........
詳情可查看【模型驗證】
使用ActionFilterAttribute過濾器我們可以進行校驗操作,核心代碼如下:
/// <summary> /// 接口請求前操作,使用ActionFilterAttribute過濾器 /// </summary> /// <param name="actionContext"></param> public override void OnActionExecuting(HttpActionContext actionContext) { if (!actionContext.ModelState.IsValid) { var data=new Dictionary<string,string>(); if (actionContext.ModelState.Keys.Count > 0) { for (var i=0;i<actionContext.ModelState.Keys.Count;i++) { if (actionContext.ModelState.Values.ElementAt(i).Errors.Count > 0) { data.Add(actionContext.ModelState.Keys.ElementAt(i), actionContext.ModelState.Values.ElementAt(i).Errors.First().ErrorMessage); } } } actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest, new { StatusCode = HttpStatusCode.BadRequest, Data = "", Message = "參數驗證問題或必填參數未填寫,請核對", Details = data }); } }
得出來的效果類似。
如果您有更好的建議和想法歡迎提出,共同進步!
當你想在你的代碼中找到一個錯誤時,這很難;當你認為你的代碼是不會有錯誤時,這就更難了。