C#的Attribute如何为代码添加元数据?

来源:程序开发作者:云朵头衔:草根站长
导读:本期聚焦于云朵创作的《C#的Attribute如何为代码添加元数据?》,敬请观看详情。把Attribute理解成贴在代码元素上的标签,听起来简单,但它和注释、配置文件有本质区别。Attribute会随程序集一起编译,进入元数据表,运行时能被反射读取,框架也因此可以在不修改调用代码的前提下完成验证、序列化、依赖注入等通用能力。本文先从一个自定义特性入手,说明声明方式、AttributeUsage约束、构造函数参数和命名参数的差异,再结合反射实现一个轻量级模型校验器。随后对比Attribute与接口、配置中心的适用边界,并指出AllowMultiple、Inherited容易踩到的细节。读懂这些之后,再去分析ASP.NET Core中的路由特性或System.Text.Json的序列化特性,就能理解背后的元数据驱动逻辑。

C# 中的 Attribute 并不是用来直接完成业务流程的,它更像一组随程序集一起编译的声明式描述。无论是给类打上 Serializable,还是给属性加上 Required,编译器都会把这些标记写入模块的元数据中。运行阶段通过反射读取这些信息,调用方就能把通用逻辑和业务模型解耦。这种能力在框架设计里尤其重要,因为框架作者通常无法预知使用者的类型结构,只能依靠元数据来驱动行为。

C#的Attribute如何为代码添加元数据?

很多人第一次接触 Attribute 时会把它当成一种特殊注释,其实二者完全不同。注释在编译时会被丢弃,而 Attribute 会持久化在程序集里。注释只能给人看,Attribute 则是给程序看的。正是由于这个差别,序列化框架才能根据某个属性上的标记改变输出名称,ORM 才能根据字段上的标记决定列的类型和长度。下面通过自定义特性、反射读取和实际校验三个环节把整个链路拆开。

Attribute如何进入程序集元数据

自定义特性本质上是一个继承自 System.Attribute 的类。类名习惯以 Attribute 结尾,使用时可以省略后缀。编译器看到方括号里的特性标记后,会实例化对应的特性类型,并把构造函数参数与命名参数写入程序集的元数据表。这个过程发生在编译期,而不是运行期,因此特性里不能包含随运行时变化的状态,也不能依赖外部服务。

下面定义一个简单的接口信息特性:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ApiInfoAttribute : Attribute
{
    public string Version { get; }

    public string Description { get; set; }

    public ApiInfoAttribute(string version)
    {
        Version = version;
    }
}

在类和方法上使用它:

[ApiInfo("1.0", Description = "订单服务接口")]
public class OrderService
{
    [ApiInfo("2.0", Description = "创建订单")]
    public void Create(Order order)
    {
        // 业务逻辑
    }
}

这里的 "1.0" 是位置参数,对应构造函数 ApiInfoAttribute(string version)。Description 是命名参数,对应类中的公共可写属性。编译器会记录这些值,但不会主动触发任何行为。特性只有在某个框架或代码主动通过反射读取时才有意义。换句话说,Attribute 本身不做事,真正做事的是读取它的那部分代码。

运行阶段可以使用反射获取这些标记:

var type = typeof(OrderService);
var attribute = type.GetCustomAttributes(typeof(ApiInfoAttribute), true)
                    .FirstOrDefault() as ApiInfoAttribute;

if (attribute != null)
{
    Console.WriteLine(attribute.Version);
    Console.WriteLine(attribute.Description);
}

从元数据到反射读取的完整链路就是:声明特性类、应用到代码元素、编译器写入元数据、运行时通过反射还原实例。这个机制让框架可以在不修改使用者代码的前提下,获得关于类型、方法、属性的额外信息。

AttributeUsage的三个关键选项

自定义特性通常会标注 AttributeUsage,这是特性作用于类本身的特性,用来约束使用范围。参数 AttributeTargets 是一个枚举组合,表示该特性可以贴在哪些目标上,比如 Class、Method、Property、Field、Parameter 等。如果不写 AttributeUsage,默认可以用于所有目标,但明确限制有助于在编译期发现误用。

第二个选项是 AllowMultiple,默认值为 false。也就是说同一个目标上默认只能出现一次同一个特性,重复使用会直接编译报错。如果希望一个类被打上多个标签,可以把 AllowMultiple 设为 true。例如:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public class TagAttribute : Attribute
{
    public string Name { get; }
    public TagAttribute(string name)
    {
        Name = name;
    }
}

[Tag("order")]
[Tag("core")]
public class OrderService
{
}

读取多个实例时,可以用 GetCustomAttributes 返回数组并遍历:

var tags = typeof(OrderService).GetCustomAttributes(typeof(TagAttribute), true);

foreach (TagAttribute tag in tags)
{
    Console.WriteLine(tag.Name);
}

