为了支持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, 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: }
为了支持AOP的编程模式,我为.NET Core写了一个轻量级的Interception框架[开源]的更多相关文章
- 关于在C#中实现AOP 拦截编程模式的新的探索
前面有篇文章,是从其他个人博客中贴过来的.地址:http://www.lanhusoft.com/Article/240.html 作者总结实现的挺好. 但是.不能不考虑性能!!使用 ContextB ...
- 从壹开始前后端分离【 .NET Core2.0 +Vue2.0 】框架之九 || 依赖注入IoC学习 + AOP界面编程初探
更新 1.如果看不懂本文,或者比较困难,先别着急问问题,我单写了一个关于依赖注入的小Demo,可以下载看看,多思考思考注入的原理: https://github.com/anjoy8/BlogArti ...
- Z从壹开始前后端分离【 .NET Core2.2/3.0 +Vue2.0 】框架之九 || 依赖注入IoC学习 + AOP界面编程初探
本文梯子 本文3.0版本文章 更新 代码已上传Github+Gitee,文末有地址 零.今天完成的绿色部分 一.依赖注入的理解和思考 二.常见的IoC框架有哪些 1.Autofac+原生 2.三种注入 ...
- ASP.NET Core 6框架揭秘实例演示[13]:日志的基本编程模式[上篇]
<诊断跟踪的几种基本编程方式>介绍了四种常用的诊断日志框架.其实除了微软提供的这些日志框架,还有很多第三方日志框架可供我们选择,比如Log4Net.NLog和Serilog 等.虽然这些框 ...
- Spring AOP 切面编程的方法
spring aop的使用分为两种,一种是使用注解来实现,一种是使用配置文件来实现. 先来简单的介绍一下这两种方法的实现,接下来详细的介绍各处的知识点便于查阅.目录如下: 1.基于注解实现spring ...
- Scalaz(54)- scalaz-stream: 函数式多线程编程模式-Free Streaming Programming Model
长久以来,函数式编程模式都被认为是一种学术研究用或教学实验用的编程模式.直到近几年由于大数据和多核CPU的兴起造成了函数式编程模式在一些实际大型应用中的出现,这才逐渐改变了人们对函数式编程无用论的观点 ...
- Scalaz(43)- 总结 :FP就是实用的编程模式
完成了对Free Monad这部分内容的学习了解后,心头豁然开朗,存在心里对FP的疑虑也一扫而光.之前也抱着跟大多数人一样的主观概念,认为FP只适合学术性探讨.缺乏实际应用.运行效率低,很难发展成现实 ...
- Scalaz(10)- Monad:就是一种函数式编程模式-a design pattern
Monad typeclass不是一种类型,而是一种程序设计模式(design pattern),是泛函编程中最重要的编程概念,因而很多行内人把FP又称为Monadic Programming.这其中 ...
- MXNet设计笔记之:深度学习的编程模式比较
市面上流行着各式各样的深度学习库,它们风格各异.那么这些函数库的风格在系统优化和用户体验方面又有哪些优势和缺陷呢?本文旨在于比较它们在编程模式方面的差异,讨论这些模式的基本优劣势,以及我们从中可以学到 ...
随机推荐
- python - 简明 性能测试
简洁测试: # python -m cProfile test.py 代码注入: # -*- coding: utf-8 -*- class test(object): pass class test ...
- application tips
trace(ApplicationDomain.currentDomain == ApplicationDomain.currentDomain); trace(stage.loaderInfo.ap ...
- redis 在centos下的安装部署
安装的redis版本是 redis-3.0.2 请严格按照以下步骤进行 可以免除以下错误 1 make[2]: cc: Command not found 异常原因:没有安装gcc 解决方案:yum ...
- java监测方法运行时间/效率方法
前言: 这周在写一个小项目,虽然小但是是纯调外部接口的,调完了接口还不停的循环接口返回的数据(已转换JSONArray),然后再判断值,再做不同处理,关键是数据量还比较大,这刚做完还没开始上线,测试也 ...
- Android 下实现图片的移动
网上看到的demo,感觉很有趣,但是 实用性不是太强,记录一下. 源码下载地址:请戳这里---------------->
- 常用应用层协议HTTP、RTSP、RTMP比较
HTTP(超文本传输协议).RTSP(Real Time Streaming Protocol实时流传输协议).RTMP(Routing Table Maintenance Protocol路由选择表 ...
- unity, audio falloff
要达到声音随距离衰减的效果,需要使用3D音效,即把Spatial Blend设为1,然后再调节Min Distance和Max Distance(蓝色线框球体),并选择合适的Volume Rollof ...
- min_free_kbytes
http://kernel.taobao.org/index.php?title=Kernel_Documents/mm_sysctl min_free_kbytes 先看官方解释:This is u ...
- selenium python (十)浏览器多窗口处理
#!/usr/bin/python# -*- coding: utf-8 -*-__author__ = 'zuoanvip'#在测试过程中有时候会遇到出现多个浏览器窗口的情况,这时候我们可以通过窗口 ...
- [Asp.net MVC]Asp.net MVC5系列——Razor语法
Razor视图引擎是Asp.net MVC3中新扩展的内容,并且也是它的默认视图引擎.还有另外一种Web Forms视图引擎.通过前面的文章可知在Asp.net mvc5中创建视图,默认使用的是Raz ...