C#反射與特性(七):自定義特性以及應用


【微信平台,此文僅授權《NCC 開源社區》訂閱號發布】
本章的內容,主要是對屬性和字段進行賦值和讀值、自定義特性、將特性應用到實際場景。

本文內容已經上傳到 https://gitee.com/whuanle/reflection_and_properties/blob/master/C%23反射與特性(7)自定義特性以及應用.cs

1,屬性字段的賦值和讀值

第五篇中,介紹了成員方法的重載已經調用方式,第六篇中,對以往知識進行了總結以及實踐練習,這一節將介紹對屬性和字段的操作。

從前面我們知道,通過反射可以獲取到屬性 PropertyInfo 、字段 FieldInfo,在《C#反射與特性(三):反射類型的成員》的 1.2 獲取屬性、字段成員中,有詳細介紹。這里不再詳細贅述,下面正式進入話題。

PropertyInfo 中的 GetValue()SetValue() 可以獲得或者設置 實例屬性和字段的值。

創建一個類型

    public class MyClass
    {
        public string A { get; set; }
    }

編寫測試代碼

            // 獲取 Type 以及 PropertyInfo
            Type type = typeof(MyClass);
            PropertyInfo property = type.GetProperty(nameof(MyClass.A));

            // 實例化 MyClass
            object example1 = Activator.CreateInstance(type);
            object example2 = Activator.CreateInstance(type);

            // 對實例 example 中的屬性 A 進行賦值
            property.SetValue(example1,"賦值測試");
            property.SetValue(example2, "Natasha牛逼");


            // 讀取實例中的屬性值
            Console.WriteLine(property.GetValue(example1));
            Console.WriteLine(property.GetValue(example2));

這里要強調的是,反射中的類型調用操作(調用方法屬性等),必須是通過實例來完成。

那些 Type 、PropertyInfo 都是對元數據的讀取,只能讀,只有實例才能對程序產生影響。

從上面的操作中,我們通過反射,創建兩個 example 實例,然后再通過反射對實例進行操作,實現讀值賦值。

屬性的值操作非常簡單,沒有別的內容要說明了。

2,自定義特性和特性查找

在 ASP.NET Core 中,對於 Controller 和 Action ,我們可以使用 [HttpGet][HttpPost][HttpDelete] 等特性,定義請求類型以及路由地址。

在 EFCore 中,我們可以使用 [Key][Required] 等特性,其它框架也有各種各樣的特性。

特性可以用來修飾類、屬性、接口、結構、枚舉、委托、事件、方法、構造函數、字段、參數、返回值、程序集、類型參數和模塊等。

2.1 特性規范和自定義特性

C# 中,預定義了三種特性類型:

名稱 類型 說明
Conditional 位映射特性 可以映射到類型元數據的特定位上,public、abstract 以及 sealed 都會編譯為位映射特性
AttributeUsage 自定義特性 自定義的特性
Obsolete 偽自定義特性 與自定義特性類似,但偽自定義特性會被編譯器或者CLR內部進行優化

位映射特性大多數只在空間中占據一位空間,非常高效。

特性是一個類,繼承了 Attribute ,特性(類)的命名,必須以 Attribute 作為后綴。

2.1.1 定義特性

首先創建一個類繼承 System.Attribute

    public class MyTestAttribute : Attribute
    {

    }

2.1.2 限制特性的使用

通過 AttributeUsageAttribute 限定定義特性可以應用在哪種類型上。

使用示例

    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
    public class MyTestAttribute : Attribute
    {

    }

AttributeUsageAttribute 定義一個特性時,大概格式如下

[AttributeUsage(
   validon,
   AllowMultiple=allowmultiple,
   Inherited=inherited
)]

validon 指 AttributeTargets 枚舉,AttributeTargets 枚舉類型如下

