本文作为ASP.NET Core的预备知识,详细讲解下.NET中的特性特性ASP.NET Core开发中是使用最频繁的技术之一,同时也是AOP编程的技术基础。因此很又必要对特性进行一个详细的说明,为将来的开发打下基础。

特性(Attribute)概述

概念

C#语言中,特性是元数据,附加于字段或代码块,如程序集(assemblies)、成员变量、数据类型。编译器反射式编程可访问特性。开发者可以决定把特性作为元数据,专门用于表示与给定应用程序,类和成员有关的,与实例无关的各类信息。开发者也可以决定把一些特性暴露为属性(properties),用作更大的应用程序框架的一部分。特性可以实现为类(派生自System.Attribute)。特性是对现有代码的一种扩展,本身并不会影响既有的逻辑,除非主动去获取,使用特性里实现的功能。

实现

下面具体介绍特性的实现。单纯的码字都是很苍白的,直接上代码!

首先是声明一个自定义的特性:

1
2
3
4
5
6
7
8
9
namespace AttributeDemo.CustomAttributes
{
  //这里声明自定义特性的使用范围,是否可以继承,是否可以重复使用在同一个对象上
  [System.AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
  //特性必须继承Attribute类
  public sealed class CustomAttribute : Attribute
  {
  }
}

AttributeTargets定义了什么地方可以使用特性,此处all声明AttributeTargets中规定的位置都可以使用这个自定义的特性。

接下来使用该特性,我将展示特性可以被使用位置。比如我们在一个student类上使用该特性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
namespace AttributeDemo
{
  //可以对类整体使用
  //Attribute可以省略,因此可以写为[Custom]
  [CustomAttribute]
  public class Student
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public string EMail { get; set; }
    //可以对属性字段使用
    [CustomAttribute]
    public string PhoneNumber { get; set; }
    //可以对方法使用
    [CustomAttribute]
    public void Study()
    {
      Console.WriteLine($"{Name}正在学习中。。。");
    }
    //可以对返回值使用
    [return: CustomAttribute]
    public string SayHi([CustomAttribute] string name)//可以对参数列表使用
    {
      return $"Hello {name}";
    }
  }
}

那么在这些地方标识我们自定义的特性会发生什么呢?下面我用ILSpy反编译下生成的程序,看看我们追加的特性到底做了什么。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
.class public auto ansi beforefieldinit AttributeDemo.Student
extends [System.Runtime]System.Object
{
//特性的构造函数
.custom instance void AttributeDemo.CustomAttributes.CustomAttribute::.ctor() = (
  01 00 00 00
)
// Fields
// 省略

// Methods
// 省略:get/set methods

.method public hidebysig
  instance void Study () cil managed
{
  //特性的构造函数
  .custom instance void AttributeDemo.CustomAttributes.CustomAttribute::.ctor() = (
    01 00 00 00
  )
  // Method begins at RVA 0x20f6
  // Code size 24 (0x18)
  .maxstack 8

  // {
  IL_0000: nop
  // Console.WriteLine(Name + "正在学习中。。。");
  IL_0001: ldarg.0
  IL_0002: call instance string AttributeDemo.Student::get_Name()
  IL_0007: ldstr "正在学习中。。。"
  IL_000c: call string [System.Runtime]System.String::Concat(string, string)
  IL_0011: call void [System.Console]System.Console::WriteLine(string)
  // }
  IL_0016: nop
  IL_0017: ret
} // end of method Student::Study

.method public hidebysig
  instance string SayHi (
    string name
  ) cil managed
{
  //两个特性构造函数,分别对应返回值,参数列表声明的特性标记
  .param [0]
    .custom instance void AttributeDemo.CustomAttributes.CustomAttribute::.ctor() = (
      01 00 00 00
    )
  .param [1]
    .custom instance void AttributeDemo.CustomAttributes.CustomAttribute::.ctor() = (
      01 00 00 00
    )
  // Method begins at RVA 0x2110
  // Code size 17 (0x11)
  .maxstack 2
  .locals init (
    [0] string
  )

  // {
  IL_0000: nop
  // return "Hello " + name;
  IL_0001: ldstr "Hello "
  IL_0006: ldarg.1
  IL_0007: call string [System.Runtime]System.String::Concat(string, string)
  IL_000c: stloc.0
  // (no C# code)
  IL_000d: br.s IL_000f

  IL_000f: ldloc.0
  IL_0010: ret
} // end of method Student::SayHi

.method public hidebysig specialname rtspecialname
  instance void .ctor () cil managed
{
  // Method begins at RVA 0x212d
  // Code size 8 (0x8)
  .maxstack 8

  // {
  IL_0000: ldarg.0
  // (no C# code)
  IL_0001: call instance void [System.Runtime]System.Object::.ctor()
  // }
  IL_0006: nop
  IL_0007: ret
} // end of method Student::.ctor

// Properties
// 省略:属性声明
.property instance string PhoneNumber()
{
  //特性构造函数
  .custom instance void AttributeDemo.CustomAttributes.CustomAttribute::.ctor() = (
    01 00 00 00
  )
  .get instance string AttributeDemo.Student::get_PhoneNumber()
  .set instance void AttributeDemo.Student::set_PhoneNumber(string)
}
} // end of class AttributeDemo.Student

