第32篇 .Net特性Attribute的高階使用

似梦亦非梦發表於2024-10-15

今天給大家講講.net中特性的高階使用

1.什麼是特性

特性(Attribute)是用於在執行時傳遞程式中各種元素(比如類、方法、結構、列舉、元件等)的行為資訊的宣告性標籤。
用[]來標識,在.Net 框架提供了兩種型別的特性:預定義特性和自定義特性。

2.預定義特性

  • AttributeUsage
  • Conditional
  • obsolete

2.1 AttributeUsage
使用AttributeUsage特性,必須整合Attribute抽象類;

[AttributeUsage(AttributeTargets.Property)]//只能標記在屬性上
public class MyCustomAttribute: Attribute
{

}

AttributeUsage 使用語法詳細如下:

[AttributeUsage(AttributeTargets.Class |//特性只能運用於類上
AttributeTargets.Constructor |//特性只能運用於建構函式上
AttributeTargets.Field |//特性只能運用於欄位上
AttributeTargets.Method |//特性只能運用於方法上
AttributeTargets.Property, //特性只能運用於屬性上
AllowMultiple = true)]//true:可以為程式元素指定有多個例項

其中:

引數 allowmultiple(可選的)為該特性的 AllowMultiple 屬性(property)提供一個布林值。如果為 true,則該特性是多用的。預設值是 false(單用的)。

2.2 Conditional

這個預定義特性標記了一個條件方法,
它會引起方法呼叫的條件編譯,取決於指定的值,比如 Debug 或 Trace。例如,當除錯程式碼時顯示變數的值。
規定該特性的語法如下:

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();
	   }
}

當上面的程式碼被編譯和執行時,它會產生下列結果:

image

2.3 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);
		}
	}

當編譯程式時會出現如下效果,通常該特性用於在方法過期上、版本變更等等
image

public class MyTest
	{
		[Obsolete("該方法已經不可使用,請使用最新XXX方法",true)]
		public static void Message(string msg)
		{
			Console.WriteLine(msg);
		}
	}

當編譯程式時會出現如下效果,可導致程式無法生成
image

3.自定義特性

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.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方法,執行如下:
image

4.Attribute特性的作用

使用面向抽象程式設計的思想進行最佳化,新增一個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;
		}
	}

為了寫升級程式碼,我新增了一個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();
		}
	}

image

最終我們做到了透過特性進行校驗欄位資料,不再寫那種繁瑣又臭又長的判斷程式碼了。以上程式碼還可以繼續最佳化,還可以使用泛型快取提高其效能。

最後介紹一波微軟的模型驗證,
引用【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 
				});
			}
		}

相關文章