枚舉 說明
All 32767 可以對任何應用程序元素應用屬性
Assembly 1 可以對程序集應用屬性
Class 4 可以對類應用屬性
Constructor 32 可以對構造函數應用屬性
Delegate 4096 可以對委托應用屬性
Enum 16 可以對枚舉應用屬性
Event 512 可以對事件應用屬性
Field 256 可以對字段應用屬性
GenericParameter 16384 可以對泛型參數應用屬性。 目前,此屬性僅可應用於 C#、Microsoft 中間語言 (MSIL) 和已發出的代碼中
Interface 1024 可以對接口應用屬性
Method 64 可以對方法應用屬性
Module 2 可以對模塊應用屬性。 Module 引用的是可移植可執行文件(.dll 或 .exe),而不是 Visual Basic 標准模塊
Parameter 2048 可以對參數應用屬性
Property 128 可以對屬性 (Property) 應用屬性 (Attribute)
ReturnValue 8192 可以對返回值應用屬性
Struct 8 可以對結構應用屬性,即值類型

AllowMultiple 標識是否允許在同一個地方多次使用此特性,默認不允許。如果設置為 true,則可以在同一個屬性或字段等,多次使用此特性。

Inherited 指派生類繼承一個使用此特性的類型時,是否允許派生類繼承此特性。例如 A 使用了此特性,B 繼承於 A,如果 Inherited = true,則派生類也會擁有此特性。

2.1.3 特性的構造函數和屬性

特性可以擁有構造函數和屬性字段等,這些信息通過使用特性時配置。

定義一個特性

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)]
    public class MyTestAttribute : Attribute
    {
        private string A;
        public string Name { get; set; }
        public MyTestAttribute(string message)
        {
            A = message;
        }
    }

使用

    public class MyClass
    {
        [MyTest("test", Name = "666")]
        public string A { get; set; }
    }

2.2 檢索特性

前面創建了自定義特性,然后就到了查找/檢索特性的環節。

但是這些步驟有什么用處呢?作用於什么場景呢?這里先不用管,按照步驟做一次先。

檢索特性的方式有兩種

  • 調用 Type 或者 MemberInfo 的 GetCustomAttributes 方法;
  • 調用 Attribute.GetCustomAttribute 或者 Attribute.GetCustomAttributes 方法;

2.2.1 方式一

先定義特性

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)]
    public class ATestAttribute : Attribute
    {
        public string NameA { get; set; }
    }

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field)]
    public class BTestAttribute : Attribute
    {
        public string NameB { get; set; }
    }

使用特性

    [ATest(NameA = "Myclass")]
    public class MyClass
    {
        [Required]
        [EmailAddress]
        [ATest(NameA = "A")]
        public string A { get; set; }

        [Required]
        [EmailAddress]
        [ATest(NameA = "B")]
        [BTest(NameB = "BB")]
        public string B { get; set; }
    }

運行時檢索

            Type type = typeof(MyClass);
            MemberInfo[] member = type.GetMembers();

            // Type 或者 MemberInfo 的 GetCustomAttributes 方法


            // Type.GetCustomAttributes() 獲取類型的特性
            IEnumerable<Attribute> attrs = type.GetCustomAttributes();

            Console.WriteLine(type.Name + "具有的特性:");
            foreach (ATestAttribute item in attrs)
            {
                Console.WriteLine(item.NameA);
            }

            Console.WriteLine("**********");

            // 循環每個成員
            foreach (MemberInfo item in member)
            {
                // 獲取每個成員擁有的特性
                var attrList = item.GetCustomAttributes();
                foreach (Attribute itemNode in attrList)
                {
                    // 如果是特性 ATestAttribute
                    if (itemNode.GetType() == typeof(ATestAttribute))
                        Console.WriteLine(((ATestAttribute)itemNode).NameA);

                    else if (itemNode.GetType() == typeof(BTestAttribute))
                        Console.WriteLine(((BTestAttribute)itemNode).NameB);

                    else
                        Console.WriteLine("這不是我定義的特性:" + itemNode.GetType());
                }
            }

2.2.2 方式二

上面的自定義特性和 MyClass 類不作改變,將 Main 方法的代碼改成如下

            Type type = typeof(MyClass);

            // Attribute[] classAttr = Attribute.GetCustomAttributes(type);
            // 獲取類型的指定特性
            Attribute classAttr = Attribute.GetCustomAttribute(type,typeof(ATestAttribute));
            Console.WriteLine(((ATestAttribute)classAttr).NameA);

