为了支持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会按照顺序被先后执行。

image

二、安装NuGet包

这个框架目前涉及到如下两个框架,基础的模型实现在Dora.Interception这个包中,Dora.Interception.Castle则利用Castle.DynamicProxy针对代理的创建提供了一个默认实现。

  • Dora.Interception
  • Dora.Interception.Castle

这两个NuGet包已经上传到nuget.org,所以我们可以直接使用它们。假设我们创建了一个空的ASP.NET Core控制台应用,我们可以通过执行如下的命名

image

三、定义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中间件,所以会出入如下图所示的错误页面。而服务端的控制台会显示记录下的错误日志。

image

六、如果你不喜欢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: }

作者:蒋金楠
微信公众账号:大内老A
微博: www.weibo.com/artech
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
相关实践学习
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
相关文章
|
2月前
|
开发框架 .NET C#
ASP.NET Core Blazor 路由配置和导航
大家好,我是码农刚子。本文系统介绍Blazor单页应用的路由机制,涵盖基础配置、路由参数、编程式导航及高级功能。通过@page指令定义路由,支持参数约束、可选参数与通配符捕获,结合NavigationManager实现页面跳转与参数传递,并演示用户管理、产品展示等典型场景,全面掌握Blazor路由从入门到实战的完整方案。
277 6
|
开发框架 .NET 开发者
简化 ASP.NET Core 依赖注入(DI)注册-Scrutor
Scrutor 是一个简化 ASP.NET Core 应用程序中依赖注入(DI)注册过程的开源库,支持自动扫描和注册服务。通过简单的配置,开发者可以轻松地从指定程序集中筛选、注册服务,并设置其生命周期,同时支持服务装饰等高级功能。适用于大型项目,提高代码的可维护性和简洁性。仓库地址:&lt;https://github.com/khellang/Scrutor&gt;
314 5
|
11月前
|
C# Android开发 iOS开发
2025年全面的.NET跨平台应用框架推荐
2025年全面的.NET跨平台应用框架推荐
513 23
|
消息中间件 开发框架 监控
NET任务调度框架Hangfire使用指南
Hangfire 是一个用于 .NET 应用程序的开源任务调度框架,支持长时间运行任务、定时任务等。通过简单的安装配置,即可将任务从主线程分离,提升应用性能。支持多种数据库,提供丰富的任务类型如立即执行、延迟执行和周期性任务,并有可视化管理界面 Hangfire Dashboard。还支持安全性配置及扩展插件,如 Hangfire.HttpJob,适合各种复杂场景下的任务调度需求。
1194 1
NET任务调度框架Hangfire使用指南
|
开发框架 .NET C#
在 ASP.NET Core 中创建 gRPC 客户端和服务器
本文介绍了如何使用 gRPC 框架搭建一个简单的“Hello World”示例。首先创建了一个名为 GrpcDemo 的解决方案,其中包含一个 gRPC 服务端项目 GrpcServer 和一个客户端项目 GrpcClient。服务端通过定义 `greeter.proto` 文件中的服务和消息类型,实现了一个简单的问候服务 `GreeterService`。客户端则通过 gRPC 客户端库连接到服务端并调用其 `SayHello` 方法,展示了 gRPC 在 C# 中的基本使用方法。
278 5
在 ASP.NET Core 中创建 gRPC 客户端和服务器
|
12月前
|
监控 前端开发 API
一款基于 .NET MVC 框架开发、功能全面的MES系统
一款基于 .NET MVC 框架开发、功能全面的MES系统
364 5
|
12月前
|
开发框架 算法 中间件
ASP.NET Core 中的速率限制中间件
在ASP.NET Core中,速率限制中间件用于控制客户端请求速率,防止服务器过载并提高安全性。通过`AddRateLimiter`注册服务,并配置不同策略如固定窗口、滑动窗口、令牌桶和并发限制。这些策略可在全局、控制器或动作级别应用,支持自定义响应处理。使用中间件`UseRateLimiter`启用限流功能,并可通过属性禁用特定控制器或动作的限流。这有助于有效保护API免受滥用和过载。 欢迎关注我的公众号:Net分享 (239字符)
290 1
|
开发框架 缓存 .NET
GraphQL 与 ASP.NET Core 集成:从入门到精通
本文详细介绍了如何在ASP.NET Core中集成GraphQL,包括安装必要的NuGet包、创建GraphQL Schema、配置GraphQL服务等步骤。同时,文章还探讨了常见问题及其解决方法,如处理复杂查询、错误处理、性能优化和实现认证授权等,旨在帮助开发者构建灵活且高效的API。
347 3
|
传感器 人工智能 供应链
.NET开发技术在数字化时代的创新作用,从高效的开发环境、强大的性能表现、丰富的库和框架资源等方面揭示了其关键优势。
本文深入探讨了.NET开发技术在数字化时代的创新作用,从高效的开发环境、强大的性能表现、丰富的库和框架资源等方面揭示了其关键优势。通过企业级应用、Web应用及移动应用的创新案例,展示了.NET在各领域的广泛应用和巨大潜力。展望未来,.NET将与新兴技术深度融合,拓展跨平台开发,推动云原生应用发展,持续创新。
169 4
|
3月前
|
XML 安全 Java
使用 Spring 的 @Aspect 和 @Pointcut 注解简化面向方面的编程 (AOP)
面向方面编程(AOP)通过分离横切关注点,如日志、安全和事务,提升代码模块化与可维护性。Spring 提供了对 AOP 的强大支持,核心注解 `@Aspect` 和 `@Pointcut` 使得定义切面与切入点变得简洁直观。`@Aspect` 标记切面类,集中处理通用逻辑;`@Pointcut` 则通过表达式定义通知的应用位置,提高代码可读性与复用性。二者结合,使开发者能清晰划分业务逻辑与辅助功能,简化维护并提升系统灵活性。Spring AOP 借助代理机制实现运行时织入,与 Spring 容器无缝集成,支持依赖注入与声明式配置,是构建清晰、高内聚应用的理想选择。
462 0

热门文章

最新文章