Attribute 不能被注入,仅是编译后存于元数据的被动标签;需通过反射读取并配合过滤器、AOP等机制主动触发逻辑,且构造函数参数必须为编译期常量。

Attribute 本身不能被“注入”,它只是元数据
很多人以为 Attribute 能像 DI 容器里的服务一样被“注入”到构造函数或方法参数里——不行。C# 的 Attribute 编译后只存为元数据,运行时不会自动实例化、更不会参与依赖解析。你看到的“注入效果”,其实是靠反射 + 手动提取 + 配合其他机制(比如 ASP.NET Core 的过滤器、EF Core 的模型配置)间接实现的。
所以别搜“如何注入 Attribute”,该搜的是“怎么在运行时读取并用好它”。常见错误是直接把 [MyAttr] 往参数上一写,结果编译报错:Attribute 'MyAttr' is not valid on this declaration type——因为没搞清 Attribute 的 AttributeTargets 限制。
- 定义自定义
Attribute时,必须显式指定AttributeUsage,比如:[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
- 不加
AttributeUsage默认只允许打在类上,打在方法/属性/参数上会编译失败 -
AllowMultiple = true才能在一个目标上重复使用同一特性,否则第二次加会报错
读取 Attribute 必须用反射,且注意性能开销
运行时获取 Attribute 唯一可靠方式是调用 GetCustomAttribute 或 GetCustomAttributes。但反射慢,尤其高频调用(如 Web 请求中每请求查一次)容易成为瓶颈。
典型误用:在 Action 方法里每次调用都去 GetType().GetMethod(...).GetCustomAttribute —— 这不是不能用,而是不该裸用。
- 优先缓存结果:用
ConcurrentDictionary存已查过的值 - ASP.NET Core 中,推荐走
IActionFilter或IAuthorizationHandler,它们天然在 pipeline 里被框架缓存和复用 - EF Core 模型配置里用
[Column]这类特性,是框架在OnModelCreating阶段一次性反射读取并缓存,不是每次 SaveChanges 都重读
想让 Attribute “生效”,得自己写触发逻辑
加了 [LogExecutionTime] 不代表方法就自动计时;加了 [ValidateModel] 不代表校验就跑起来。Attribute 是被动的标签,真正干活的永远是你写的拦截器、过滤器、AOP 织入点或初始化代码。
比如实现一个简单日志特性:
[AttributeUsage(AttributeTargets.Method)]
public class LogExecutionTimeAttribute : Attribute { }
它什么也不做。要让它起效,你还得:
- 写一个
ActionFilterAttribute(ASP.NET Core)或IAopAdvice(用 Castle DynamicProxy) - 在
OnActionExecuting里调用context.ActionDescriptor.GetCustomAttribute() - 如果存在,才启动
Stopwatch,并在OnActionExecuted里记录耗时 - 别忘了处理异步方法:
OnActionExecutionAsync才是正确入口点
Attribute 构造函数参数只能是常量表达式
这是编译期硬限制。你想传一个 ILogger 或 IConfiguration 进 Attribute?不可能。所有构造函数参数、属性赋值,必须是编译时可确定的值。
常见翻车现场:
-
[MyAttr(LogLevel.Warning)]✅ 可以(枚举字面量) -
[MyAttr("user_id")]✅ 可以(字符串字面量) -
[MyAttr(MyConstants.UserIdKey)]✅ 可以(const 字段) -
[MyAttr(_logger)]❌ 编译失败(变量、实例、方法调用都不行) -
[MyAttr(typeof(UserService))]✅ 可以(typeof是特许的编译期操作)
需要运行时动态值?别塞进 Attribute 构造函数,改用属性(public string ConfigKey { get; set; }),然后在反射读取后,再通过 DI 容器解析对应服务来补全逻辑。
最常被忽略的一点:Attribute 类里所有公开成员都必须是可序列化的,且不能有复杂初始化逻辑——它可能被跨 AppDomain 或跨进程反序列化(比如在某些远程调试或测试框架里),构造函数里 new 对象、访问静态字段、读配置,都可能在意外时机崩掉。