我们可以看到,在程序编译后,根据我们声明的特性标记,编译器自动为我们在对应位置注入了特性相关的代码。这就给了我们进一步的操作空间,我们可以利用特性追加功能,添加更丰富的信息。那么,我们是如何利用特性的呢?下一节我们介绍下特性的使用方式。这里始终记得一点:特性不会影响既有代码,它是不会自动执行的,要使用特性,必须手动调用特性相关方法。

特性的使用

取得特性

要使用特性,首先我们要能够访问到它。特性和普通的类是不一样的,传统的方式是无法访问到的。开篇我们提到,特性是元数据。编译器可以访问元数据,因此部分自带的特性都是在编译时生效的,它们告诉编译器应该怎么处理它们标记的对象。比如,ObsoleteSerialize等等。另一个可以取得元数据的方式就是通过反射技术了。下面的代码,展示了如何取得上面声明的特性:

首先给特性类增加些内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
namespace AttributeDemo.CustomAttributes
{
  //这里声明自定义特性的使用范围,是否可以继承,是否可以重复使用在同一个对象上
  [System.AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
  //特性必须继承Attribute类
  public sealed class CustomAttribute : Attribute
  {
    public CustomAttribute()
    {

    }
    public CustomAttribute(string description, string remark = "")
    {
        Description = description;
        Remark = remark;
    }

    public string Description { get; }
    public string Remark { get; }

    public void Show()
    {
        Console.WriteLine($"This is {nameof(CustomAttribute)}");
    }
  }
}

然后修改Student

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
namespace AttributeDemo
{
  //可以对类整体使用
  [CustomAttribute(description:"类特性示例",remark: "类特性")]
  public class Student
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public string EMail { get; set; }
    //可以对属性字段使用
    [CustomAttribute(description: "属性示例", remark: "属性特性")]
    public string PhoneNumber { get; set; }
    //可以对方法使用
    [CustomAttribute(description: "方法示例", remark: "方法特性")]
    public void Study()
    {
        Console.WriteLine($"{Name}正在学习中。。。");
    }
    //可以对返回值使用
    [return: CustomAttribute(description: "返回值示例", remark: "返回值特性")]
    public string SayHi([CustomAttribute(description: "参数示例", remark: "参数特性")] string name)//可以对参数列表使用
    {
        return $"Hello {name}";
    }
  }
}

追加一个访问数据的方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private static void GetAttribue()
{
  //取得类的特性
  Type classType = typeof(Student);
  if (classType.IsDefined(typeof(CustomAttribute),true))
  {
    object[] classAttributes = classType.GetCustomAttributes(typeof(CustomAttribute), true);
    CustomAttribute classAttribute = (CustomAttribute)classAttributes[0];
    Console.WriteLine($"class description: {classAttribute.Description} | class remark: {classAttribute.Remark}");
  }
  //取得方法的特性
  MethodInfo methodInfo = classType.GetMethod("Study");
  if (methodInfo.IsDefined(typeof(CustomAttribute), true))
  {
    CustomAttribute methodeAttribute = methodInfo.GetCustomAttribute<CustomAttribute>();
    Console.WriteLine($"method description: {methodeAttribute.Description} | method remark: {methodeAttribute.Remark}");
  }
  //取得属性的特性
  PropertyInfo propertyInfo = classType.GetProperty("PhoneNumber");
  if (propertyInfo.IsDefined(typeof(CustomAttribute), true))
  {
    CustomAttribute propertyAttribute = propertyInfo.GetCustomAttribute<CustomAttribute>();
    Console.WriteLine($"property description: {propertyAttribute.Description} | property remark: {propertyAttribute.Remark}");
  }
  //取得参数的特性
  MethodInfo methodInfo1 = classType.GetMethod("SayHi");
  ParameterInfo paramInfo = methodInfo1.GetParameters()[0];
  if (paramInfo.IsDefined(typeof(CustomAttribute), true))
  {
    CustomAttribute ParameterAttribute = paramInfo.GetCustomAttribute<CustomAttribute>();
    Console.WriteLine($"parameter description: {ParameterAttribute.Description} | parameter remark: {ParameterAttribute.Remark}");
  }
  //取得返回值的特性
  ParameterInfo paramInfo1 = methodInfo1.ReturnParameter;
  if (paramInfo1.IsDefined(typeof(CustomAttribute), true))
  {
    CustomAttribute returnAttribute = paramInfo1.GetCustomAttribute<CustomAttribute>();
    Console.WriteLine($"returnValue description: {returnAttribute.Description} | returnValue remark: {returnAttribute.Remark}");
  }
}

运行结果如下:

1
2
3
4
5
class description: 类特性示例 | class remark: 类特性
method description: 方法示例 | method remark: 方法特性
property description: 属性示例 | property remark: 属性特性
parameter description: 参数示例 | parameter remark: 参数特性
returnValue description: 返回值示例 | returnValue remark: 返回值特性