3,設計一個數據驗證工具

為了學以致用,這里實現一個數據驗證功能,能否檢查類型中的屬性是否符合要求。

要求實現:

  • 能夠檢查對象的屬性是否符合格式要求;

  • 自定義驗證失敗消息;

  • 動態實現

  • 良好的編程風格和可拓展性

代碼完成后大約這個樣子(250行左右):

3.1 定義抽象驗證特性類

首先定義一個抽象特性類,作為我們自定義驗證的基礎類,方便后面實現拓展。

    /// <summary>
    /// 自定義驗證特性的抽象類
    /// </summary>
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
    public abstract class MyValidationAttribute : Attribute
    {
        private string Message;
        /// <summary>
        /// 驗證不通過時,提示信息
        /// </summary>
        public string ErrorMessage
        {
            get
            {
                return string.IsNullOrEmpty(Message) ? "默認報錯" : Message;
            }
            set
            {
                Message = value;
            }
        }

        /// <summary>
        /// 檢查驗證是否通過
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public virtual bool IsValid(object value)
        {
            return value == null ? false : true;
        }
    }

設計原理:

ErrorMessage 為自定義的驗證失敗提示消息;如果使用時不填寫,默認為 "默認報錯"

IsValid 指示自定義驗證特性類的驗證入口,通過此方法可以檢查屬性是否通過了驗證。

3.2 實現多個自定義驗證特性

基於 MyValidationAttribute ,我們繼承后,開始實現不同類型的數據驗證。

這里實現了四個驗證:非空驗證、手機號驗證、郵箱格式驗證、是否為數字驗證。

    /// <summary>
    /// 標識屬性或字段不能為空
    /// </summary>
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
    public class MyEmptyAttribute : MyValidationAttribute
    {
        /// <summary>
        /// 驗證是否為空
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        public override bool IsValid(object value)
        {
            if (value == null)
                return false;

            if (string.IsNullOrEmpty(value.ToString()))
                return false;
            return true;
        }
    }

    /// <summary>
    /// 是否是手機號格式
    /// </summary>
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
    public class MyPhoneAttribute : MyValidationAttribute
    {
        public override bool IsValid(object value)
        {
            if (value == null)
                return false;

            if (string.IsNullOrEmpty(value.ToString()))
                return false;

            string pattern = "^((13[0-9])|(14[5,7])|(15[0-3,5-9])|(17[0,3,5-8])|(18[0-9])|166|198|199|(147))\\d{8}$";
            Regex regex = new Regex(pattern);
            return regex.IsMatch(value.ToString());
        }
    }

    /// <summary>
    /// 是否是郵箱格式
    /// </summary>
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
    public class MyEmailAttribute : MyValidationAttribute
    {
        public override bool IsValid(object value)
        {
            if (value == null)
                return false;

            if (string.IsNullOrEmpty(value.ToString()))
                return false;

            string pattern = @"^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$";
            Regex regex = new Regex(pattern);
            return regex.IsMatch(value.ToString());
        }
    }

    /// <summary>
    /// 是否全是數字
    /// </summary>
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
    public class MyNumberAttribute : MyValidationAttribute
    {
        public override bool IsValid(object value)
        {
            if (value == null)
                return false;

            if (string.IsNullOrEmpty(value.ToString()))
                return false;

            string pattern = "^[0-9]*$";
            Regex regex = new Regex(pattern);
            return regex.IsMatch(value.ToString());
        }
    }

實現原理:

通過正則表達式去判斷屬性值是否符合格式(正則表達式都是我抄來的,筆者本人對正則表達式不熟)。

需要說明的是,上面的驗證代碼,還是需要改進的,要適應各種類型的驗證。

3.3 檢查特性是否屬於自定義驗證特性

檢查一個特性是否屬於我們自定義驗證的特性。

如果不是的話,就不需要理會。

        /// <summary>
        /// 檢查特性是否屬於 MyValidationAttribute 類型的特性
        /// </summary>
        /// <param name="attribute">要檢查的特性</param>
        /// <returns></returns>
        private static bool IsMyValidationAttribute(Attribute attribute)
        {
            Type type = attribute.GetType();
            return type.BaseType == typeof(MyValidationAttribute);
        }

