C#如何创建可向类或方法注入代码的自定义特性
首先直接给结论:仅靠自定义特性+反射,完全无法实现Fody那种无侵入修改原类代码的效果,你查到的「特性本身只是元数据,无外部配合时不产生任何作用」是完全准确的。
你举的Fody精简INotifyPropertyChanged实现的例子,标记特性的代码如下:
[ImplementPropertyChanged] public class MyClass : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; public string MyProperty { get; set; } }
编译后最终生成的代码逻辑,和你手动编写的完整版本完全一致:
public class MyClass : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; private string myProperty; public string MyProperty { get { return myProperty; } set { if (Equals(myProperty, value)) return; myProperty = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MyProperty))); } } }
识别特性的外部代码的三类放置位置
所有依赖特性生效的逻辑,本质都是「主动扫描元数据+执行对应处理」的代码,根据执行时机不同,会放在三个完全不同的位置:
- 运行时管线:最常见的场景,比如ASP.NET Core、WPF框架本身的运行时代码,或者你自己写的应用启动逻辑,在程序运行到对应阶段时,通过反射扫描程序集中标记了目标特性的成员,执行注册、校验、调用等逻辑。
- 编译流程钩子:代码生成/IL改写逻辑会放在这里,比如Roslyn源生成器、Fody这类IL编织工具,都是在编译过程中执行,还没生成最终可执行文件的时候就完成了代码处理。
- 设计时扩展:比如Visual Studio的WPF设计器、Roslyn分析器的代码提示逻辑,在你写代码的时候就会扫描特性,给出提示或者调整设计时渲染效果。
反射的能力边界:为什么实现不了Fody的效果
反射是纯运行时的能力,它只能做到:
- 读取程序集里的特性元数据
- 动态创建类型实例、调用成员、访问私有字段/方法
- 在运行时动态生成新的类型(比如动态代理类)
但反射永远无法修改已经编译完成的原有类型的IL代码。你看到很多AOP框架用反射+动态代理实现类似的拦截效果,本质是运行时生成了一个继承原类的代理子类,重写虚属性/虚方法的逻辑插入拦截代码,这种方案有很明显的局限:
- 要求被拦截的属性/方法必须标记为
virtual - 必须通过代理类创建实例,直接
new MyClass()得到的原类实例不会有任何拦截逻辑 - 有一定的运行时性能开销
和Fody那种直接修改原类代码、不管怎么创建实例都能生效、零运行时开销的效果有本质区别。
Fody这类IL注入功能的实现原理
你看到的[ImplementPropertyChanged]特性,背后的处理逻辑完全是在编译阶段执行的,大致流程如下:
- Fody作为MSBuild的一个Task,被插入到C#编译流程中:当C#编译器把代码编译成IL临时程序集之后,还没生成最终dll/exe之前,Fody就会启动执行
- 遍历临时程序集中的所有类型,扫描标记了目标特性的类
- 解析这些类的IL代码,找到所有自动实现的公共属性,拆解自动属性生成的隐藏后台字段、getter/setter逻辑
- 修改setter的IL代码:插入新旧值对比逻辑,在给后台字段赋值之后,插入触发
PropertyChanged事件的IL指令,和你手动写的带事件触发的属性代码完全一致 - 把修改完成的IL写回最终生成的程序集
你最终部署运行的dll里,根本不存在原始的自动属性,已经是完整的带事件触发的属性实现,运行时没有任何反射或者代理的开销。
除了IL编织,现在C#官方提供的Roslyn源生成器也能实现类似的效果:它不需要改写IL,而是在编译阶段扫描标记了自定义特性的partial类,直接生成额外的partial类代码,把属性的实现、事件触发逻辑自动补全,效果和Fody接近,调试更简单,是目前更推荐的自定义实现方案。
自定义实现同类功能的可选路径
如果不想依赖第三方Fody包,你可以根据自己的需求选两种实现方案:
- 追求简单快速:用动态代理库+反射实现,开发成本低,适合业务逻辑简单、能接受虚方法限制的场景
- 追求无运行时开销:写Roslyn源生成器,编译期扫描自定义特性,自动生成INotifyPropertyChanged的相关实现,没有任何运行时代价,兼容性更好
不建议自己从零实现IL编织器,需要精通ECMA-335 IL规范,处理各种边界case的成本极高。
内容的提问来源于stack exchange,提问作者TheMystic6