特性应用

取得枚举类型的注释

平时开发时,经常会用到枚举类型及其相关判断,而有时我们想显示枚举类型的注释,怎么办?下面用特性来解决这个问题。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
namespace AttributeDemo.CustomAttributes
{
    public class RemarkAttribute : Attribute
    {
        private readonly string remark;

        public RemarkAttribute(string remark)
        {
            this.remark = remark;
        }

        public string GetRemark()
        {
            return remark;
        }
    }
}

namespace AttributeDemo.Extensions
{
  public enum UserState
  {
    /// <summary>
    /// 正常
    /// </summary>
    [RemarkAttribute("正常")]
    Normal = 0,
    /// <summary>
    /// 冻结
    /// </summary>
    [RemarkAttribute("冻结")]
    Frozen,
    /// <summary>
    /// 删除
    /// </summary>
    [RemarkAttribute("删除")]
    Deleted
  }

  public static class RemarkExtension
  {
    public static string GetRemark(this Enum value)
    {
      Type type = value.GetType();
      FieldInfo field = type.GetField(value.ToString());
      if (field.IsDefined(typeof(RemarkAttribute), true))
      {
        RemarkAttribute attr = field.GetCustomAttribute<RemarkAttribute>();
        return attr.GetRemark();
      }
      return value.ToString();
    }
  }

}

使用

1
2
UserState userState = UserState.Normal;
Console.WriteLine(userState.GetRemark());

数据有效性检查

一般对于用户提交的数据,我们都需要进行数据有效性的检查,之后才能提交到数据库。本次我们使用特性,优雅的解决这个问题。

声明检查数据长度的特性(因为想把数据校验作为一个共通处理,因此需要首先声明一个抽象类):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
namespace AttributeDemo.CustomAttributes
{

  public abstract class CustomValidateAttribute : Attribute
  {
    public abstract bool Validate(object value);
  }

  public class LengthValidateAttribute : CustomValidateAttribute
  {
    private readonly int minLen;
    private readonly int maxLen;

    public LengthValidateAttribute(int minLen, int maxLen)
    {
      this.minLen = minLen;
      this.maxLen = maxLen;
    }

    public override bool Validate(object value)
    {
      if (value != null && !string.IsNullOrEmpty(value.ToString()))
      {
        int len = value.ToString().Length;
        if (len >= minLen && len <= maxLen)
        {
          return true;
        }
      }
      return false;
    }
  }
}

把特性附加到类中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
namespace AttributeDemo
{
  //可以对类整体使用
  [CustomAttribute(description:"类特性示例",remark: "类特性")]
  public class Student
  {
    public int Id { get; set; }
    public string Name { get; set; }
    [LengthValidateAttribute(16, 100)]//追加对邮箱的长度检查
    public string EMail { get; set; }
    //可以对属性字段使用
    [CustomAttribute(description: "属性示例", remark: "属性特性")]
    [LengthValidateAttribute(6, 9)]//追加对电话号码的长度检查
    public string PhoneNumber { get; set; }
    //可以对方法使用
    [CustomAttribute(description: "方法示例", remark: "方法特性")]
    public void Study()
    {
      Console.WriteLine($"{Name}正在学习中。。。");
    }
    //可以对返回值使用
    [return: CustomAttribute(description: "返回值示例", remark: "返回值特性")]
    public string SayHi([CustomAttribute(description: "参数示例", remark: "参数特性")] string name)//可以对参数列表使用
    {
      return $"Hello {name}";
    }
  }
}

再对Student类添加一个扩展方法(如果想对更广泛范围的对象进行数据校验,可以对它们的基类追加扩展方法):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static class ValidateExtension
{
  public static bool Validate(this Student value)
  {
    int errCount = 0;

    Type type = value.GetType();
    PropertyInfo[] properties = type.GetProperties();
    foreach (PropertyInfo property in properties)
    {
      if (property.IsDefined(typeof(CustomValidateAttribute), true))
      {
        IEnumerable<CustomValidateAttribute> attris = property.GetCustomAttributes<CustomValidateAttribute>();
        foreach (CustomValidateAttribute attr in attris)
        {
          if (!attr.Validate(property.GetValue(value)))
          {
            Console.WriteLine($"数据校验失败:字段[{property.Name}]");
            errCount++;
          }
        }
      }
    }

    return errCount == 0 ? true : false;
  }
}

调用数据校验:

1
2
3
4
5
6
7
8
Student stu = new Student
{
  Id = 1,
  EMail = "xxxxx@xxxx.com",
  Name = "brein",
  PhoneNumber = "1234567890"
};
stu.Validate();

输出校验结果:

1
2
数据校验失败:字段[PhoneNumber]
数据校验失败:字段[EMail]

以上,是两个平时用的比较多的关于特性的应用场景。在ASP.NET Core中,特性还有更多应用场景,例如:FilterValidateMVC/API相关特性, AOP应用等等。可以说特性无处不在且非常重要。充分掌握特性相关知识,是掌握ASP.NET Core的充分必要条件。

(全文完)