實現原理:

我們自定義的驗證特性類,都繼承了 MyValidationAttribute 類型,如果一個特性的父類不是 MyValidationAttribute ,那肯定不是我們實現的特性。

3.4 檢查屬性值是否符合自定義驗證特性的要求

這里涉及到屬性取值、方法調用等,我們通過實例對象、特性對象、屬性對象三者去判斷一個屬性的值是否符合這個特性的要求。

        /// <summary>
        /// 驗證此屬性是否通過驗證,只能驗證 繼承了 MyValidationAttribute 的屬性
        /// </summary>
        /// <param name="attr">屬性帶有的特性</param>
        /// <param name="property">要驗證的屬性</param>
        /// <param name="obj">實例對象</param>
        /// <returns></returns>
        private static (bool, string) StartValid(Attribute attr, PropertyInfo property, object obj)
        {
            // 指定獲取實例對象的屬性值
            object value = property.GetValue(obj);
            // 獲取特性的 IsValid 方法
            MethodInfo attrMethod = attr.GetType().GetMethod("IsValid", new Type[] { typeof(object) });
            // 獲取特性的 IsValid 屬性
            PropertyInfo attrProperty = attr.GetType().GetProperty("ErrorMessage");

            // 開始檢查,獲取檢查結果
            bool checkResult = (bool)attrMethod.Invoke(attr, new object[] { value });

            // 獲取特性的 ErrorMessage 屬性
            string errorMessage = (string)attrProperty.GetValue(attr);

            // 通過驗證的話,就沒有報錯信息
            if (checkResult == true)
                return (true, null);

            // 驗證不通過,返回預定義的信息
            return (false, errorMessage);
        }

設計原理:

  • 首先要驗證的屬性的值;

  • 調用這個特性的 IsValid 方法,檢查值是否通過驗證;

  • 獲取自定義的驗證失敗消息;

  • 返回驗證結果;

3.5 實現解析功能

我們要實現一個功能:

解析對象的所有屬性,逐一對屬性進行檢索,使用到我們設計的自定義驗證特性的屬性,就執行檢查,去獲取驗證結果。

        /// <summary>
        /// 解析功能
        /// </summary>
        /// <param name="list"></param>
        private static void Analysis(List<object> list)
        {
            foreach (var item in list)
            {
                Console.WriteLine("\n\n檢查對象屬性是否通過檢查");
                // 獲取實例對象的類型
                Type type = item.GetType();
                // 獲取類的屬性列表
                PropertyInfo[] properties = type.GetProperties();

                // 對每個屬性進行檢查,是否符合要求
                foreach (PropertyInfo itemNode in properties)
                {
                    Console.WriteLine($"\n屬性:{itemNode.Name},值為 {itemNode.GetValue(item)}");
                    // 此屬性的所有特性
                    IEnumerable<Attribute> attList = itemNode.GetCustomAttributes();
                    if (attList != null)
                    {
                        // 開始對屬性進行特性驗證
                        foreach (Attribute itemNodeNode in attList)
                        {
                            // 如果不是我們自定義的驗證特性,則跳過
                            if (!IsMyValidationAttribute(itemNodeNode))
                                continue;
                            var result = StartValid(itemNodeNode, itemNode, item);

                            // 驗證跳過,提示消息
                            if (result.Item1)
                            {
                                Console.WriteLine($"通過了 {itemNodeNode.GetType().Name} 驗證");
                            }
                            // 沒通過驗證的話
                            else
                            {
                                Console.WriteLine($"未通過了 {itemNodeNode.GetType().Name} 驗證,報錯信息: {result.Item2}");
                            }
                        }
                    }
                    Console.WriteLine("*****屬性分割線******");
                }
                Console.WriteLine("########對象分割線########");
            }
        }

設計原理:

上面有三個循環,第一個是沒什么意義;

因為我們的參數對象是一個對象列表,批量驗證對象,所以需要逐個對象進行分析;

第二個循環,是逐個獲取屬性;

第三個循環是逐個獲取屬性的特性;

上面消息獲取完畢,即可開始進行驗證。

