本文作为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
|
我们可以看到,在程序编译后,根据我们声明的特性标记,编译器自动为我们在对应位置注入了特性相关的代码。这就给了我们进一步的操作空间,我们可以利用特性追加功能,添加更丰富的信息。那么,我们是如何利用特性的呢?下一节我们介绍下特性的使用方式。这里始终记得一点:特性不会影响既有代码,它是不会自动执行的,要使用特性,必须手动调用特性相关方法。
特性的使用
取得特性
要使用特性,首先我们要能够访问到它。特性和普通的类是不一样的,传统的方式是无法访问到的。开篇我们提到,特性是元数据。编译器可以访问元数据,因此部分自带的特性都是在编译时生效的,它们告诉编译器应该怎么处理它们标记的对象。比如,Obsolete
,Serialize
等等。另一个可以取得元数据的方式就是通过反射技术了。下面的代码,展示了如何取得上面声明的特性:
首先给特性类增加些内容:
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
中,特性还有更多应用场景,例如:Filter
,Validate
, MVC
/API
相关特性, AOP
应用等等。可以说特性无处不在且非常重要。充分掌握特性相关知识,是掌握ASP.NET Core
的充分必要条件。
(全文完)
文章作者
Brein
上次更新
2021年7月10日 21时58分57秒
许可协议
转载本站文章请注明作者和出处 Brein's Blog,请勿用于任何商业用途