对于所有的AOP框架来说,多个拦截器最终会应用到某个方法上。这些拦截器按照指定的顺序构成一个管道,管道的另一端就是针对目标方法的调用。从设计角度来将,拦截器和中间件本质是一样的,那么我们可以按照类似的模式来设计拦截器。
我们为整个拦截器管道定义了一个统一的执行上下文,并将其命名为InvocationContext。如下面的代码片段所示,我们可以利用InvocationContext对象得到方法调用上下文的相关信息,其中包括两个方法(定义在接口和实现类型),目标对象、参数列表(含输入和输出参数)、返回值(可读写)。Properties 属性提供了一个自定义的属性容器,我们可以利用它来存放任意与当前方法调用上下文相关的信息。如果需要调用后续的拦截器或者目标方法(如果当前为最后一个拦截器),我们只需要直接调用 ProceedAsync 方法即可。
public abstract class InvocationContext { public abstract MethodInfo Method { get; } public MethodInfo TargetMethod { get; } public abstract object Target { get; } public abstract object[] Arguments { get; } public abstract object ReturnValue { get; set; } public abstract IDictionary<string, object> Properties { get; } public Task ProceedAsync(); }
既然所有的拦截器都是在同一个InvocationContext上下文中执行的,那么我们可以将任意的拦截操作定义成一个 Func<InvocationContext, Task> 对象。Func<InvocationContext, Task>对象不仅可以表示某项单一的拦截操作,实际上包括目标方法调用在内的整个拦截器管道都可以表示成一个Func<InvocationContext, Task>对象。由于这个委托的重要性,我们将它定义成如下这个InterceptDelegate类型。
public delegate Task InterceptDelegate(InvocationContext context);
如果以ASP.NET Core框架的请求处理管道作为类比,那么InvocationContext相当于HttpContext,而InterceptDelegate自然对应的就是RequestDelegate。我们知道ASP.NET Core框架将中间件表示成Func<RequestDelegate, RequestDelegate>对象,那么拦截器自然就可以表示成一个 Func<InterceptDelegate, InterceptDelegate> 。如果读者朋友对此不太理解,可以参阅我的文章《 200行代码,7个对象——让你了解ASP.NET Core框架的本质 》。由于拦截器的重要性,我们也将它定义成如下这个单独的InterceptorDelegate类型。
public delegate InterceptDelegate InterceptorDelegate(InterceptDelegate next);
Dora.Interception和ASP.NET Core采用几乎一致的设计。对于ASP.NET Core来说,虽然中间件最终是通过Func<InterceptDelegate, InterceptDelegate>表示的,但是我们可以将中间件定义成一个按照约定定义的类型。Dora.Interception同样支持基于约定的拦截器类型定义。
public class FoobarInterceptor { private readonly IFoo _foo; private readonly IBar _bar; private readonly string _baz; public FoobarInterceptor(IFoo foo, IBar bar, string baz) { _foo = foo; _bar = bar; _baz = baz; } public async InvokeAsync(InvocationContext context) { await PreInvokeAsync(); await context.ProceedAsync(); await PostInvokeAsync(); } }
如上定义的FoobarInterceptor展现了一个典型的基于约定定义的拦截器类型,它体现了如下的约定:
由于拦截器最终是利用.NET Core的依赖注入框架提供的,所以依赖服务可以直接注入拦截器的构造函数中。但是就服务的生命周期来讲,拦截器本质上是一个 Singleton 服务,我们不应该将 Scoped 服务注入到它的构造函数中。如果具有针对Scoped服务注入的需要,我们应该将它注入到InvokeAsync方法中。
public class FoobarInterceptor { private readonly string _baz; public FoobarInterceptor(string baz) { _baz = baz; } public async InvokeAsync(InvocationContext context, IFoo foo, IBar bar) { await PreInvokeAsync(); await context.ProceedAsync(); await PostInvokeAsync(); } }
当Dora.Interception在调用InvokeAsync方法的时候,它会利用 当前Scope的IServiceProvider对象 来提供其参数。对于ASP.NET Core应用来说,如果拦截器的执行在整个请求处理的调用链中,这个IServiceProvider对象就是当前HttpContext的RequestServices属性。如果当前IServiceProvider不存在,作为根的IServiceProvider对象会被使用。