为了支持AOP的编程模式,我为.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, )
  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:    
   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( interceptable)
   5:     {
   6:         _service = ;
   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:             
  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( 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.();
  10:     }
  11:  
  12:     public void Configure(IApplicationBuilder app)
  13:     {
  14:         app.UseDeveloperExceptionPage()
  15:             .UseMvc();
  16:     }
  17: }

目前来说,如果采用这种方法,我们需要让注入的服务实现一个空的IInterceptable接口,因为我会利用它来确定某个对象是否需要封装成代理,将来我会将这个限制移除。

   1: public class FoobarService : IFoobarService, 
   2: {
   3:     [ErrorLogger("App")]
   4:     public void Invoke()
   5:     {
   6:         throw new InvalidOperationException("Manually thrown exception!");
   7:     }
   8: }

作者:蒋金楠
微信公众账号:大内老A
微博:www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

原文链接

时间: 2024-11-13 10:02:00

为了支持AOP的编程模式,我为.NET Core写了一个轻量级的Interception框架[开源]的相关文章

【C/C++学院】0817-递归汉诺塔 双层递归 /CPP结构体 /面向过程与面向对象的编程模式/类的常识共用体实现一个类的特征/QT应用于类以及类的常识

递归汉诺塔 双层递归 #include <iostream> void han(int n, char A, char B, char C) { static int num = 1; std::cout << "第" << num << "次"; num++; if (n<1) { return; } else { han(n - 1, A, C, B); std::cout << A <&l

Java的Spring框架下的AOP编程模式示例_java

Spring框架的关键组件是面向方面编程(AOP)框架.面向方面的编程不仅打破程序逻辑分成不同的部分称为所谓的担忧.跨越多个点的应用程序的功能被称为横切关注点和这些横切关注点是从应用程序的业务逻辑概念上区分开来.还有像日志记录,审计,声明性事务,安全性和高速缓存等方面的各种常见的好例子 模块化的OOP中的关键单元是类,而在AOP中模块化的单元则是切面.依赖注入可以帮助你从对方解耦应用程序对象和AOP可以帮助你从他们影响的对象分离横切关注点. AOP是一样的编程语言如Perl,.NET,Java和

分布式编程模式中的租约、事务和分布式事件机制

Jini技术面向网络及分布式计算的特性决定了Jini技术必然与传统的单机系统在许多方面有概念上和实际应用中的差别.如网络的延迟.失败,或者设备的突然撤出,将导致信息的无序和丢失:资源的获得.保存.维护和回收情况更为复杂:不同实体之间通讯和协调工作的可靠性及效率并不像单机系统中那样较为容易地获得保证.因而在 Jini 中以 Java 为基础加入了分布式编程模式,特别是引入了租约.分布式事务和分布式事件. 租约 租约的基本概念是资源只能被使用一段时间,这由租约的持有者(lease holder)和租

【译】Go语言编程模式

声明:本文为InfoQ中文站特供稿件,首发地址为:Go语言编程模式 在2016年伦敦举办的QCon大会上,Peter Bourgon做了<六年Go语言设计经验>的报告,重点探讨了在使用Go进行开发时的编程模式和反模式.在这里,我们将他给Go开发者的建议进行了简单的总结. GOPATH:将GOPATH/bin添加到"PATH"这个环境变量中,以便Go应用可以访问所需要的二进制文件.在绝大多数场景下,Bourgon建议使用全局唯一的GOPATH.有些开发者希望严格区分自己的代码

Scalaz(10)- Monad:就是一种函数式编程模式-a design pattern

   Monad typeclass不是一种类型,而是一种程序设计模式(design pattern),是泛函编程中最重要的编程概念,因而很多行内人把FP又称为Monadic Programming.这其中透露的Monad重要性则不言而喻.Scalaz是通过Monad typeclass为数据运算的程序提供了一套规范的编程方式,如常见的for-comprehension.而不同类型的Monad实例则会支持不同的程序运算行为,如:Option Monad在运算中如果遇到None值则会中途退出:St

通过编程模式起底小程序开发技术特点

从小程序诞生伊始,就有很多人开始研习小程序的机理和特点,从源代码的角度.从整体架构的角度,有很多不错的文章会令人受益. 但理论是一回事,真正理解小程序,还是需要一定的实践,才能进一步去理解小程序背后的一些想法,它和现有平台的一些异同,以及如何去适应它,做出更有趣的小程序. 小程序的编程模式 最近,我们在做「轻芒小程序+」和其它轻芒产品的小程序应用过程中,对小程序有了进一步的理解,进而有了本文. 去理解一个开发平台的特性,一个不错的角度就是从"编程模式"入手,就是看在这个平台上去开发,需

Box Model 1.00.03发布 Web编程模式

Box Model 是一个实在的Web编程模式,用于建立简单重复性任务到一个"黑盒子"的复杂应用程序的框架.该框架是受信号处理过程应用到Web的启发而设计,它将取代任何无Web编程需求的 MVC 或3层模型.它本身包含高参数化.语言.框架中每个任务的模板. Box Model 1.00.03该版本Box.lib加载模板文件和语言文件已修改,支持LanguageSource和TemplateSource作为输入. engineBox.lib已修改,读取XML文件作为一个流定义. 软件信息

对比.NET PetShop和Duwamish来探讨Ado.NET的数据库编程模式

ado|编程|数据|数据库   对比.NET PetShop和Duwamish来探讨Ado.NET的数据库编程模式 概述Ado.NET为我们提供了强大的数据库开发能力,它内置的多个对象为我们的数据库编程提供了不同的选择.但是在允许我们灵活选用的同时,许多初学者也很迷惑,我到底是应该使用DataReader还是应该使用DataAdapter?我只想读取一小部分数据,难道我一定要Fill满整个DataSet吗?为什么DataReader不能和RecordSet一样提供一个数据更新的方法?DataSe

异步操作和Web服务,第2部分:构建异步Web服务的编程模式

在本系列的第一篇文章中,我讨论了异步操作的性质以及它们如何应用于 Web 服务.在某些情况下,对 Web 服务请求的响应并不是立即提供的,而是在初始请求事务完成后的某个时候提供.Web 服务规范和标准并不显式支持这种 异步操作(asynchronous operation):但是,那些标准的确包含可以作为异步操作基础的基础架构和机制.通过本系列的第一部分,您应该已经知道了如何使用现有的基础架构来支持异步行为:如果您还没有看过那篇文章,我强烈建议您去看看,因为那里的信息将帮助您理解现在的这一部分.