很久沒寫博客了,今天在百忙之中抽空來寫篇文章,記錄一下最近深刻學習Attribute特性的筆記及心得。~~html
特性(Attribute)是用於在運行時傳遞程序中各類元素(好比類、方法、結構、枚舉、組件等)的行爲信息的聲明性標籤。您能夠經過使用特性向程序添加聲明性信息。一個聲明性標籤是經過放置在它所應用的元素前面的方括號([ ])來描述的。前端
特性(Attribute)用於添加元數據,如編譯器指令和註釋、描述、方法、類等其餘信息。在.Net 框架提供了兩種類型的特性:預約義特性和自定義特性。編程
在.net框架內提供了三種預約義特性,常用特性或對特性有了解的朋友確定見到過或用過。後端
AttributeUsageapi
預約義特性 AttributeUsage 描述瞭如何使用一個自定義特性類。它規定了特性可應用到的項目的類型。使用AttributeUsage 特性有個前提,該類必須繼承Attribute抽象類。緩存
例如:安全
[AttributeUsage(AttributeTargets.Property)]//只能標記在屬性上 public class MyCustomAttribute: Attribute { }
AttributeUsage 使用語法詳細以下:框架
[AttributeUsage(AttributeTargets.Property,AllowMultiple = true,Inherited = true)] ide
[AttributeUsage( validon, AllowMultiple=allowmultiple, Inherited=inherited )]
其中:函數
例如:
[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(message)]
[Obsolete(message, iserror)]
其中:
下面的實例演示了該特性:
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); } }
當編譯程序時會出現以下效果,可致使程序沒法生成
以上是三種預約義特性的介紹
.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方法,執行以下:
從執行結果發現,咱們拿到了咱們想要信息。那麼在實際過程當中有哪些用途呢?接下來就進入文章主題。
在實際開發中,咱們常常看到如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】
裏面有:
詳情可查看【模型驗證】
使用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 }); } }
得出來的效果相似。
若是您有更好的建議和想法歡迎提出,共同進步!
當你想在你的代碼中找到一個錯誤時,這很難;當你認爲你的代碼是不會有錯誤時,這就更難了。