這里必須拿到三個參數:

  • 實例化的對象:反射的基礎是元數據,反射操作的基礎是實例對象;
  • 類型的屬性 PropertyInfo :要通過 PropertyInfo 獲取到實例對象的屬性值;
  • 特性對象 Attribute:從實例對象中獲取到的特性 Attribute 對象;

3.6 編寫一個模型類

我們編寫一個模型類型,來使用自定義的驗證特性

    public class User
    {
        [MyNumber(ErrorMessage = "Id必須全部為數字")]
        public int Id { get; set; }

        [MyEmpty(ErrorMessage = "用戶名不能為空")]
        public string Name { get; set; }

        [MyEmpty]
        [MyPhone(ErrorMessage = "這不是手機號")]
        public long Phone { get; set; }

        [MyEmpty]
        [MyEmail]
        public string Email { get; set; }
    }

使用方法跟 EFCore 的差不多,非常簡單。

你也可以多創建幾個模型類進行測試。

3.7 執行驗證

我們來實例化多個模型類並設置值,然后調用解析功能進行驗證。

在 Main 功能加上以下代碼:

            List<object> users = new List<object>()
            {
                new User
                {
                    Id = 0
                },
                new User
                {
                    Id=1,
                    Name="痴者工良",
                    Phone=13510070650,
                    Email="666@qq.com"
                },
                new User
                {
                    Id=2,
                    Name="NCC牛逼",
                    Phone=6666666,
                    Email="NCC@NCC.NCC"
                }
            };

            Analysis(users);

如無意外,執行結果應該是這樣的

檢查對象屬性是否通過檢查

屬性:Id,值為 0
通過了 MyNumberAttribute 驗證
*****屬性分割線******

屬性:Name,值為
未通過了 MyEmptyAttribute 驗證,報錯信息: 用戶名不能為空
*****屬性分割線******

屬性:Phone,值為 0
通過了 MyEmptyAttribute 驗證
未通過了 MyPhoneAttribute 驗證,報錯信息: 這不是手機號
*****屬性分割線******

屬性:Email,值為
未通過了 MyEmptyAttribute 驗證,報錯信息: 默認報錯
未通過了 MyEmailAttribute 驗證,報錯信息: 默認報錯
*****屬性分割線******
########對象分割線########


檢查對象屬性是否通過檢查

屬性:Id,值為 1
通過了 MyNumberAttribute 驗證
*****屬性分割線******

屬性:Name,值為 痴者工良
通過了 MyEmptyAttribute 驗證
*****屬性分割線******

屬性:Phone,值為 13510070650
通過了 MyEmptyAttribute 驗證
通過了 MyPhoneAttribute 驗證
*****屬性分割線******

屬性:Email,值為 666@qq.com
通過了 MyEmptyAttribute 驗證
通過了 MyEmailAttribute 驗證
*****屬性分割線******
########對象分割線########


檢查對象屬性是否通過檢查

屬性:Id,值為 2
通過了 MyNumberAttribute 驗證
*****屬性分割線******

屬性:Name,值為 NCC牛逼
通過了 MyEmptyAttribute 驗證
*****屬性分割線******

屬性:Phone,值為 6666666
通過了 MyEmptyAttribute 驗證
未通過了 MyPhoneAttribute 驗證,報錯信息: 這不是手機號
*****屬性分割線******

屬性:Email,值為 NCC@NCC.NCC
通過了 MyEmptyAttribute 驗證
通過了 MyEmailAttribute 驗證
*****屬性分割線******
########對象分割線########

3.8 總結

通過七篇文章的示例,估計你已經學會了反射的基礎操作和應用了吧?

本篇文章實現了特性的應用。

單純學會 “自定義特性” ,沒有卵用,要學會如何利用特性去實現業務,才有用處。

本篇對特性的使用, ORM 、ASP.NET Core 等都有常見的應用。

第六篇的時候,我們實現了簡單的依賴注入和 Controller / Action 導航,利用本篇的內容,可以修改第六篇實現的代碼,增加一個路由表的功能,訪問 URL 時,不需要通過 {/Controller/Action} 的路徑去訪問,可以隨意映射 URL 規則。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM