C# Reflection and Attributes (Part 7): Custom Attributes and Their Applications

2020年1月12日 4514点热度 0人点赞 2条评论
内容目录

This chapter primarily focuses on the assignment and reading of properties and fields, custom attributes, and applying attributes to practical scenarios.

The content of this article has been uploaded to https://gitee.com/whuanle/reflection_and_properties/blob/master/C%23反射与特性(7)自定义特性以及应用.cs

1. Assignment and Reading of Property Fields

The fifth article introduced method overloading and calling methods. The sixth article summarized previous knowledge and practical exercises. This section will introduce operations on properties and fields.

From the previous discussion, we know that properties can be accessed through reflection using PropertyInfo and fields using FieldInfo. In "C# Reflection and Attributes (III): Members of Reflecting Types" section 1.2, detailed information is provided on obtaining properties and field members. We will not elaborate further here; let's formally dive into the topic.

The GetValue() and SetValue() methods in PropertyInfo allow you to get or set the values of instance properties and fields.

Creating a type

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

Writing test code

            // Get Type and PropertyInfo
            Type type = typeof(MyClass);
            PropertyInfo property = type.GetProperty(nameof(MyClass.A));
        // Instantiate MyClass
        object example1 = Activator.CreateInstance(type);
        object example2 = Activator.CreateInstance(type);

        // Assign values to property A in example instance
        property.SetValue(example1, "Value Test");
        property.SetValue(example2, "Natasha is awesome");

        // Read property values from instances
        Console.WriteLine(property.GetValue(example1));
        Console.WriteLine(property.GetValue(example2));</code></pre>

It's important to emphasize that reflection operations involving type calls (like calling methods or properties) must be done through instances.

Types and PropertyInfo are only for reading metadata and cannot affect the program; only instances can do so.

From the operations above, we created two example instances using reflection and then manipulated the instances to achieve value reading and assignment.

Operations for the value of properties are quite straightforward, and there is nothing more to elaborate on.

2. Custom Attributes and Attribute Lookup

In ASP.NET Core, we can use attributes like [HttpGet], [HttpPost], [HttpDelete] for Controllers and Actions to define request types and routing addresses.

In EFCore, attributes like [Key], [Required] can be used, and other frameworks also have various types of attributes.

Attributes can be used to annotate classes, properties, interfaces, structs, enums, delegates, events, methods, constructors, fields, parameters, return values, assemblies, type parameters, and modules.

2.1 Specifications and Custom Attributes

C# pre-defines three types of attributes:

Name Type Description
Conditional Bitmapped Attribute Can be mapped to specific bits of type metadata; public, abstract, and sealed are compiled as bitmapped attributes.
AttributeUsage Custom Attribute Custom-defined attributes.
Obsolete Pseudo-Custom Attribute Similar to custom attributes but optimized by the compiler or CLR internally.

Bitmapped attributes mostly occupy only one bit of space in memory, making them very efficient.

An attribute is a class that inherits from Attribute, and the name of an attribute class must end with the suffix Attribute.

2.1.1 Defining Attributes

First, create a class that inherits from System.Attribute.

    public class MyTestAttribute : Attribute
    {
}</code></pre>

2.1.2 Restricting Attribute Usage

Using AttributeUsageAttribute allows you to specify what types the defined attribute can be applied to.

Usage example

    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
    public class MyTestAttribute : Attribute
    {
}</code></pre>

The basic format for defining an attribute with AttributeUsageAttribute is as follows:

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

validon refers to the AttributeTargets enumeration, and the types of AttributeTargets enumeration are as follows:

Enumeration Value Description
All 32767 Attributes can be applied to any application elements.
Assembly 1 Attributes can be applied to an assembly.
Class 4 Attributes can be applied to a class.
Constructor 32 Attributes can be applied to a constructor.
Delegate 4096 Attributes can be applied to a delegate.
Enum 16 Attributes can be applied to an enum.
Event 512 Attributes can be applied to an event.
Field 256 Attributes can be applied to a field.
GenericParameter 16384 Attributes can be applied to generic parameters; currently, this attribute can only be applied in C#, Microsoft Intermediate Language (MSIL), and compiled code.
Interface 1024 Attributes can be applied to an interface.
Method 64 Attributes can be applied to a method.
Module 2 Attributes can be applied to a module; Module refers to portable executable files (.dll or .exe), not to Visual Basic standard modules.
Parameter 2048 Attributes can be applied to a parameter.
Property 128 Attributes can be applied to a property.
ReturnValue 8192 Attributes can be applied to a return value.
Struct 8 Attributes can be applied to a struct (value type).

AllowMultiple indicates whether this attribute can be used multiple times in the same location. By default, it is not allowed. If set to true, the attribute can be applied multiple times to the same field or property, etc.

Inherited indicates whether derived classes can inherit this attribute when inheriting a type that uses this attribute. For example, if A uses this attribute and B inherits from A, then if Inherited = true, the derived class will also have this attribute.

2.1.3 Attribute Constructors and Properties

Attributes can have constructors and property fields, which are configured when using the attribute.

Defining an attribute

    [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;
        }
    }

Using the attribute

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

2.2 Retrieving Attributes

After creating custom attributes, we come to the retrieval phase of attributes.

But what are the purposes of these steps? In what scenarios are they applicable? Don't worry about that for now, let's implement the steps first.

There are two methods for retrieving attributes:

  • Call the GetCustomAttributes method of Type or MemberInfo;
  • Call the Attribute.GetCustomAttribute or Attribute.GetCustomAttributes methods;

2.2.1 Method One

First, define the attributes.

    [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; }
}</code></pre>

Using the attributes

    [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; }
}</code></pre>

Retrieving at runtime

            Type type = typeof(MyClass);
            MemberInfo[] member = type.GetMembers();
        // GetCustomAttributes method of Type or MemberInfo

        // Type.GetCustomAttributes() retrieves attributes of the type
        IEnumerable<ATestAttribute> attrs = type.GetCustomAttributes<ATestAttribute>();

        Console.WriteLine(type.Name + " has attributes:");
        foreach (ATestAttribute item in attrs)
        {
            Console.WriteLine(item.NameA);
        }

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

        // Loop through each member
        foreach (MemberInfo item in member)
        {
            // Get attributes of each member
            var attrList = item.GetCustomAttributes();
            foreach (Attribute itemNode in attrList)
            {
                // If it's an 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("This is not my defined attribute: " + itemNode.GetType());
            }
        }</code></pre>

2.2.2 Method Two

With the previous custom attributes and MyClass class unchanged, change the code in the Main method to the following:

            Type type = typeof(MyClass);
        // Attribute[] classAttr = Attribute.GetCustomAttributes(type);
        // Retrieve specified attributes of the type
        ATestAttribute classAttr = Attribute.GetCustomAttribute(type, typeof(ATestAttribute)) as ATestAttribute;
        Console.WriteLine(classAttr.NameA);</code></pre>

3. Designing a Data Validation Tool

To apply what we've learned, we will implement a data validation feature that can check if the properties within a type meet specified requirements.

Requirements include:

  • The ability to check whether an object's properties meet format requirements;
  • Custom validation failure messages;
  • Dynamic implementation;
  • Good programming style and extensibility.

After the code is complete, it will look something like this (around 250 lines):

3.1 Defining An Abstract Validation Attribute Class

First, define an abstract attribute class as the base class for our custom validation, making it easier to implement extensions later on.

    /// 
    /// Custom validation attribute abstract class
    /// 
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
    public abstract class MyValidationAttribute : Attribute
    {
        private string Message;
        /// 
        /// Error message when validation fails
        /// 
        public string ErrorMessage
        {
            get
            {
                return string.IsNullOrEmpty(Message) ? 默认报错 : Message;
            }
            set
            {
                Message = value;
            }
        }
    /// <summary>
    /// Checks whether the validation is successful
    /// </summary>
    /// <param name="value"></param>
    /// <returns></returns>
    public virtual bool IsValid(object value)
    {
        return value == null ? false : true;
    }
}</code></pre>

Design Principle:

ErrorMessage is a custom message for validation failure; if not specified during use, it defaults to 默认报错.

IsValid indicates the entry point for the custom validation attribute class; this method can check whether the property has passed validation.

3.2 Implementing Multiple Custom Validation Attributes

Based on MyValidationAttribute, we inherit from it to implement different types of data validation.