第三个选项是 Inherited,它决定派生类是否会继承基类上的特性。默认值为 true,意味着特性会沿着继承链向下传递。但需要注意,这个继承规则在反射时还受 GetCustomAttributes 方法的第二个参数影响。如果调用时传入 false,即使特性本身设置了 Inherited = true,也不会读取继承来的特性。对于方法重写、属性重写的情况,继承行为可能会和直觉不太一样,具体表现取决于目标类型以及反射时的参数组合。

构造参数和命名参数也需要分清。位置参数必须按照构造函数参数顺序提供,并且必须全部给出。命名参数放在位置参数之后,对应类中公共字段或可写属性。只有 public 的字段或包含 setter 的属性才能作为命名参数。特性参数类型有明确限制,通常只能是基础类型、string、Type、枚举、object 以及这些类型的一维数组。不能把任意复杂对象作为特性参数,因为编译器需要在编译期确定并存储这些值。

用反射实现一个轻量级模型校验器

Attribute 最常见的落地场景之一就是数据校验。与其在每个服务方法里手动写一串 if 判断,不如把校验规则声明在模型字段旁边,再交给一个通用校验器统一执行。先定义两个校验特性:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class RequiredAttribute : Attribute
{
}

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class StringLengthAttribute : Attribute
{
    public int MaxLength { get; }
    public StringLengthAttribute(int maxLength)
    {
        MaxLength = maxLength;
    }
}

然后在模型类上标注:

public class Customer
{
    [Required]
    [StringLength(20)]
    public string Name { get; set; }

    [StringLength(100)]
    public string Address { get; set; }
}

校验器通过反射遍历属性,读取每个属性上的特性并执行对应规则:

using System.Collections.Generic;
using System.Linq;
using System.Reflection;

public static class Validator
{
    public static List<string> Validate(object value)
    {
        var errors = new List<string>();

        foreach (var property in value.GetType().GetProperties())
        {
            var propertyValue = property.GetValue(value);

            var required = property.GetCustomAttributes(typeof(RequiredAttribute), true).FirstOrDefault();
            if (required != null && (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString())))
            {
                errors.Add(property.Name + " 不能为空");
            }

            var length = property.GetCustomAttributes(typeof(StringLengthAttribute), true)
                                  .FirstOrDefault() as StringLengthAttribute;
            if (length != null && propertyValue != null)
            {
                if (propertyValue.ToString().Length > length.MaxLength)
                {
                    errors.Add(property.Name + " 长度不能超过 " + length.MaxLength);
                }
            }
        }

        return errors;
    }
}

这个 Validator 不依赖具体的 Customer 类,任何标注了 Required 和 StringLength 的模型都可以复用它。约束信息和校验逻辑被拆开了,模型只负责描述自身的数据形状,基础设施负责解释这些描述。这正是元数据驱动编程的核心好处。

当然,反射不是没有代价。频繁反射读取属性信息会带来一定性能损耗,如果请求量很大,应该缓存 PropertyInfo 和特性实例。可以预先构建一个静态元数据缓存,用类型作为 key,保存每个属性对应的校验特性列表,避免每次校验都重新反射。.NET 内置的 System.ComponentModel.DataAnnotations 其实已经实现了类似机制,但自己写一遍能更清楚地看到 Attribute 与反射是如何配合工作的。

Attribute与接口、配置中心的适用边界

Attribute 适合描述静态的、与代码结构紧密相关的元数据。它一旦编译进程序集,就不能在运行时随意改变。如果某个阈值或开关经常调整,把它写成 Attribute 就意味着每次修改都要重新编译部署,这时配置中心或数据库更合适。例如一个限流阈值如果与业务活动有关,就应该放进配置系统,而不是写成 [RateLimit(50)] 这样的硬编码。

接口表达的是能力契约,Attribute 表达的则是声明式约束和横向关注点,两者并不互斥。一个类既可以实现 IOrderService,同时也可以被打上 [ApiInfo("1.0")]。接口保证方法签名一致,Attribute 则给基础设施提供更细粒度的描述信息。ASP.NET Core 中的 [HttpGet] 就是一个典型例子,它同时标记了 HTTP 方法和路由模板,框架启动时扫描控制器并构建路由表,整个过程不需要开发者额外写注册代码。

还要注意 Attribute 类自身不应该承载复杂业务逻辑。因为特性实例通常由反射创建,构造函数里不适合访问数据库、网络或容器服务。如果发现某个特性类里开始出现大量行为代码,往往是设计边界出了问题。Attribute 应当保持为一份轻量描述,真正的处理逻辑应放在读取它的框架或服务中。这样职责清晰,测试和替换也会更容易。

总的来说,Attribute 的核心价值在于把不变的信息从业务逻辑中剥离,交给基础设施统一解读。声明、读取、约束三个环节理解清楚之后,再去看 ASP.NET Core 的特性路由、System.Text.Json 的序列化选项,或自己设计一套元数据驱动机制,都会更加顺畅。

C# Attribute元数据反射修改时间:2026-10-01 07:51:57

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1001/64156.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。