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: }
文章列表
留言列表