Four validations are implemented here: non-empty validation, mobile phone number validation, email format validation, and numeric validation.

    /// 
    /// Indicates that a property or field cannot be empty
    /// 
    [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
    public class MyEmptyAttribute : MyValidationAttribute
    {
        /// 
        /// Check if it is empty
        /// 
        /// 
        /// 
        public override bool IsValid(object value)
        {
            if (value == null)
                return false;
        if (string.IsNullOrEmpty(value.ToString()))
            return false;
        return true;
    }
}

/// <summary>
/// Checks if it is a mobile phone number format
/// </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}%%EDITORCONTENT%%quot;;
        Regex regex = new Regex(pattern);
        return regex.IsMatch(value.ToString());
    }
}

/// <summary>
/// Checks if it is in email format
/// </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_-]+)+%%EDITORCONTENT%%quot;;
        Regex regex = new Regex(pattern);
        return regex.IsMatch(value.ToString());
    }
}

/// <summary>
/// Checks if it is all numeric
/// </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]*%%EDITORCONTENT%%quot;;
        Regex regex = new Regex(pattern);
        return regex.IsMatch(value.ToString());
    }
}</code></pre>

Implementation Principle:

Determine if the property value matches the format through regular expressions (the regular expressions are copied, and the author is not familiar with them).

It should be noted that the above validation code still needs improvement to adapt to various types of validation.

3.3 Checking if the Attribute Belongs to Custom Validation Attributes

Check if an attribute belongs to our custom validation attributes.

If it does not, it should be ignored.

        /// 
        /// Checks if the attribute belongs to the MyValidationAttribute type
        /// 
        ///  The attribute to check
        /// 
        private static bool IsMyValidationAttribute(Attribute attribute)
        {
            Type type = attribute.GetType();
            return type.BaseType == typeof(MyValidationAttribute);
        }

Implementation Principle:

Our custom validation attribute classes inherit from MyValidationAttribute; if an attribute's parent class is not MyValidationAttribute, then it is definitely not an attribute we implemented.

3.4 Checking if Property Value Meets Custom Validation Attribute Requirements

This involves property value retrieval, method invocation, etc. We use the instance object, attribute object, and property object to determine whether a property's value meets the attribute's requirements.

        /// 
        /// Validates if this property passes validation; can only validate properties that inherit from MyValidationAttribute
        /// 
        ///  The attribute attached to the property
        ///  The property to validate
        ///  The instance object
        /// 
        private static (bool, string) StartValid(Attribute attr, PropertyInfo property, object obj)
        {
            // Specify to retrieve the property value of the instance object
            object value = property.GetValue(obj);
            // Get the IsValid method of the attribute
            MethodInfo attrMethod = attr.GetType().GetMethod(IsValid, new Type[] { typeof(object) });
            // Get the ErrorMessage property of the attribute
            PropertyInfo attrProperty = attr.GetType().GetProperty(ErrorMessage);
        // Begin checking, get the result
        bool checkResult = (bool)attrMethod.Invoke(attr, new object[] { value });

        // Get the ErrorMessage attribute of the attribute
        string errorMessage = (string)attrProperty.GetValue(attr);

        // If passed validation, there is no error message
        if (checkResult == true)
            return (true, null);

        // If validation fails, return the predefined message
        return (false, errorMessage);
    }</code></pre>

Design Principle:

  • First, verify the property's value;
  • Invoke the IsValid method of this attribute to check if the value passes validation;
  • Retrieve the custom error message for validation failure;
  • Return the validation result;

3.5 Implementing Parsing Functionality

We need to implement a function:

