NET Core写了一个轻量级的Interception框架[开源]
ASP.NET Core具有一个以ServiceCollection和ServiceProvider为核心的依赖注入框架,虽然这只是一个很轻量级的框架,但是在大部分情况下能够满足我们的需要。不过我觉得它最缺乏的是针对AOP的支持,虽然这个依赖注入框架提供了扩展点使我们可以很容易地实现与第三方框架的集成,但是我又不想“节外生枝”,为此我们趁这个周末写了一个简单的Interception框架来解决这个问题。通过这个命名为Dora.Interception的框架,我们可以采用一种非常简单、直接而优雅地(呵呵)在这个原生的DI框架上实现针对AOP的编程。目前这只是一个Beta(Beta1)版本,我将它放到了github上(https://github.com/jiangjinnan/Dora)。我写这篇文章不是为了说明这个Dora.Interception的设计和实现原理,而是为了介绍如何利用它在一个ASP.NET Core与原生的DI框架结合实现AOP的编程模式。两个实例可以从这里下载。
目录
一、基本原理
二、安装NuGet包
三、定义Interceptor
四、定义InterceptorAttribute
五、以DI的方式注入代理
六、如果你不喜欢IInterceptable<T>接口
一、基本原理
和大部分针AOP/Interception的实现一样,我们同样采用“代理”的方式实现对方法调用的拦截和注入。如下图所示,我们将需要以AOP方法注入的操作定义成一个个的Interceptor,并以某种方式(我采用的是最为直接的标注Attribute的形式)应用到某个类型或者方法上。在运行的时候我们为目标对象创建一个代理,我们针对代理对象的调用将会自动传递到目标对象。不过在目标对象最终被调用的时候,注册的Interceptor会按照顺序被先后执行。
二、安装NuGet包
这个框架目前涉及到如下两个框架,基础的模型实现在Dora.Interception这个包中,Dora.Interception.Castle则利用Castle.DynamicProxy针对代理的创建提供了一个默认实现。
- Dora.Interception
- Dora.Interception.Castle
这两个NuGet包已经上传到nuget.org,所以我们可以直接使用它们。假设我们创建了一个空的ASP.NET Core控制台应用,我们可以通过执行如下的命名
三、定义Interceptor
假设我们创建这样一个Interceptor,它能够捕获后续执行过程中抛出的异常,并将异常消息写入日志,我们将这个Interceptor命名为ErrorLogger。如下所示的就是这个ErrorLogger的完整定义。
1: public class ErrorLogger
2: {
3: private InterceptDelegate _next;
4: private ILogger _logger;
5: public ErrorLogger(InterceptDelegate next, ILoggerFactory loggerFactory, string category)
6: {
7: _next = next;
8: _logger = loggerFactory.CreateLogger(category);
9: }
10:
11: public async Task InvokeAsync(InvocationContext context)
12: {
13: try
14: {
15: await _next(context);
16: }
17: catch (Exception ex)
18: {
19: _logger.LogError(ex.Message);
20: throw;
21: }
22: }
23: }
考虑到依赖注入的使用,我们并没有为具体的Interceptor类型定义一个接口,用户仅仅需要按照如下的约定来定义这个Interceptor类型就可以了。对ASP.NET Core的管道设计比较熟悉的人应该可以看出这与中间件的设计是一致的。
- Interceptor具有一个这样一个公共构造函数:它的第一个参数是一个InterceptDelegate 类型的委托,我们通过它调用后续的Interceptor或者目标对象。我们并不对后续的参数做任何约束,它们可以采用DI的方式进行注入(比如上面的loggerFactory参数)。如果不能以DI的形式提供的参数(比如参数category),在后面注册的时候需要显式指定。
- 拦截注入的功能虚线实现在一个名为InvokeAsync的方法中,该方法的需要返回一个Task对象,并且要求方法中包含一个类型为InvocationContext 的对象,该对象表示执行代理方法的执行上下文。如下面的代码片段所示,我们不仅仅可以得到与当前方法调用相关的上下文信息,还可以直接利用它设置参数的值和最终返回的值。InvokeAsync方法需要自行决定是否继续调用后续的Interceptor和目标对象,这可以直接通过在构造函数中指定的这个InterceptDelegate 来完成。
1: namespace Dora.Interception
2: {
3: public abstract class InvocationContext
4: {
5: protected InvocationContext();
6:
7: public abstract object[] Arguments { get; }
8: public abstract Type[] GenericArguments { get; }
9: public abstract object InvocationTarget { get; }
10: public abstract MethodInfo Method { get; }
11: public abstract MethodInfo MethodInvocationTarget { get; }
12: public abstract object Proxy { get; }
13: public abstract object ReturnValue { get; set; }
14: public abstract Type TargetType { get; }
15:
16: public abstract object GetArgumentValue(int index);
17: public abstract void SetArgumentValue(int index, object value);
18: }
19: }
由于构造函数和InvokeAsync方法都支持依赖注入,所以ErrorLogger也可以定义成如下的形式(ILoggerFactory 在InvokeAsync方法中注入)。
1: public class ErrorLogger
2: {
3: private InterceptDelegate _next;
4: private string _category;
5: public ErrorLogger(InterceptDelegate next, string category)
6: {
7: _next = next;
8: _category = category;
9: }
10:
11: public async Task InvokeAsync(InvocationContext context, ILoggerFactory loggerFactory)
12: {
13: try
14: {
15: await _next(context);
16: }
17: catch (Exception ex)
18: {
19: loggerFactory.CreateLogger(_category).LogError(ex.Message);
20: throw;
21: }
22: }
23: }
四、定义InterceptorAttribute
由于我们采用标注Attribute的方式,我们为这样的Attribute定义了一个名为InterceptorAttribute的基类。针对ErrorLogger的ErrorLoggerAttribute定义如下,它的核心在与需要实现抽象方法Use并利用作为参数的IInterceptorChainBuilder 注册对应的ErrorLogger。IInterceptorChainBuilder 中定义了一个泛型的方法使我们很容易地实现针对某个Interceptor类型的注册。该方法的第一个参数是整数,它决定注册的Interceptor在整个Interceptor有序列表中的位置。InterceptorAttribute中定义了对应的Order属性。如果注册Interceptor类型的构造还是具有不能通过依赖注入的参数,我们需要在调用Use方法的时候显式指定(比如category)。
1: [AttributeUsage( AttributeTargets.Class| AttributeTargets.Method, AllowMultiple = false)]
2: public class ErrorLoggerAttribute : InterceptorAttribute
3: {
4: private string _category;
5:
6: public ErrorLoggerAttribute(string category)
7: {
8: _category = category;
9: }
10: public override void Use(IInterceptorChainBuilder builder)
11: {
12: builder.Use<ErrorLogger>(this.Order, _category);
13: }
14: }
InterceptorAttribute可以应用在类和方法上(我不赞成将它应用到接口上),在默认情况下它的AllowMultiple 属性为False。如果我们希望Interceptor链中可以包含多个相同类型的Interceptor,我们可以将AllowMultiple 属性设置为True。值得一提的是,在AllowMultiple 属性为False的情况下,如果类型和方法上都应用了同一个InterceptorAttribute,那么只会选择应用在方法上的那一个。在如下的代码中,我们将ErrorLoggerAttribute应用到总是会抛出异常的Invoke方法中,并且将日志类型设置为“App”。
1: public interface IFoobarService
2: {
3: void Invoke();
4: }
5:
6: public class FoobarService : IFoobarService
7: {
8: [ErrorLogger("App")]
9: public void Invoke()
10: {
11: throw new InvalidOperationException("Manually thrown exception!");
12: }
13: }
五、以DI的方式注入代理
我们依然会以DI的方式来使用上面定义的服务IFoobarService,但是毫无疑问,注入的对象必须是目标对象(FoobarService)的代理,我们注册的Interceptor才能生效,为了达到这个目的,我们需要使用如下这个IInterceptable<T>接口,它的Proxy属性为我们返回需要的代理对象。
1: namespace Dora.Interception
2: {
3: public interface IInterceptable<T> where T : class
4: {
5: T Proxy { get; }
6: }
7: }
比如我们选在在MVC应用中将IFoobarService注入到Controller中,我们可以采用如下的定义方式。
1: public class HomeController
2: {
3: private IFoobarService _service;
4: public HomeController(IInterceptable<IFoobarService> interceptable)
5: {
6: _service = interceptable.Proxy;
7: }
8: [HttpGet("/")]
9: public string Index()
10: {
11: _service.Invoke();
12: return "Hello World";
13: }
14: }
接下来我们来完成这个应用余下的部分。如下面的代码片段所示,我们在作为启动类Startup的ConfigureServicves方法中调用IServiceCollection的扩展方法AddInterception注册于Interception相关的服务。为了确定ErrorLogger是否将异常信息写入日志,我们在Main方法中添加了针对ConsoleLoggerProvider的注册,并选择只写入类型为“App”的日志。
1: public class Program
2: {
3: public static void Main(string[] args)
4: {
5: new WebHostBuilder()
6: .ConfigureLogging(factory=>factory.AddConsole((category, level)=>category == "App"))
7: .UseKestrel()
8: .UseStartup<Startup>()
9: .Build()
10: .Run();
11: }
12: }
13:
14: public class Startup
15: {
16: public void ConfigureServices(IServiceCollection services)
17: {
18: services
19: .AddInterception()
20: .AddScoped<IFoobarService, FoobarService>()
21: .AddMvc();
22: }
23:
24: public void Configure(IApplicationBuilder app)
25: {
26: app.UseDeveloperExceptionPage()
27: .UseMvc();
28: }
29: }
运行该应用后,如果我们利用浏览器访问该应用,由于我们注册了DeveloperExceptionPageMiddleware中间件,所以会出入如下图所示的错误页面。而服务端的控制台会显示记录下的错误日志。
六、如果你不喜欢IInterceptable<T>接口
Interception自身的特质决定我们只有注入目标对象的代理才能让注册的Interceptor被执行,这个问题我们是利用IInterceptable<T>接口来实现的,可能有人觉得这种方法不是很爽的话,我们还有更好的解决方案。我们先将HomeController写成正常的形式。
1: public class HomeController
2: {
3: private IFoobarService _service;
4: public HomeController(IFoobarService service)
5: {
6: _service = service;
7: }
8: [HttpGet("/")]
9: public string Index()
10: {
11: _service.Invoke();
12: return "Hello World";
13: }
14: }
接下来我们需要在Startup的ConfigureServices方法调用ServiceCollection的ToInterceptable方法即可。
1: public class Startup
2: {
3: public void ConfigureServices(IServiceCollection services)
4: {
5: services
6: .AddInterception()
7: .AddScoped<IFoobarService, FoobarService>()
8: .AddMvc();
9: services.ToInterceptable();
10: }
11:
12: public void Configure(IApplicationBuilder app)
13: {
14: app.UseDeveloperExceptionPage()
15: .UseMvc();
16: }
17: }
目前来说,如果采用这种方法,我们需要让注入的服务实现一个空的IInterceptable接口,因为我会利用它来确定某个对象是否需要封装成代理,将来我会将这个限制移除。
1: public class FoobarService : IFoobarService, IInterceptable
2: {
3: [ErrorLogger("App")]
4: public void Invoke()
5: {
6: throw new InvalidOperationException("Manually thrown exception!");
7: }
8: }