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

[TOC]html

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

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

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

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

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

PropertyInfo 中的 GetValue()SetValue() 能夠得到或者設置 實例屬性和字段的值。微信

建立一個類型框架

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

編寫測試代碼ide

// 獲取 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 規則。

原文出處:https://www.cnblogs.com/whuanle/p/12182962.html

相關文章
相關標籤/搜索