Parse all properties of an object, systematically retrieve each property, and perform checks on properties using our designed custom validation attributes to obtain validation results.

        /// 
        /// Parsing functionality
        /// 
        /// 
        private static void Analysis(List list)
        {
            foreach (var item in list)
            {
                Console.WriteLine(\n\nChecking if object properties pass validation);
                // Get the type of the instance object
                Type type = item.GetType();
                // Get the list of properties of the class
                PropertyInfo[] properties = type.GetProperties();
            // Check each property for compliance
            foreach (PropertyInfo itemNode in properties)
            {
                Console.WriteLine(%%EDITORCONTENT%%quot;\nProperty: {itemNode.Name}, Value: {itemNode.GetValue(item)});
                // All attributes of this property
                IEnumerable<attribute> attList = itemNode.GetCustomAttributes();
                if (attList != null)
                {
                    // Begin verifying the property attributes
                    foreach (Attribute itemNodeNode in attList)
                    {
                        // If it's not our custom validation attribute, skip
                        if (!IsMyValidationAttribute(itemNodeNode))
                            continue;
                        var result = StartValid(itemNodeNode, itemNode, item);

                        // If validation passes, show message
                        if (result.Item1)
                        {
                            Console.WriteLine(%%EDITORCONTENT%%quot;Passed {itemNodeNode.GetType().Name} validation);
                        }
                        // If validation fails
                        else
                        {
                            Console.WriteLine(%%EDITORCONTENT%%quot;Failed {itemNodeNode.GetType().Name} validation, Error message: {result.Item2});
                        }
                    }
                }
                Console.WriteLine(*****Property Divider******);
            }
            Console.WriteLine(########Object Divider########);
        }
    }</attribute></object></code></pre>

Design Principle:

There are three loops above; the first one is not significant;

Because the parameter object is a list of objects, to perform batch validation, we need to analyze each object one by one;

The second loop retrieves properties one by one;

The third loop retrieves attributes for each property one by one;

Once we have the above messages, we can begin validation.

Three parameters must be obtained:

  • The instantiated object: the basis of reflection is metadata, and the basis of reflection operations is the instance object;
  • The property's PropertyInfo: PropertyInfo is used to access the instance object's property value;
  • The attribute object: the Attribute object obtained from the instance object;

3.6 Writing a Model Class

We will write a model type to use the custom validation attributes.

    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; }
}</code></pre>

The usage is quite simple, similar to EFCore.

You can also create several model classes for testing.

3.7 Performing Validation

Let's instantiate several model classes, set values, and then call the parsing functionality to perform validation.

Add the following code to the Main function:

            List users = new List()
            {
                new User
                {
                    Id = 0
                },
                new User
                {
                    Id=1,
                    Name=whuanle,
                    Phone=13510070650,
                    Email=666@qq.com
                },
                new User
                {
                    Id=2,
                    Name=NCC牛逼,
                    Phone=6666666,
                    Email=NCC@NCC.NCC
                }
            };
        Analysis(users);</object></object></code></pre>

If everything goes as expected, the output should look like this:

Check whether object properties pass the validation

Property: Id, Value: 0 Passed MyNumberAttribute validation Property Separator*

Property: Name, Value: Failed MyEmptyAttribute validation, Error Message: Username cannot be empty Property Separator*

Property: Phone, Value: 0 Passed MyEmptyAttribute validation Failed MyPhoneAttribute validation, Error Message: This is not a phone number Property Separator*

Property: Email, Value: Failed MyEmptyAttribute validation, Error Message: Default error Failed MyEmailAttribute validation, Error Message: Default error Property Separator* ########Object Separator########

Check whether object properties pass the validation

Property: Id, Value: 1 Passed MyNumberAttribute validation Property Separator*

Property: Name, Value: whuanle Passed MyEmptyAttribute validation Property Separator*

Property: Phone, Value: 13510070650 Passed MyEmptyAttribute validation Passed MyPhoneAttribute validation Property Separator*

Property: Email, Value: 666@qq.com Passed MyEmptyAttribute validation Passed MyEmailAttribute validation Property Separator* ########Object Separator########

Check whether object properties pass the validation

Property: Id, Value: 2 Passed MyNumberAttribute validation Property Separator*

Property: Name, Value: NCC牛逼 Passed MyEmptyAttribute validation Property Separator*

Property: Phone, Value: 6666666 Passed MyEmptyAttribute validation Failed MyPhoneAttribute validation, Error Message: This is not a phone number Property Separator*

Property: Email, Value: NCC@NCC.NCC Passed MyEmptyAttribute validation Passed MyEmailAttribute validation Property Separator* ########Object Separator########

3.8 Summary

Through the examples in seven articles, you have probably learned the basics of reflection operations and applications, right?

This article implemented the application of attributes.

Simply learning "custom attributes" is useless; you need to learn how to utilize attributes to implement business logic in order to be useful.

There are common applications of attributes in ORM, ASP.NET Core, etc.

In the sixth article, we achieved simple dependency injection and Controller/Action navigation. Utilizing the content of this article, you can modify the code implemented in the sixth article to add the functionality of a routing table. When accessing a URL, you won't need to go through the /Controller/Action path but can map URL rules arbitrarily.

痴者工良

高级程序员劝退师

文章评论