相较于社区其他主流的AOP框架,Dora.Interception在Interceptor提供了完全不同的编程方式。我们并没有为Interceptor定义一个接口,正是因为不需要实现一个预定义的接口,Dora.Interception下的Interceptor定义变得更加自由。除此之外,Interceptor的异步执行是我在设计Dora.Interception之初最为关心的问题,也就是说如果Interceptor应用的目标方法是异步的,Interceptor自身也应该被赋予异步执行的能力。接下来我们就来聊聊如果你使用了Dora.Interception,如何定义你的Interceptor。
目录 一、两种代理类型生成方式 二、InterceptorDelegate 三、定义Interceptor类型 四、支持方法注入
Dora.Interception采用动态生成代理类型(采用IL Emit)的方式来实现针对目标方法的拦截,具体来说我们提供了两种类型的代理类型生成方式:
AOP的核心在于将一些非业务的功能定义成相应的Interceptor,并以横切(Crosscutting)的形式注入到针对目标方法的调用过程中。换句话说,Interceptor需要拦截目标方法的执行,并在针对当前执行方法的上下文中执行被注入的操作。如果我们将方法执行的上下文定义成一个InvocationContext,那么Interceptor需要执行的拦截操作就可以表示成一个Action<InvocationContext>。但是我们在上面说过了,一个Interceptor应该被赋予异步执行的能力,按照基于Task的并行编程模式,Interceptor自身执行的拦截操作应该表示成一个Func<InvocationContext, Task>。
我们在Dora.Interception定义了如下这个抽象的InvocationContext来表示需要被拦截的方法执行上下文。它的Method和TargetMethod返回代表当前方法的MethodBase对象,如果采用基于接口的代理类型生成方式,前者表示定义在接口上的方法,后者则表示定义在目标类型上的方法。如果采用基于虚方法的代理类型生成方式,两个属性返回的是同一个对象,表示定义在被拦截类型中的方法。至于Proxy和Target则很明显,分别表示当前的代理对象和被封装的目标对象,如果采用基于虚方法的代理类型生成方式,两个属性返回同一个对象。
1 public abstract class InvocationContext
2 {
3 public abstract MethodBase Method { get; }
4 public MethodBase TargetMethod { get; }
5 public abstract object Proxy { get; }
6 public abstract object Target { get; }
7 public abstract object[] Arguments { get; }
8 public abstract object ReturnValue { get; set; }
9 }
InvocationContext的Arguments表示当前方法调用的参数,既包括一般的输入参数,也包括ref/out参数。值得一提的是,Arguments属性是可读可写的,也就说Interceptor可以通过修改该属性中某个元素的值进而实现修改某个参数值的目的。由于整个调用过程共享同一个InvocationContext对象,所以某个Interceptor对Arguments所作的任何修改都将影响到后续Intercecptor以及最终目标方法的执行。InvocationContext的ReturnValue表示方法调用的返回值。如果目标方法最终被调用,它的返回值将最终反映在这个属性上。这个属性是可读可写的,任意Interceptor都可以通过修改这个属性得到改变方法调用返回值的目的。
由于当前方法调用的执行上下文被表示成一个InvocationContext对象,所以实现在Interceptor上的拦截操作可以表示成一个Func<InvocationContext, Task>类型的委托。不过为了编程方便,我们专门定义了如下这个对应的委托类型InterceptDelegate。由于一个方法上可以同时应用多个Interceptor,那么对应一个Interceptor在完成了自身定义的拦截操作之后,它还将决定是否继续调用后续的Interceptor或者目标方法,或者说针对后续Interceptor或者目标方法的调用也属于当前拦截操作的一部分,所以我们定义了另一个委托类型InterceptorDelegate来表示一个Interceptor对象。
1 public delegate Task InterceptDelegate(InvocationContext context);
2 public delegate InterceptDelegate InterceptorDelegate(InterceptDelegate next);
对于表示Interceptor的InterceptorDelegate委托,它的输入和输出都是InterceptDelegate委托,Interceptor通过作为输入的InterceptDelegate委托实现针对后续Interceptor或者目标方法的调用,它返回的InterceptDelegate委托对象则体现实现的拦截操作。从这个意义上讲,一个InterceptorDelegate委托不仅仅表示一个单一的Interceptor对象,也可以表示由多一个Interceptor组成的Interceptor Chain。从另一个角度讲,由于一个Interceptor已经实现了针对后续Interceptor的执行,所以一个Interceptor本身就表示一个Interceptor Chain。
虽然Dora.Interception在底层总是使用一个InterceptorDelegate委托表示Interceptor(Chain),为了编程上的便利,我们依然将Interceptor定义成一个类型,我们定义的Interceptor类型只需要采用如下的“约定”即可:
接下来我们就通过实例演示的方式来简单介绍一下如何遵循上述的这些约定来定义我们的Interceptor类型。如下面的代码片段所示,作为Interceptor类型的FoobarInterceptor具有一个公共的实例构造函数,作为强制要求的第一个参数next表示用于调用后续Interceptor或者目标方法的InterceptDelegate委托对象。除了该参数,我们还定义了额外两个接口类型的参数,这些参数都被保存到对应的字段或者属性上。拦截操作定义在InvokeAsync方法,这个方法的方法名(InvokeAsync)、返回类型(Task)和第一个参数的类型(InvocationContext)都是我们约定的一部分。在这个方法中,我们输出Foo和Bar属性,并最终利用构造函数指定的InterceptDelegate委托对象将调用向后传递。
1 public class FoobarInterceptor
2 {
3 public IFoo Foo { get; }
4 public IBar Bar { get; }
5 private InterceptDelegate _next;
6 public FoobarInterceptor(InterceptDelegate next, IFoo foo, IBar bar)
7 {
8 _next = next;
9 this.Foo = foo;
10 this.Bar = bar;
11 }
12
13 public Task InvokeAsync(InvocationContext context)
14 {
15 Console.WriteLine(this.Foo);
16 Console.WriteLine(this.Bar);
17 return _next(context);
18 }
19 }
20
21
22 public class FoobarAttribute : InterceptorAttribute
23 {
24 public override void Use(IInterceptorChainBuilder builder)
25 {
26 builder.Use<FoobarInterceptor>(this.Order);
27 }
28 }
29
30 public interface IFoo { }
31 public interface IBar { }
32 public class Foo : IFoo { }
33 public class Bar : IBar { }
FoobarAttribute是用于注册FoobarInterceptor的特性,FoobarAttribute派生于InterceptorAttribute这个抽象基类,关于InterceptorAttribute以及相关Interceptor注册的类型我们将在后续的文章中进行介绍。Dora.Interception的一个显著的特征就是与.NET Core的DI实现了无缝集成,具体体现在Interceptor中所需的任何服务都可以直接采用DI的方式来提供,比如FoobarInterceptor的Foo和Bar属性对应的服务实例。如下面的代码片段所示,我们将FoobarAttribute标注到Demo类型的虚方法Invoke上。在Main方法中,我们将IFoo、IBar和Demo对应的服务注册添加到创建的ServiceCollection上,然后调用后者的BuildInterceptableServiceProvider方法创建一个具有“拦截”特性的ServiceProvider。如果由这个ServiceProvider提供的服务类型能够被拦截,它会利用相应的代理类型生成机制动态地生成对应的代理类型,并最终创建出对应的代理实例。
1 class Program
2 {
3 static void Main()
4 {
5 var demo = new ServiceCollection()
6 .AddScoped<IFoo, Foo>()
7 .AddScoped<IBar, Bar>()
8 .AddScoped<Demo, Demo>()
9 .BuildInterceptableServiceProvider()
10 .GetRequiredService<Demo>();
11 demo.Invoke();
12 }
13 }
14
15 public class Demo
16 {
17 [Foobar]
18 public virtual void Invoke()
19 {
20 Console.WriteLine("Demo.Invoke() is invoked");
21 }
22 }
执行上面的代码会在控制台上产生如下的输出结果,我们可以看出应用在Domo.Invoke方法上的FoobarInteceptor被正常执行,它依赖的两个服务类型Foo和Bar正好与服务注册一致。
对于面定义的FoobarInteceptor来说,它依赖的两个服务Foo和Bar实际上是通过构造器注入的方式提供的,实际上我们还具有更加简洁的方案,那就是直接在InvokeAsync方法中对它们进行注入,这也是我们为什么不为Interceptor定义接口的原因。直接采用方法注入会让你的Interceptor变得更加简洁。
1 public class FoobarInterceptor
2 {
3 private InterceptDelegate _next;
4 public FoobarInterceptor(InterceptDelegate next)
5 {
6 _next = next;
7 }
8
9 public Task InvokeAsync(InvocationContext context, IFoo foo, IBar bar)
10 {
11 Console.WriteLine(foo);
12 Console.WriteLine(bar);
13 return _next(context);
14 }
15 }