C#反射与特性(七):自定义特性以及应用
<p></p><div class="toc"><div class="toc-container-header">目录</div><ul><li>1,属性字段的赋值和读值</li><li>2,自定义特性和特性查找<ul><li>2.1 特性规范和自定义特性<ul><li>2.1.1 定义特性</li><li>2.1.2 限制特性的使用</li><li>2.1.3 特性的构造函数和属性</li></ul></li><li>2.2 检索特性<ul><li>2.2.1 方式一</li><li>2.2.2 方式二</li></ul></li></ul></li><li>3,设计一个数据验证工具<ul><li>3.1 定义抽象验证特性类</li><li>3.2 实现多个自定义验证特性</li><li>3.3 检查特性是否属于自定义验证特性</li><li>3.4 检查属性值是否符合自定义验证特性的要求</li><li>3.5 实现解析功能</li><li>3.6 编写一个模型类</li><li>3.7 执行验证</li><li>3.8 总结</li></ul></li></ul></div><p></p><p>【微信平台,此文仅授权《NCC 开源社区》订阅号发布】<br>
本章的内容,主要是对<strong>属性和字段进行赋值和读值</strong>、自定义特性、将特性应用到实际场景。</p>
<p>本文内容已经上传到 https://gitee.com/whuanle/reflection_and_properties/blob/master/C%23反射与特性(7)自定义特性以及应用.cs</p>
<p><img src="https://img2018.cnblogs.com/blog/1315495/202001/1315495-20200112161345647-672170607.png" alt="" loading="lazy"></p>
<h2 id="1属性字段的赋值和读值">1,属性字段的赋值和读值</h2>
<p>第五篇中,介绍了成员方法的重载已经调用方式,第六篇中,对以往知识进行了总结以及实践练习,这一节将介绍对属性和字段的操作。</p>
<p>从前面我们知道,通过反射可以获取到属性 PropertyInfo 、字段 FieldInfo,在《C#反射与特性(三):反射类型的成员》的 1.2 获取属性、字段成员中,有详细介绍。这里不再详细赘述,下面正式进入话题。</p>
<p>PropertyInfo 中的 <code>GetValue()</code>和 <code>SetValue()</code> 可以获得或者设置 实例属性和字段的值。</p>
<p>创建一个类型</p>
<pre><code class="language-c#"> public class MyClass
{
public string A { get; set; }
}
</code></pre>
<p>编写测试代码</p>
<pre><code class="language-c#"> // 获取 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));
</code></pre>
<p>这里要强调的是,反射中的类型调用操作(调用方法属性等),必须是通过<strong>实例</strong>来完成。</p>
<p>那些 Type 、PropertyInfo 都是对元数据的读取,只能读,只有实例才能对程序产生影响。</p>
<p>从上面的操作中,我们通过反射,创建两个 example 实例,然后再通过反射对实例进行操作,实现读值赋值。</p>
<p>属性的值操作非常简单,没有别的内容要说明了。</p>
<h2 id="2自定义特性和特性查找">2,自定义特性和特性查找</h2>
<p>在 ASP.NET Core 中,对于 Controller 和 Action ,我们可以使用 <code></code>、<code></code>、<code></code> 等特性,定义请求类型以及路由地址。</p>
<p>在 EFCore 中,我们可以使用 <code></code>、<code></code> 等特性,其它框架也有各种各样的特性。</p>
<p>特性可以用来修饰类、属性、接口、结构、枚举、委托、事件、方法、构造函数、字段、参数、返回值、程序集、类型参数和模块等。</p>
<h3 id="21-特性规范和自定义特性">2.1 特性规范和自定义特性</h3>
<p>C# 中,预定义了三种特性类型:</p>
<table>
<thead>
<tr>
<th>名称</th>
<th>类型</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>Conditional</td>
<td>位映射特性</td>
<td>可以映射到类型元数据的特定位上,public、abstract 以及 sealed 都会编译为位映射特性</td>
</tr>
<tr>
<td>AttributeUsage</td>
<td>自定义特性</td>
<td>自定义的特性</td>
</tr>
<tr>
<td>Obsolete</td>
<td>伪自定义特性</td>
<td>与自定义特性类似,但伪自定义特性会被编译器或者CLR内部进行优化</td>
</tr>
</tbody>
</table>
<p>位映射特性大多数只在空间中占据一位空间,非常高效。</p>
<p>特性是一个类,继承了 Attribute ,特性(类)的命名,必须以 <code>Attribute</code> 作为后缀。</p>
<h4 id="211-定义特性">2.1.1 定义特性</h4>
<p>首先创建一个类继承 <code>System.Attribute</code></p>
<pre><code class="language-c#"> public class MyTestAttribute : Attribute
{
}
</code></pre>
<h4 id="212-限制特性的使用">2.1.2 限制特性的使用</h4>
<p>通过 <code>AttributeUsageAttribute</code> 限定定义特性可以应用在哪种类型上。</p>
<p>使用示例</p>
<pre><code class="language-c#">
public class MyTestAttribute : Attribute
{
}
</code></pre>
<p>AttributeUsageAttribute 定义一个特性时,大概格式如下</p>
<pre><code class="language-c#">[AttributeUsage(
validon,
AllowMultiple=allowmultiple,
Inherited=inherited
)]
</code></pre>
<p>validon 指 AttributeTargets 枚举,AttributeTargets 枚举类型如下</p>
<table>
<thead>
<tr>
<th>枚举</th>
<th>值</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>All</td>
<td>32767</td>
<td>可以对任何应用程序元素应用属性</td>
</tr>
<tr>
<td>Assembly</td>
<td>1</td>
<td>可以对程序集应用属性</td>
</tr>
<tr>
<td>Class</td>
<td>4</td>
<td>可以对类应用属性</td>
</tr>
<tr>
<td>Constructor</td>
<td>32</td>
<td>可以对构造函数应用属性</td>
</tr>
<tr>
<td>Delegate</td>
<td>4096</td>
<td>可以对委托应用属性</td>
</tr>
<tr>
<td>Enum</td>
<td>16</td>
<td>可以对枚举应用属性</td>
</tr>
<tr>
<td>Event</td>
<td>512</td>
<td>可以对事件应用属性</td>
</tr>
<tr>
<td>Field</td>
<td>256</td>
<td>可以对字段应用属性</td>
</tr>
<tr>
<td>GenericParameter</td>
<td>16384</td>
<td>可以对泛型参数应用属性。 目前,此属性仅可应用于 C#、Microsoft 中间语言 (MSIL) 和已发出的代码中</td>
</tr>
<tr>
<td>Interface</td>
<td>1024</td>
<td>可以对接口应用属性</td>
</tr>
<tr>
<td>Method</td>
<td>64</td>
<td>可以对方法应用属性</td>
</tr>
<tr>
<td>Module</td>
<td>2</td>
<td>可以对模块应用属性。 <code>Module</code> 引用的是可移植可执行文件(.dll 或 .exe),而不是 Visual Basic 标准模块</td>
</tr>
<tr>
<td>Parameter</td>
<td>2048</td>
<td>可以对参数应用属性</td>
</tr>
<tr>
<td>Property</td>
<td>128</td>
<td>可以对属性 (Property) 应用属性 (Attribute)</td>
</tr>
<tr>
<td>ReturnValue</td>
<td>8192</td>
<td>可以对返回值应用属性</td>
</tr>
<tr>
<td>Struct</td>
<td>8</td>
<td>可以对结构应用属性,即值类型</td>
</tr>
</tbody>
</table>
<p>AllowMultiple 标识是否允许在同一个地方多次使用此特性,默认不允许。如果设置为 true,则可以在同一个属性或字段等,多次使用此特性。</p>
<p>Inherited 指派生类继承一个使用此特性的类型时,是否允许派生类继承此特性。例如 A 使用了此特性,B 继承于 A,如果 <code>Inherited= true</code>,则派生类也会拥有此特性。</p>
<h4 id="213-特性的构造函数和属性">2.1.3 特性的构造函数和属性</h4>
<p>特性可以拥有构造函数和属性字段等,这些信息通过使用特性时配置。</p>
<p>定义一个特性</p>
<pre><code class="language-c#">
public class MyTestAttribute : Attribute
{
private string A;
public string Name { get; set; }
public MyTestAttribute(string message)
{
A = message;
}
}
</code></pre>
<p>使用</p>
<pre><code class="language-c#"> public class MyClass
{
public string A { get; set; }
}
</code></pre>
<h3 id="22-检索特性">2.2 检索特性</h3>
<p>前面创建了自定义特性,然后就到了查找/检索特性的环节。</p>
<p>但是这些步骤有什么用处呢?作用于什么场景呢?这里先不用管,按照步骤做一次先。</p>
<p>检索特性的方式有两种</p>
<ul>
<li>调用 Type 或者 MemberInfo 的 GetCustomAttributes 方法;</li>
<li>调用 Attribute.GetCustomAttribute 或者 Attribute.GetCustomAttributes 方法;</li>
</ul>
<h4 id="221-方式一">2.2.1 方式一</h4>
<p>先定义特性</p>
<pre><code class="language-c#">
public class ATestAttribute : Attribute
{
public string NameA { get; set; }
}
public class BTestAttribute : Attribute
{
public string NameB { get; set; }
}
</code></pre>
<p>使用特性</p>
<pre><code class="language-c#">
public class MyClass
{
public string A { get; set; }
public string B { get; set; }
}
</code></pre>
<p>运行时检索</p>
<pre><code class="language-c#"> 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());
}
}
</code></pre>
<h4 id="222-方式二">2.2.2 方式二</h4>
<p>上面的自定义特性和 MyClass 类不作改变,将 Main 方法的代码改成如下</p>
<pre><code class="language-c#"> Type type = typeof(MyClass);
// Attribute[] classAttr = Attribute.GetCustomAttributes(type);
// 获取类型的指定特性
Attribute classAttr = Attribute.GetCustomAttribute(type,typeof(ATestAttribute));
Console.WriteLine(((ATestAttribute)classAttr).NameA);
</code></pre>
<h2 id="3设计一个数据验证工具">3,设计一个数据验证工具</h2>
<p>为了学以致用,这里实现一个数据验证功能,能否检查类型中的属性是否符合要求。</p>
<p>要求实现:</p>
<ul>
<li>
<p>能够检查对象的属性是否符合格式要求;</p>
</li>
<li>
<p>自定义验证失败消息;</p>
</li>
<li>
<p>动态实现</p>
</li>
<li>
<p>良好的编程风格和可拓展性</p>
</li>
</ul>
<p>代码完成后大约这个样子(250行左右):</p>
<p><img src="https://img2018.cnblogs.com/blog/1315495/202001/1315495-20200112161440043-1360569284.png" alt="" loading="lazy"></p>
<h3 id="31-定义抽象验证特性类">3.1 定义抽象验证特性类</h3>
<p>首先定义一个抽象特性类,作为我们自定义验证的基础类,方便后面实现拓展。</p>
<pre><code class="language-c#"> /// <summary>
/// 自定义验证特性的抽象类
/// </summary>
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;
}
}
</code></pre>
<p>设计原理:</p>
<p>ErrorMessage 为自定义的验证失败提示消息;如果使用时不填写,默认为 <code>"默认报错"</code>。</p>
<p>IsValid 指示自定义验证特性类的验证入口,通过此方法可以检查属性是否通过了验证。</p>
<h3 id="32-实现多个自定义验证特性">3.2 实现多个自定义验证特性</h3>
<p>基于 MyValidationAttribute ,我们继承后,开始实现不同类型的数据验证。</p>
<p>这里实现了四个验证:非空验证、手机号验证、邮箱格式验证、是否为数字验证。</p>
<pre><code class="language-c#"> /// <summary>
/// 标识属性或字段不能为空
/// </summary>
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>
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)|(14)|(15)|(17)|(18)|166|198|199|(147))\\d{8}$";
Regex regex = new Regex(pattern);
return regex.IsMatch(value.ToString());
}
}
/// <summary>
/// 是否是邮箱格式
/// </summary>
public class MyEmailAttribute : MyValidationAttribute
{
public override bool IsValid(object value)
{
if (value == null)
return false;
if (string.IsNullOrEmpty(value.ToString()))
return false;
string pattern = @"^+@+(\.+)+$";
Regex regex = new Regex(pattern);
return regex.IsMatch(value.ToString());
}
}
/// <summary>
/// 是否全是数字
/// </summary>
public class MyNumberAttribute : MyValidationAttribute
{
public override bool IsValid(object value)
{
if (value == null)
return false;
if (string.IsNullOrEmpty(value.ToString()))
return false;
string pattern = "^*$";
Regex regex = new Regex(pattern);
return regex.IsMatch(value.ToString());
}
}
</code></pre>
<p>实现原理:</p>
<p>通过正则表达式去判断属性值是否符合格式(正则表达式都是我抄来的,笔者本人对正则表达式不熟)。</p>
<p>需要说明的是,上面的验证代码,还是需要改进的,要适应各种类型的验证。</p>
<h3 id="33-检查特性是否属于自定义验证特性">3.3 检查特性是否属于自定义验证特性</h3>
<p>检查一个特性是否属于我们自定义验证的特性。</p>
<p>如果不是的话,就不需要理会。</p>
<pre><code class="language-c#"> /// <summary>
/// 检查特性是否属于 MyValidationAttribute 类型的特性
/// </summary>
/// <param name="attribute">要检查的特性</param>
/// <returns></returns>
private static bool IsMyValidationAttribute(Attribute attribute)
{
Type type = attribute.GetType();
return type.BaseType == typeof(MyValidationAttribute);
}
</code></pre>
<p>实现原理:</p>
<p>我们自定义的验证特性类,都继承了 MyValidationAttribute 类型,如果一个特性的父类不是 <code>MyValidationAttribute</code> ,那肯定不是我们实现的特性。</p>
<h3 id="34-检查属性值是否符合自定义验证特性的要求">3.4 检查属性值是否符合自定义验证特性的要求</h3>
<p>这里涉及到属性取值、方法调用等,我们通过实例对象、特性对象、属性对象三者去判断一个属性的值是否符合这个特性的要求。</p>
<pre><code class="language-c#"> /// <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);
}
</code></pre>
<p>设计原理:</p>
<ul>
<li>
<p>首先要验证的属性的值;</p>
</li>
<li>
<p>调用这个特性的 <code>IsValid</code> 方法,检查值是否通过验证;</p>
</li>
<li>
<p>获取自定义的验证失败消息;</p>
</li>
<li>
<p>返回验证结果;</p>
</li>
</ul>
<h3 id="35-实现解析功能">3.5 实现解析功能</h3>
<p>我们要实现一个功能:</p>
<p>解析对象的所有属性,逐一对属性进行检索,使用到我们设计的自定义验证特性的属性,就执行检查,去获取验证结果。</p>
<pre><code class="language-c#"> /// <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("########对象分割线########");
}
}
</code></pre>
<p>设计原理:</p>
<p>上面有三个循环,第一个是没什么意义;</p>
<p>因为我们的参数对象是一个对象列表,批量验证对象,所以需要逐个对象进行分析;</p>
<p>第二个循环,是逐个获取属性;</p>
<p>第三个循环是逐个获取属性的特性;</p>
<p>上面消息获取完毕,即可开始进行验证。</p>
<p>这里必须拿到三个参数:</p>
<ul>
<li>实例化的对象:反射的基础是元数据,反射操作的基础是实例对象;</li>
<li>类型的属性 PropertyInfo :要通过 PropertyInfo 获取到实例对象的属性值;</li>
<li>特性对象 Attribute:从实例对象中获取到的特性 Attribute 对象;</li>
</ul>
<h3 id="36-编写一个模型类">3.6 编写一个模型类</h3>
<p>我们编写一个模型类型,来使用自定义的验证特性</p>
<pre><code class="language-c#"> public class User
{
public int Id { get; set; }
public string Name { get; set; }
public long Phone { get; set; }
public string Email { get; set; }
}
</code></pre>
<p>使用方法跟 EFCore 的差不多,非常简单。</p>
<p>你也可以多创建几个模型类进行测试。</p>
<h3 id="37-执行验证">3.7 执行验证</h3>
<p>我们来实例化多个模型类并设置值,然后调用解析功能进行验证。</p>
<p>在 Main 功能加上以下代码:</p>
<pre><code class="language-c#"> 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);
</code></pre>
<p>如无意外,执行结果应该是这样的</p>
<pre><code class="language-c#">检查对象属性是否通过检查
属性: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 验证
*****属性分割线******
########对象分割线########
</code></pre>
<h3 id="38-总结">3.8 总结</h3>
<p>通过七篇文章的示例,估计你已经学会了反射的基础操作和应用了吧?</p>
<p>本篇文章实现了特性的应用。</p>
<p>单纯学会 “自定义特性” ,没有卵用,要学会如何利用特性去实现业务,才有用处。</p>
<p>本篇对特性的使用, ORM 、ASP.NET Core 等都有常见的应用。</p>
<p>第六篇的时候,我们实现了简单的依赖注入和 Controller / Action 导航,利用本篇的内容,可以修改第六篇实现的代码,增加一个路由表的功能,访问 URL 时,不需要通过 <code>{/Controller/Action}</code> 的路径去访问,可以随意映射 URL 规则。</p>
</div>
<div id="MySignature" role="contentinfo">
痴者工良(https://whuanle.cn)<br><br>
来源:https://www.cnblogs.com/whuanle/p/12182962.html
頁:
[1]