在.NET生态中,面向切面编程(AOP)并不像在Java中那样流行。尽管存在许多AOP框架,但它们大多基于代理(proxy)模式,使用起来并不直观。本文将介绍一个名为Cauldron.Interception.Fody的Fody插件,它提供了基本的拦截器来拦截方法、属性和构造函数,旨在消除样板代码。
背景:2015年,被要求为一个已经维护和运行多年的大型应用程序添加非侵入性日志记录功能。对来说,实现这一功能的唯一可能就是使用拦截器。当时已经知道PostSharp、Fody+Plugins、Spring.NET和Castle,但需要的是IL-weavers,而不是代理。IL-weavers在构建程序集时操作代码,而不是在运行时创建和继承被拦截的类。经过几天的搜索PostSharp的替代品,得出结论:没有其他选择。不要误会,PostSharp很棒,但它不是免费的(是的,有一个免费版本,但它有限制)。
对于那个项目,最终使用了PostSharp,但缺乏真正的替代品激励创建自己的。从2017年初开始,将其作为Nuget包提供。
获取Cauldron.Interception.Fody:可以直接从Nuget Gallery或Visual Studio Nuget Manager获取这个插件。
支持的.NET版本:当前版本的Cauldron.Interception.Fody支持NET45、NETStandard和UWP。
创建第一个方法拦截器:在这个示例中,将创建一个简单的方法拦截器来记录方法的执行。
方法拦截器必须实现'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
{
return 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{PropertyName}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 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{PropertyName}Set'被更改为'{CtorArgument:0}'。'{CtorArgument:0}'占位符将被编织器用构造函数参数的值替换。在这种情况下,索引0是名为'methodName'的参数。
委托类型Action也被更改为Action
以下代码演示了拦截器的使用:
[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;
}
public void RaisePropertyChanged(string propertyName) =>
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
结论:IL编织拦截是一个非常有用的工具,可以帮助最小化代码,使其更易于阅读和维护,同时不会过多地影响应用程序的性能。像其他任何事情一样,过度使用拦截器会导致相反的结果。如果一切都自动发生,代码将很难理解。所以请明智地使用它。