本篇目录
本节的源码本人已托管于Coding上:点击查看。
本文实验环境:VS 2017 Community。
要有效地使用AOP,AOP自己的架构及其对大代码架构的影响是要理解的重要概念。 当你在设计和实现一个架构时,PostSharp可以快速且自动地帮助你在编译时识别错误。
直到现在,我们一直在狭隘地研究PostSharp和AOP:一次一个切面和一个类。现在从架构师的角度, 来看看PostSharp是如何与整个系统配合在一起的。 PostSharp包含了使架构师工作更简单的工具,以及确保各切面本身都具有良好架构的工具。
在某些时候关于PostSharp,你可能关心的一件事就是我的所有例子都是将特性放在独立的方法和属性上,这也许看起来很繁琐和重复,如果你在一个大的代码库中也必须这么做的话,确实很重复且繁琐。 幸运的是,PostSharp并不要求你始终这样做。 接下来会看看多播切面特性的方式,以便我们可以重用切面且不需要太多的特性重复。
因为PostSharp是作为编译时工具实现的,它为我们开辟了编写可以在正常编译时间之后立即运行的代码的大门。 我们可以利用这个机会编写代码,用于验证:切面正在正确的地方使用,并且不会在运行时引起问题,以及整个项目的结构和架构。 这种方法会使得早早发现问题(或者我喜欢称之为失败更快,或最早失败)。
我们也将借此机会执行切面的初始化。 如果你有昂贵的操作(如使用Reflection),那么最好地让它在构建期间就完成不要等到运行时。
编译时初始化和验证
我们来看一下上一章的PostSharp构建过程,看看它如何适应普通的.NET构建过程。 回想一下,你有编译时阶段(代码编译成CIL)和运行阶段(其中CIL被编译为及时被执行的机器指令),见下图。 编译时的AOP工具,如PostSharp,又增加了一个步骤(后编译器),并在编译之后但在执行之前修改CIL。
PostSharp为你编写的每个方面执行几个步骤。 每个方面都是使用aspect的构造函数实例化。 PostSharp然后执行验证步骤(调用你切面的CompileTimeValidate方法)来检查切面是否正在正确使用。 然后PostSharp执行一个初始化步骤(调用切面的CompileTimeInitialize方法)来执行任何昂贵的计算,而不是等到运行时。 最后,PostSharp会获得这个切面实例并将其序列化(到二进制流),以便可以稍后在运行时进行反序列化和执行。
下面看一下后期编译器的详细图解,对应上面这段话的解释:
本节重点介绍该过程的验证和初始化步骤。 直到这个章节中,为了保持简单,在例子中,我没有定义任何CompileTimeValidate或CompileTimeInitialize代码。 所有这些步骤仍然会执行,但是因为我们没有定义CompileTimeValidate或CompileTimeInitialize,那些步骤没有做任何事情。
使用CompileTimeValidate,PostSharp可以让我们更快地失败。 如果我们可以在编译时验证某些事情,那么就不必等到运行时才去发现错误或者获得异常。CompileTimeInitialize可以让我们提前处理代价高昂的事情。 如果在运行程序之前我们可以处理一个(可能代价高昂的)操作,那么就尽可能处理吧。
编译时初始化
通常,一个切面需要知道一些关于它被用在了哪里的信息,例如方法名称,一些参数信息或其他信息。所有这些信息可以由PostSharp API提供,该API使用Reflection填充这些信息到你的切面(例如,args参数)。 Reflection信息是我见过的初始化中最常见的事情,但是在整个程序执行之前可以获得和实例化的其他任何昂贵的信息也是必须处理的,还是那句话,能在编译时去做的,就不要等到运行时。
快速看个例子,我们来看一下基本的日志记录切面(之前的章节中已经演示多次了)。 不是只记录一个字符串,而是记录它的方法名称。 如果我使用PostSharp 的OnMethodBoundaryAspect,并在运行时这样做,代码如下:
using PostSharp.Aspects;
using System;
namespace AopInitialAndValidate
{
[Serializable]
public class MyLoggingAspect:OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionArgs args)
{
Console.WriteLine($"The method name is {args.Method.Name}");
}
}
}
using static System.Console;
namespace AopInitialAndValidate
{
class Program
{
static void Main(string[] args)
{
var mm = new MyClass();
mm.MyMethod();
Read();
}
}
class MyClass
{
[MyLoggingAspect]
public void MyMethod()
{
WriteLine("Now in MyMethod...");
}
}
}
如果你检查args.Method,你会注意到它是一个类型为MethodBase的对象,位于System.Reflection中。 虽然这对我们的例子来说关系并不大,因为只有一个边界方法,但是在大型应用程序中,每次切面都使用反射会使得性能大打折扣。
但是在运行时付出这个性能代价是不必要的。 方法名称在程序运行时不会改变,所以为什么不在PostSharp的后期编译过程中,将方法的名称在编译时确定下来呢? 在下面,我们覆盖此切面的CompileTimeInitialize并将该方法名称存储在私有字符串字段中的。 在OnEntry中,使用该字符串字段而不是args.Method.Name。
using System.Reflection;
namespace AopInitialAndValidate
{
[Serializable]
public class MyLoggingAspect:OnMethodBoundaryAspect
{
private string _methodName = string.Empty;
public override void OnEntry(MethodExecutionArgs args)
{
//这次直接使用私有字段而不是反射
Console.WriteLine($"The method name is {_methodName}");
}
public override void CompileTimeInitialize(MethodBase method, AspectInfo aspectInfo)
{
_methodName = method.Name;//存储方法名称到私有字段
}
}
}
PostShrp 许可
关于PostSharp许可的一个注意事项:如果你使用Express(免费)版本,那么这个初始化对性能没有太大的影响,因为它没有进行任何优化,args.Method始终使用Reflection来填充OnEntry。 但是,遵循使用CompileTimeInitialize这一实践仍然是个好主意,以防你最终需要AspectShaper的完整版本可以提供的切面优化。
使用Reflection只是在编译时可以执行的一种操作而不是运行时。 如果要执行其他缓慢或昂贵的操作,那么应该在CompileTimeInitialize中进行处理。
因为PostSharp已经在编译时运行这个初始化代码,为什么不借此机会在运行一些这个切面的验证代码呢?
切面验证的正确用法
在PostSharp中使用CompileTimeValidate可以让我们检查切面应用到具体代码的上下文,以及确保在运行时正确运行。要使用CompileTimeValidate,只需要在切面中重写该方法。
下面新建一个控制台项目,添加postsharp nuget包,代码如下:
using System;
using PostSharp.Aspects;
namespace AopValidate
{
[Serializable]
public class MyLocationAspect:LocationInterceptionAspect
{
public override void OnGetValue(LocationInterceptionArgs args)
{
//当读取属性值时会打印
Console.WriteLine("Now in getter of property");
args.ProceedGetValue();
}
}
}
using static System.Console;
namespace AopValidate
{
class Program
{
[MyLocationAspect]
public static string MyName { get; set; }
static void Main(string[] args)
{
MyName = "farb";
Read();
}
}
}
但是,我这里有个奇怪的需求,只有当属性名称为“farb”(忽略大小写)时,才能给该属性使用该切面,否则,认为切面使用不当,这时就会编译失败。我将重写CompileTimeValidate方法(代码如下)并检查位置的名称。 如果不是“farb”,那么我将使用PostSharp API的Message类写出错误消息。
public override bool CompileTimeValidate(LocationInfo locationInfo)
{
if (!locationInfo.Name.ToLower().Equals("farb"))
{
//locationInfo是关于属性的反射信息
//Message是PostSharp提供的API
//SeverityType是一个定义消息严重级别的枚举
Message.Write(locationInfo, SeverityType.Error, "MyCompileErrorCode01", "The name of property must be farb,not case sentive");
return false;
}
return true;
}
此时,直接编译会报错,因为"MyName".ToLower().Equals("farb")
的值是false,因此会出现编译错误。farb,Farb,FarB等都是编译成功的。
我不会在这里全面地介绍这个PostSharp功能,但我会指出几件有趣的事情。 首先,请注意CompileTimeValidate返回一个bool。 如果CompileTimeValidate返回false,该切面将不会被应用那个指定的位置,即切面使用失效。 如果我把aspect属性放在100个属性上,而且只有一个被命名为“farb”,那么该切面只会被应用一次。
第二,请注意,我选择的SeverityLevel是“错误”。通过这样做,我告诉PostSharp编写一个编译器错误,因此Visual Studio也会当作错误处理。 如果我使用“警告”的严重级别,消息将显示为警告,并且项目会继续编译。 根据我的经验,警告经常被忽略,所以一般我喜欢坚持使用“错误”。另一个关于错误消息值得注意的是:后期编译器不会停止在它发现的第一个错误 , 它将继续处理各切面并写出每个消息。
最后,错误代码和消息可以是任何你想要的字符串。当PostSharp没在Message中包含行号和文件名时,将错误代码和消息描述的越详细并且尽可能让任何人遇到这些CompileTimeValidate错误可以轻松找到违规代码,是一个很好的实践。错误消息中包含完整类名,完整属性类型和属性名称,创建有意义的错误代码也是好的实践(当然,同时使用类似的信息方法切面再好不过了)。
现在你已经看到了一些CompileTimeInitialize和CompileTimeValidate的基本示例。 现在看一个真实的案例切面。
真实案例:复习线程
我们来回顾一下第3章中的线程示例。回想一下,我们创建了一个WorkerThread方面来等待另一个线程,以及一个UIThread切面,以确保
从该WorkerThread线程调用的任何UI代码都在UI线程上运行。 我们再来一个特别看看UIThread,并寻找这个切面可能引起的问题:
using PostSharp.Aspects;
using System;
using System.Windows.Forms;
namespace RevisitThread
{
[Serializable]
public class UIThread:MethodInterceptionAspect
{
public override void OnInvoke(MethodInterceptionArgs args)
{
var form =(Form) args.Instance;
if (form.InvokeRequired)
{
form.Invoke(new Action(args.Proceed));
}
else
{
args.Proceed();
}
}
}
}
这个方面取决于args.Instance是一个Form类型的对象。 如果不是,怎么办?然后,将args.Instance强制转换为Form将导致InvalidCastException(无效转换异常)抛出。 我们可以添加运行时检查以避免这种情况。 不使用强制转换,我们可以使用C# as
运算符来转换对象,然后检查以确保在尝试调用InvokeRequired和Invoke之前,该转换是有效的。 当使用as,如果该转换无法执行,那么返回一个null,也不会有异常抛出:
var form = args.Instance as Form;
if (form==null)
{
args.Proceed();
}
if (form.InvokeRequired)
{
form.Invoke(new Action(args.Proceed));
}
else
{
args.Proceed();
}
这样做至少不会使切面抛出异常。 但它仍然有问题:UIThread被用在没有从Form继承的类上怎么办?是否有一个新的团队成员
不熟悉线程工作的? 有人试图使用这个切面在Windows Forms之外的UI框架? 如果我们只在运行时检查,它只解决了异常问题,但其他问题呢?
不要拖延,马上使用这个切面处理这样的问题。 这次不使用as转换,我们来使用CompileTimeValidate以确保在编译时,UIThread切面始终用于Form类中的方法。 如果不是,我们会写一个有关UIThread正在被不正确使用的信息错误消息,并阻止构建成功。
public override bool CompileTimeValidate(MethodBase method)
{
//使用IsAssignableFrom检查DeclaringType是否从Form继承
if (typeof(Form).IsAssignableFrom(method.DeclaringType))
{
return true;
}
else
{
string errMsg = $"UIThread must be used in Form.[Assembly:{method.DeclaringType.Assembly.FullName},Class:{method.DeclaringType},method:{method.Name}]";
PostSharp.Extensibility.Message.Write(
method,
SeverityType.Error,
"UIThreadFormError01",
errMsg
);
return false;
}
}
在该切面验证失败的地方,我写了一个解释错误的消息,包括程序集,类和方法名称。 你还可以写出参数或你认为可能的任何其他有助于找到问题的信息。
分不清哪一个Message?
如果你正在使用Windows Forms应用程序,请确保你不要将System.Windows.Forms.Message与PostSharp.Extensibility.Message混淆。为了清楚起见,我在上一个例子中写了全命名空间类名。
现在,不必等待程序崩溃或其他后果,你可以使用此CompileTimeValidate代码来更快地失败。 我将把这个切面放在没有继承Form的类(我将把一个UIThread特性放在类NotaWindowsForm中的一个方法中MyMethod),看看当我尝试构建时会发生什么:
class NotaWindowsForm
{
[UIThread]
public void MyMethod()
{
}
}
编译失败,如下所示:
使用CompileTimeValidate并不能消除关于AOP使用的交流和团队合作的需要,它会使得交流发生得更快,更容易和更便宜的解决。 你想做的最后一件事就是部署到生产环境前一天进行一次讨论。
编译时验证是PostSharp所有版本中可用的更有趣和强大的功能之一。 事实上,它使得许多开发人员编写只包含编译时验证的切面。 这些验证切面可以用于检查项目的代码,并将其发现的任何错误视为编译器错误。它们没有切入点,并且不包含程序启动后运行的代码。 这个
是一种常见的技术,PostSharp开发人员决定将其做成一个称为架构约束的一流功能。
架构约束
在PostSharp中,架构约束功能可帮助你为项目编写完整性检查。 可将其视为项目架构的单元测试。
PostSharp 许可
在我们进入本章之前,我们来声明一些许可和技术问题。 此功能(架构约束)与AOP并不严格相关。 它在PostSharp的免费Express版本也并不可用,但展示后期编译器IL操纵的强大仍然是有趣的事情。 上一节的编译时验证可用于所有版本的PostSharp,包括免费版本。
在本节中,我将给出PostSharp允许你创建的约束类型的概述,我会向你展示一个真实的例子,如果你使用NHibernate,架构约束可能会派上用场。
强制架构
架构约束的主要思想是你可以编写其他代码以自动的方式检查你的项目代码。 我们都知道,即使一个项目编译,这并不意味着它不会失败。 如果在编译时,我们可以运行一些额外的检查,那么我们会更早地了解问题(继续本章的主题--失败更快)。
PostSharp使我们能够编写两种不同类型的架构约束:标量和引用。 这种分离部分是语义的,部分是技术性的。 这两种类型的约束都可以在编译时执行C#编译器本身并没有提供的规则,但引用约束都会在PostSharp处理引用代码元素的程序集检查的。
- 标量约束
标量约束是一个简单的约束,它是为了影响单个独立的代码段。 这最像在切面中使用CompileTimeValidate方法(除了没有切面部分)。
例如,当你使用NHibernate时,实体的所有属性都必须是标记为virtual(在前面的章节中涵盖了NHibernate使用Castle DynamicProxy)。 我经常忘记给属性标记virtual,直到我运行程序并使用实体的时候才发现这问题。
我宁愿在编译时收到一条错误信息。 在下一小节中我们会更详细地看看,介绍一个NHibernate virtual约束的实际示例。
示例不限于NHibernate。 如果你之前使用过WCF,那么也许你已经创建了一个DataContract类,添加了一个新的属性,并忘记在其上放置DataMember特性。 或者,你是否创建了一个ServiceContract接口,并忘记将OperationContract特性放在已添加到其中的新方法中? 这些是令人沮丧的问题,正常的C#编译器不会检测到,但是标量约束可以早期检测到。
- 引用约束
引用约束是更广泛的架构约束形式。 引用约束意味着在组件,引用和关系之间实施架构设计。 这种约束形式对于架构师来说可能是有用的,特别是如果你正在开发API。
PostSharp带有三个开箱即用的约束,你可以在一些特定的情况下使用:ComponentInternal,InternalImplements和Internal。见下图:
当然,你完全可以编写自己的引用约束。 我经常遇到的一个烦恼是,sealed可能被滥用,从而限制了可扩展性。 有时有一个很好的理由使用sealed,但不是经常。 它也可能使测试困难。 因此,为了防止sealed潜行在一个项目中,我创建了一个称为“Unsealable”的引用约束。
要写这个约束,我创建了一个名为Unsealable的类,它继承自PostSharp的引用约束代码基类。 在这个类里面,我覆盖了ValidateCode方法,它接收一个目标对象以及一个程序集 。 我扫描整个程序集以查找从目标类派生的密封类。 我还需要使用一个特性(MulticastAttributeUsage)告诉PostSharp这个约束究竟应该准确应用到哪个项目。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using PostSharp.Constraints;
using PostSharp.Extensibility;
namespace ArchitectureConstraints
{
[Serializable]
[MulticastAttributeUsage(MulticastTargets.Class)]
public class Unsealable : ReferentialConstraint
{
public override void ValidateCode(object target, Assembly assembly)
{
//目标类型是一个标记为Unsealable的类
var targetType = (Type)target;
List<Type> sealedSubClasses = assembly.GetTypes()//获得所有类型
.Where(t => t.IsSealed)//类型是密封的
.Where(t => targetType.IsAssignableFrom(t))//类型继承自目标类型
.ToList();
//遍历所有密封子类
sealedSubClasses.ForEach(s =>
{
//为每个类输出错误信息
Message.Write(s,SeverityType.Error,
"Unsealable001",
$"Error on {s.FullName}.Subclasses of {targetType.FullName} cannot be sealed."
);
});
}
}
}
任何使用该Unsealable特性标记的类意味着任何其他从它继承的类不能是密封的,否则,将导致一个错误,从而阻止构建完成。
接下来演示这个约束。 编写一个名为MyUnsealableClass的类; 把Unsealable 特性放在它上面; 编写另一个从MyUnsealableClass继承的类叫TryingToSeal; 使TryingToSeal成为一个密封类;并尝试编译。
namespace ArchitectureConstraints
{
class Program
{
static void Main(string[] args)
{
}
}
[Unsealable]
public class MyUnsealableClass
{}
public sealed class TryingToSeal:MyUnsealableClass
{}
}
注意:如果这里你使用的是Express版本,那么编译是成功的,因为此功能在Express版本不可用。这里,我安装了一个45天试用的Ultimate版本,编译就报错了。
再强调一次,这些架构约束不能消除团队沟通的需要。 作为一名架构师,如果你不希望开发者密封他们的类,那么这会使得适当使用C#密封关键字发生得更早。总之, 失败越快越好。
说到快速失败,我们来看一下在NHibernate中创建一个实体类,处理virtual关键字的标量约束。
真实案例:NHibernate
NHibernate实体的每个属性必须是virtual的。 但在创建新的属性和更改数据访问代码,我经常忘了使用virtual修饰属性。
必须是virtual的吗?
如果禁用延迟加载,则可以具有不是虚拟的NHibernate实体属性。 但是延迟加载是NHibernate的一个重要功能,所以默认情况下是NHibernate会假设你想要懒加载,除非你在.hbm映射文件中进行了配置禁用懒加载。
C#编译器不关心(也不应该关心)这个问题,应该让我继续编译和运行代码 ,但当我尝试建立一个会话工厂时,会得到非常熟悉的InvalidProxyTypeException。
现在我不得不停止我正在做的事情 - 我正在尝试的任何功能或者我正在演示的网站,然后将virtual添加到属性。 并希望我没有忘记任何其他的属性。 因为如果我还遗忘了给其他属性添加virtual,那么我必须再修正多次。
我想避免这种烦恼: 我宁愿失败得更快。 让我们创建一个PostSharp标量约束,使编译时就意识到这些错误。
编写标量约束类似于编写引用约束。 我创建一个继承自PostSharp ScalarConstraint基类的类,并覆盖ValidateCode方法。 在该方法中,获取目标类型的所有属性(在我们的例子中,目标是每个实体类的Type)。对于目标类型的每个属性,检查以确保它是虚拟的。 如果不是,写一个消息。 再一次使用MulticastAttributeUsage特性来指示此约束可以应用到哪一个元素。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PostSharp.Constraints;
using PostSharp.Extensibility;
using System.Reflection;
namespace ArchitectureConstraints
{
[Serializable]
[MulticastAttributeUsage(MulticastTargets.Class)]//约束用在类上
public class NhEntity : ScalarConstraint
{
public override void ValidateCode(object target)//注意这次只有一个参数,没有assembly参数
{
//因为目标是类,所以可转成Type类型
var targetType = (Type)target;
//获取实例的所有公共属性
var properties = targetType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => !p.GetGetMethod().IsVirtual);//且属性不是虚拟的
foreach (PropertyInfo p in properties)
{
//对于没有virtual修饰的属性,打印错误消息
Message.Write(targetType, SeverityType.Error,
"NhVirtual001",
$"Property {p.Name} in entity class {targetType.FullName} is not virtual .");
}
}
}
}
要使用此约束,可以在领域模型的每个实体类上放置一个[NhEntity]特性。 但是如果你又创建了一个新的实体并忘记在上面放置属性,该怎么办? 相反,我将使用PostSharp的另一个功能:特性多播。此功能允许我一次指定多个类。
我们将在下一节中探讨多播的工作原理,但这里先来一点预习。 如果我将所有的NHibernate模型实体都放在相同的命名空间(例如,NHibernateExample.Entities)中,我可以使用一个程序集指令来多播这个NhEntity标量约束:
[assembly: NHEntity(AttributeTargetTypes =
"NHibernateExample.Entities.*")]
你可以将其添加到项目中的标准AssemblyInfo.cs文件中,但可能一个更好的想法是把它放入自己的文件,如AspectInfo.cs。 现在每一个在NHibernateExample.Entities命名空间中的类将具有NHEntity标量约束。 在NHibernateExample.Entities命名空间中创建一本Book类,但保留一个属性不是虚拟的:
using System;
namespace NHibernateExample.Entities
{
public class Book
{
public virtual Guid Id { get; set; }
public string Name { get; set; }//这个不小心忘记了virtual
public virtual string Publisher { get; set; }
public virtual decimal Price { get; set; }
}
}
编译,会出现下面的错误,这样就使得我们在运行程序前可以有机会进行修改。
特性多播不限于架构约束,并且对于正常的PostSharp切面来说可能也很有用,以便最大限度地减少重复使用。 而与架构约束不同,特性组播在免费的Express版本中也可以使用。
多播
在第1章中,我将切入点定义为放置切面的“地方”。 可以将切入点认为是描述切面可以放置的语句(通过向流程图中添加额外的箭头来描述它,如下图,此图第一章中已经贴过):
到目前为止,所有的例子都是简单而狭隘的切入点:一两个成员,也可能是整个类。 所以为了保持简单,我把几个特性放在代码中。 实际上,许多切面具有更广泛的切入点,对每一个类或方法使用特性可能成为分散样板的另一种形式(虽然较少侵入性和较少纠缠)。 如果你有很多使用切面的代码,或代码经常更改,我建议你不要反复使用特性。
这就是PostSharp特性多播功能所能解决的问题(一些其他的AOP工具有称为动态切入点的功能,这是类似的)。 我们可以在三个层面应用PostSharp切面:
- 单个方法或位置级别
- 在类级别(会应用到该类的所有成员)
- 在程序集级别(会应用到该程序集的多个类和成员)
之前看到的例子基本是将切面特性用到单个方法或属性上,这样获得了应用切面所在代码的最大灵活性和控制。下面,我们开始做一些将特性放到类和程序集上的例子。
类级别多播
如果你写了一个LocationInterceptionAspect并在类级别应用它,默认情况下将拦截该类中的所有位置。
如果你编写了方法切面(OnMethodBoundaryAspect或LocationInterceptionAspect)并将其应用于类级别,则默认情况下将应用于该类中的所有方法。 这种方法将等同于使用特性四次,每个方法使用一次。见下图:
当使用切面作为特性时,在特性的构造函数中你将有很多关于多播的配置选项可用。 这些都包含在PostSharp文档中,但这里有一些显着的例子:
-
AttributeExclude
: 从接收到的多播特性中有选择地排除一些方法 -
AspectPriority
: 定义使用切面的顺序(C#中特性的顺序本质上是不确定的) -
AttributeTargetElements
: 选择使用切面的目标类型
为了演示,我们来编写LogAspect类,它只会报告哪些方法正在应用该切面。 一旦我们有了这个切 面,就可以改变配置选项,看看会发生什么:
using PostSharp.Aspects;
using System;
using static System.Console;
namespace ArchitectureConstraints.Multicasting
{
[Serializable]
public class LogAspect:OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionArgs args)
{
WriteLine($"Aspect was applied to {args.Method.Name}");
}
}
[LogAspect]
class MyClass
{
public void Method1() { }
public void Method2() {
Method3();
}
private void Method3(){}
}
}
class Program
{
static void Main(string[] args)
{
var m = new MyClass();
m.Method1();
m.Method2();
Read();
}
}
定义切面LogAspect,并将它直接使用到MyClass上,运行程序,结果如下,可以看到该切面应用到了MyClass的构造函数上和Method1/2/3上:
如果想将LogAspect应用到除了Method3之外的任何方法上,可以使用AttributeExclude
进行设置:
[LogAspect]
class MyClass
{
public void Method1() { }
public void Method2() {
Method3();
}
[LogAspect(AttributeExclude =true)]//从多播中排除这个方法
private void Method3(){}
}
再次运行程序,可以看到切面LogAspect没有应用到Method3上:
使用多个切面可能是常见的情况。 应用这些切面的顺序可能很重要。 例如,你可能希望在同一类上使用缓存的切面和安全性的切面。 下面,我使用MyClass演示这种场景:
using PostSharp.Aspects;
using System;
using static System.Console;
namespace ArchitectureConstraints.Multicasting
{
[Serializable]
class AnotherAspect:OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionArgs args)
{
WriteLine($"Another aspect was applied to {args.Method}");
}
}
}
//演示AspectPriority
[LogAspect(AspectPriority =1)]//LogAspect有最高的优先级
[AnotherAspect(AspectPriority =2)]
class MyClass
{
public void Method1() { }
public void Method2()
{
Method3();
}
private void Method3() { }
}
当你使用C#编译器时,你无法保证这些特性按照你指定的顺序应用。要强制应用顺序,可以使用AspectPriority设置。
LogAspect的优先级高于AnotherAspect,所以它将首先被应用。 如果我交换这个优先级顺序,AnotherAspect将首先被应用。
使用AttributeTargetElements
可以更详细地指出要多播的元素。 此设置使用MulticastTargets枚举,其中包括诸如Method,InstanceContructor和StaticConstructor。 如果我选择InstanceConstructor,该切面仅适用于构造函数。
/// <summary>
/// 演示AttributeTargetElements
/// </summary>
[LogAspect(AttributeTargetElements =PostSharp.Extensibility.MulticastTargets.InstanceConstructor)]
class MyClass
{
public MyClass() { }
public MyClass(int n) { }
public void Method1() { }
public void Method2()
{
Method3();
}
private void Method3() { }
}
可以看到,切面只用到了实例构造函数上:
通过在类级别进行多播,你可以获得合理的默认值(适用于所有内容)和灵活配置,如果需要,你可以使用单个成员。 在程序集级别进行多播时,可以使用这些相同的配置选项。
程序集级别多播
在本章前部分,你已经看到了如何在程序集级别使用特性多播的方式。 我们设置PostSharp将NhEntity标量约束特性应用于某个命名空间中的每个类:
[assembly: NhEntity(AttributeTargetTypes ="NHibernateExample.Entities.*")]
虽然这不是一个切面,但是因为切面和约束都是特性,所以它们都可以组播。
我们从基础开始吧。 要将一个切面应用于整个程序集,请使用语法[assembly:MyAspect]。 此语法将该切面应用到整个程序集中每个有效的目标。
要缩小范围,请使用特性构造函数中的PostSharp配置选项。 你在程序集级别上可以使用与类级别相同的选项。但在程序集级别,AttributeTargetTypes很多设置变得更有用,因为你可以使用它来应用一个切面到多个类或命名空间。
在NhEntity示例中,我将目标设置为具有通配符的命名空(NHibernateExample.Entities *)。 除了通配符( *),你可以使用正则表达式或类的确切名称。
你可以在命名空间或类型层次结构的任何部分使用通配符。 如果我有多个实体命名空间(例如Sales.Entities和Support.Entities),我也可以在命名空间的第一部分使用通配符:
[assembly: MyAspect(AttributeTargetTypes = "*.Entities.*)]
当你想要使用约定来确定哪些切面被应用时,使用正则表达式可能会很有帮助。 如果我建立了一个约定,访问数据库的每个类都是以Repository结尾命名的(AbcRepository,DefRepository等),并且我想将事务切面应用于每个repository类,我可以使用正则表达式:
[assembly: TransactionAspect(AttributeTargetTypes= "regex:.*Repository$")]
不要滥用正则表达式 - 保持简单。 使用明确和合理的惯例,并确定表达式将不会拾取它们不应该的任何目标。 如果一个正则表达式比这个例子复杂得多,你可能要重新思考你的架构或组织。
你还可以通过使用AttributeTargetMembers配置来多播到具有特定名称的各个成员。 适用相同的规则:你可以使用确切的名称,通配符或正则表达式。
如果我想将日志记录切面应用于名称中包含Delete的所有方法,我可以使用AttributeTargetMembers:
[assembly: LogAspect(AttributeTargetMembers="*Delete*")]
如果你的架构包含使用命名空间和/或明智使用约定命名的良好结构,则可以通过使用这些程序集级别切面完全控制切面被用在什么地方。 使用所有配置选项,还可以在必要时定义一些复杂的切入点。
使用程序集级别特性来帮助定义切入点的好处是,你不需要通过到处使用特性使得代码混乱。 此外,你将所有切入点定义在一个方便的文件(如AspectInfo.cs)中,这样可以更轻松地查看哪些切面被使用。
小结
在过去三章中,我强调了使用.NET中两个主要AOP工具的两种途径之间的主要区别。 它们是两个出色的框架,比较和对比它们有助于阐明AOP的基本概念和两种途径之间的重大权衡。
在本章中,我简要介绍了将多个切面应用于相同代码。 我们使用AspectPriority来确定应用程序的顺序。 在下一章中,我们将回顾两种工具,我将展示如何使用DynamicProxy进行此操作。