AOP - C# 中的方法和属性拦截






4.95/5 (18投票s)
本教程展示了 Cauldron.Interception.Fody 的用法及其功能。
引言
AOP 在 Java 世界中被广泛使用,但在 .NET 世界中却不那么普遍。实际上,.NET 中有很多 AOP 框架,但这些框架大多是代理,并且使用起来并不直观。
在本教程中,我将介绍 Fody 插件 Cauldron.Interception.Fody。Cauldron.Interception.Fody 提供了基本的拦截器来拦截方法、属性和构造函数,其功能旨在消除样板代码。
背景
早在 2015 年,我就被派去为一个已经运行了很长时间的大型应用程序添加非侵入式日志记录。对我来说,只有一种实现方式:拦截器。那时我已经知道 PostSharp、Fody+Plugins、Spring.NET 和 Castle。但我想要的是 IL-weavers,而不是代理。IL-weavers 在构建程序集时会修改代码,而不是在运行时创建和继承被拦截的类。经过几天的搜索 PostSharp 的替代品,我得出结论,没有。别误会,PostSharp 很棒,但它不是免费的(是的……有免费版本,但有局限性)。
不过,对于那个项目,我最终使用了 PostSharp,但缺乏真正的替代品促使我创建了自己的。自 2017 年初以来,我已将其作为 Nuget 包提供。
获取 Cauldron.Interception.Fody
您可以直接从 Nuget 库或从 Visual Studio Nuget 管理器中获取此插件。
支持的 .NET 版本
Cauldron.Interception.Fody 的当前版本支持 NET45、NETStandard 和 UWP。
创建你的第一个方法拦截器
在此示例中,我们将创建一个简单的 AOP 方法拦截器,用于记录方法的执行。
方法拦截器必须实现 'IMethodInterceptor
' 接口并继承 'Attribute' 类。
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] public class LoggerAttribute : Attribute, IMethodInterceptor { public void OnEnter(Type declaringType, object instance, MethodBase methodbase, object[] values) { this.AppendToFile($"Enter -> {declaringType.Name} {methodbase.Name} {string.Join(" ", values)}"); } public void OnException(Exception e) { this.AppendToFile($"Exception -> {e.Message}"); } public void OnExit() { this.AppendToFile("Exit"); } private void AppendToFile(string line) { File.AppendAllLines("log.txt", new string[] { line }); Console.WriteLine(">> " + line); } }
在此示例中,我们将创建一个名为 'Add' 的方法,并用 'LoggerAttribute
' 进行修饰。
[Logger] private static int Add(int a, int b) { return a + b; }
现在,每次调用 'Add
' 方法都会在 'log.txt' 中进行记录。
控制台还将显示以下内容
这是如何工作的?
编织器会修改你的程序集,并将拦截器添加到你的 'Add
' 方法中。生成的代码将如下所示
private static LoggerAttribute loggerAttribute; private static int Add(int a, int b) { if(loggerAttribute == null) loggerAttribute = new LoggerAttribute(); try { loggerAttribute.OnEnter(typeof(Program), null, methodof(Add), new object { a, b }); return a + b; } catch(Exception e) { loggerAttribute.OnException(e); throw; } finally { loggerAttribute.OnExit(); } }
修改发生在你的程序集的 IL 代码中,在构建期间。这意味着你不会在你的代码中看到任何修改。
介绍 AssignMethodAttribute
大家都知道 C# 中的自动属性。
public string MyProperty { get; set; }
但是,如果您想在 setter 中调用事件怎么办?
在这种情况下,您需要像这样实现 getter 和 setter:
private string _myProperty; public string MyProperty { get { this._myProperty; } set { this._myProperty = value; this.MyPropertyChanged?.Invoke(); } }
如果能这样做,是不是会更简单?
[OnPropertySet] public string MyProperty { get; set; } private void OnMyPropertySet() { this.MyPropertyChanged?.Invoke(); }
下面的属性拦截器示例实现了使用 AssignMethodAttribute
来调用关联的方法。在上面的例子中,关联的方法是 'OnMyPropertySet
' 方法。
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public sealed class OnPropertySetAttribute : Attribute, IPropertySetterInterceptor { [AssignMethod("On{Name}Set", false)] public Action onSetMethod = null; public void OnException(Exception e) { } public void OnExit() { } public bool OnSet(PropertyInterceptionInfo propertyInterceptionInfo, object oldValue, object newValue) { this.onSetMethod?.Invoke(); return false; } }
AssignMethodAttribute
修饰了字段 'onSetMethod
'。字段类型 'Action
' 描述了方法的返回类型和参数。在这种情况下,关联的方法必须是 void 且无参。
AssignMethodAttribute
的第一个参数是关联方法的名称,而 '{Name}
' 是一个占位符,将被属性的名称替换。例如,如果拦截器正在修饰名为 'OrderData
' 的属性,那么编织器将查找一个名为 'OnOrderDataSet
' 的方法。
AssignMethodAttribute
的第二个参数告诉编织器,如果找不到描述的方法,则抛出错误。
有时,所有属性都在调用其 setter 中的同一个事件。一个著名的例子是 WPF 中的 PropertyChanged
事件。
让我们修改属性拦截器以接受构造函数中的方法名。
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] public sealed class OnPropertySetAttribute : Attribute, IPropertySetterInterceptor { [AssignMethod("{CtorArgument:0}", true)] public Action<string> onSetMethod = null; public OnPropertySetAttribute(string methodName) { } public void OnException(Exception e) { } public void OnExit() { } public bool OnSet(PropertyInterceptionInfo propertyInterceptionInfo, object oldValue, object newValue) { this.onSetMethod?.Invoke(propertyInterceptionInfo.PropertyName); return false; } }
您可能已经注意到,'On{Name}Set
' 被替换为 {CtorArgument:0}
。{CtorArgument:0}
占位符将被编织器替换为构造函数参数的值。在这种情况下,索引 0 是名为 'methodName
' 的参数。
委托 Action 类型也更改为 Action<string>
,以便能够传递属性的名称。
以下代码演示了拦截器的用法
[OnPropertySet(nameof(RaisePropertyChanged))] public string DispatchDate { get; set; } [OnPropertySet(nameof(RaisePropertyChanged))] public string OrderDate { get; set; } private void RaisePropertyChanged(string propertyName) { this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }
现在,每次向被修饰的属性赋新值时,都会调用 'RaisePropertyChanged
' 方法。这比在每个 setter 中调用 'RaisePropertyChanged
' 方法要实用得多,但仍然不方便,因为它必须添加到每个属性中。
让我们修改属性拦截器的 AttributeUsage
,并向属性的目标添加 'Class
'。
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] ...
现在,可以将拦截器应用于类,它将拦截类中的所有属性。
[OnPropertySet(nameof(RaisePropertyChanged))] public class CoolViewModel { public string DispatchDate { get; set; } public string OrderDate { get; set; } public int OrderCount { get; set; } private void RaisePropertyChanged(string propertyName) => this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); }
结论
IL-编织拦截是一项非常有用的工具,可以帮助您最小化代码,使其更具可读性和可维护性,同时不会对应用程序的性能造成太大影响。像所有事物一样,过度使用拦截器会导致适得其反。如果一切都自动发生,就很难理解代码中发生了什么。所以要明智地使用它。
虽然日志记录可能是拦截器最明显的用途(事实上,我在网上看到的几乎所有示例都与日志记录有关),但在涉及大量样板代码的情况下,拦截的潜力才能得到充分发挥。为了让您了解我个人如何使用拦截器,以下是我目前使用的一些拦截器的列表:
PerformanceLoggerAttribute
- 一个属性和方法拦截器,记录执行性能。OnDemandAttribute
- 一个属性拦截器,在调用 getter 时加载其值;一种懒加载。DBInsureConnectionAttribute
- 一个方法拦截器,检查数据库连接,并在未连接时自动建立连接。RegistryValueAttribute
- 一个属性拦截器,在调用 getter 或 setter 时获取或设置一个定义的注册表值。PriviledgeAttribute
- 一个方法拦截器,检查用户是否具有执行方法的必要权限;否则抛出异常。- RegisterChildrenAttribute - 一个属性拦截器,将属性值注册到声明的 ViewModel。