Dora.Interception(github地址,覺得不錯不妨給一顆星)有別于其他AOP框架的最大的一個特點就是采用針對“約定”的攔截器定義方式。如果我們為攔截器定義了一個接口或者基類,那么攔截方法將失去任意注冊依賴服務的靈活性。除此之外,由于我們采用了動態代碼生成的機制,我們可以針對每一個目標方法生成對應的方法調用上下文,所以定義在攔截上下文上針對參數和返回值的提取和設置都是泛型方法,這樣可以避免無謂的裝箱和拆箱操作,進而將引入攔截帶來的性能影響降到最低。
目錄
一、方法調用上下文
二、攔截器類型約定
三、提取調用上下文信息
四、修改輸出參數和返回值
五、控制攔截器的執行順序
六、短路返回
七、構造函數注入
八、方法注入
九、ASP.NET Core應用的適配
一、方法調用上下文
針對同一個方法調用的所有攔截器都是在同一個方法調用上下文中進行的,我們將這個上下文定義成如下這個InvocationContext基類。我們可以利用Target和MethodInfo屬性得到當前方法調用的目標對象和目標方法。泛型的GetArgument和SetArgument用于返回和修改傳入的參數,針對返回值的提取和設置則通過GetReturnValue和SetReturnValue方法來完成。如果需要利用此上下文傳遞數據,可以將其置于Properties屬性返回的字典中。InvocationServices屬性返回針對當前方法調用范圍的IServiceProvider。如果在ASP.NET Core應用中,這個屬性將返回針對當前請求的IServiceProvider,否則Dora.Interception會為每次方法調用創建一個服務范圍,并返回該范圍內的IServiceProvider對象。
public?abstract?class?InvocationContext
{public?object?Target?{?get;?}public?abstract?MethodInfo?MethodInfo?{?get;?}public?abstract?IServiceProvider?InvocationServices?{?get;?}public?IDictionary<object,?object>?Properties?{?get;?}?public?abstract?TArgument?GetArgument<TArgument>(string?name);public?abstract?TArgument?GetArgument<TArgument>(int?index);public?abstract?InvocationContext?SetArgument<TArgument>(string?name,?TArgument?value);public?abstract?InvocationContext?SetArgument<TArgument>(int?index,?TArgument?value);public?abstract?TReturnValue?GetReturnValue<TReturnValue>();public?abstract?InvocationContext?SetReturnValue<TReturnValue>(TReturnValue?value);protected?InvocationContext(object?target);public?ValueTask?ProceedAsync()?=>?Next.Invoke(this);
}
和ASP.NET Core的中間件管道類似,應用到同一個方法上的所有攔截器最終也會根據指定的順序構建成管道。對于某個具體的攔截器來說,是否需要指定后續管道的操作是由它自己決定的。我們知道ASP.NET Core的中間件最終體現為一個Func<RequestDelegate,RequestDelegate>委托,作為輸入的RequestDelegate委托代表后續的中間件管道,當前中間件利用它實現針對后續管道的調用。Dora.Interception針對攔截器采用了更為簡單的設計,將其表示為如下這個InvokeDelegate(相當于RequestDelegate),因為InvocationContext(相當于HttpContext)的ProceedAsync方法直接可以幫助我們完整針對后續管道的調用。
public?delegate?ValueTask?InvokeDelegate(InvocationContext?context);
二、攔截器類型約定
雖然攔截器最終體現為一個InvokeDelegate對象,但是我們傾向于將其定義成一個類型。作為攔截器的類型具有如下的約定:
必須是一個公共的實例類型;
必須包含一個或者多個公共構造函數,針對構造函數的選擇由依賴注入框架決定。被選擇的構造函數可以包含任意參數,參數在實例化的時候由依賴注入容器提供或者手工指定。
攔截方法被定義在命名為InvokeAsync的公共實例方法中,此方法的返回類型為ValueTask,其中包含一個表示方法調用上下文的InvocationContext類型的參數,能夠通過依賴注入容器提供的服務均可以注入在此方法中。
三、提取調用上下文信息
由于攔截器類型的InvokeAsync方法提供了表示調用上下文的InvocationContext參數,我們可以利用它提取基本的調用上下文信息,包括當前調用的目標對象和方法,以及傳入的參數和設置的返回值。如下這個FoobarInterceptor類型表示的攔截器會將上述的這些信息輸出到控制臺上。
public?class?FoobarInterceptor
{public?async?ValueTask?InvokeAsync(InvocationContext?invocationContext){var?method?=?invocationContext.MethodInfo;var?parameters?=?method.GetParameters();Console.WriteLine($"Target:?{invocationContext.Target}");Console.WriteLine($"Method:?{method.Name}({string.Join(",?",?parameters.Select(it?=>?it.ParameterType.Name))})");if?(parameters.Length?>?0){Console.WriteLine("Arguments?(by?index)");for?(int?index?=?0;?index?<?parameters.Length;?index++){Console.WriteLine($"{index}:{invocationContext.GetArgument<object>(index)}");}Console.WriteLine("Arguments?(by?name)");foreach?(var?parameter?in?parameters){var?parameterName?=?parameter.Name!;Console.WriteLine($"{parameterName}:{invocationContext.GetArgument<object>(parameterName)}");}}await?invocationContext.ProceedAsync();if?(method.ReturnType?!=?typeof(void)){Console.WriteLine($"Return:?{invocationContext.GetReturnValue<object>()}");}}
}
我們利用InterceptorAttribute特性將這個攔截器應用到如下這個Calculator類型的Add方法中。由于我們沒有為它定義接口,只能將它定義成虛方法才能被攔截。
public?class?Calculator
{[Interceptor(typeof(FoobarInterceptor))]public?virtual?int?Add(int?x,?int?y)?=>?x?+?y;
}
在如下這段演示程序中,在將Calculator作為服務注冊到創建的ServiceCollection集合后,我們調用BuildInterceptableServiceProvider擴展方法構建一個IServiceCollection對象。在利用它得到Calculator對象之后,我們調用其Add方法。
using?App;
using?Microsoft.Extensions.DependencyInjection;var?calculator?=?new?ServiceCollection().AddSingleton<Calculator>().BuildInterceptableServiceProvider().GetRequiredService<Calculator>();Console.WriteLine($"1?+?1?=?{calculator.Add(1,?1)}");
針對Add方法的調用會被FoobarInterceptor攔截下來,后者會將方法調用上下文信息以如下的形式輸出到控制臺上(源代碼)。
四、修改輸出參數和返回值
攔截器可以篡改輸出的參數值,比如我們將上述的FoobarInterceptor類型改寫成如下的形式,它的InvokeAsync方法會將輸入的兩個參數設置為0(源代碼)。
public?class?FoobarInterceptor
{public?ValueTask?InvokeAsync(InvocationContext?invocationContext){invocationContext.SetArgument("x",?0);invocationContext.SetArgument("y",?0);return?invocationContext.ProceedAsync();}
}
再次執行上面的程序后就會出現1+1=0的現象。
在完成目標方法的調用后,返回值會存儲到上下文中,攔截器也可以將其篡改。如下這個改寫的FoobarInterceptor選擇將返回值設置為0。程序執行后也會出現上面的輸出結果(源代碼)。
public?class?FoobarInterceptor
{public?async?ValueTask?InvokeAsync(InvocationContext?invocationContext){await?invocationContext.ProceedAsync();invocationContext.SetReturnValue(0);}
}
五、控制攔截器的執行順序
攔截器最終被應用到某個方法上,多個攔截器最終會構成一個由InvokeDelegate委托表示的執行管道,構造管道的攔截器的順序可以由指定的序號來控制。如下所示的代碼片段定義了三個派生于同一個基類的攔截器類型(FooInterceptor、BarInterceptor、BazInterceptor),它們會在目標方法之前后輸出當前的類型進而確定它們的執行順序。
public?class?InterceptorBase
{public?async?ValueTask?InvokeAsync(InvocationContext?invocationContext){Console.WriteLine($"[{GetType().Name}]:?Before?invoking");await?invocationContext.ProceedAsync();Console.WriteLine($"[{GetType().Name}]:?After?invoking");}
}public?class?FooInterceptor?:?InterceptorBase?{?}
public?class?BarInterceptor?:?InterceptorBase?{?}
public?class?BazInterceptor?:?InterceptorBase?{?}
我們利用InterceptorAttribute特性將這三個攔截器應用到如下這個Invoker類型的Invoke方法上。指定的Order屬性最終決定了對應的攔截器在構建管道的位置,進而決定了它們的執行順序。
public?class?Invoker
{[Interceptor(typeof(BarInterceptor),?Order?=?2)][Interceptor(typeof(BazInterceptor),?Order?=?3)][Interceptor(typeof(FooInterceptor),?Order?=?1)]public?virtual?void?Invoke()?=>?Console.WriteLine("Invoker.Invoke()");
}
在如下所示的演示程序中,我們按照上述的方式得到Invoker對象,并調用其Invoke方法。
var?invoker?=?new?ServiceCollection().AddSingleton<Invoker>().BuildInterceptableServiceProvider().GetRequiredService<Invoker>();invoker.Invoke();
按照標注InterceptorAttribute特性指定的Order屬性,三個攔截器執行順序依次是:FooInterceptor、BarInterceptor、BazInterceptor,如下所示的輸出結果體現了這一點(源代碼)。
六、短路返回
任何一個攔截器都可以根據需要選擇是否繼續執行后續的攔截器以及目標方法,比如入門實例中的緩存攔截器將緩存結果直接設置為調用上下文的返回值,并不再執行后續的操作。對上面定義的三個攔截器類型,我們將第二個攔截器BarInterceptor改寫成如下的形式。它的InvokeAsync在輸出一段指示性文字后,不再調用上下文的ProceedAsync方法,而是直接返回一個ValueTask對象。
public?class?BarInterceptor
{public?virtual??ValueTask?InvokeAsync(InvocationContext?invocationContext){Console.WriteLine($"[{GetType().Name}]:?InvokeAsync");return?ValueTask.CompletedTask;}
}
再次執行我們的演示程序后會發現FooInterceptor和BarInterceptor會正常執行,但是BazInterceptor目標方法均不會執行(源代碼)。
七、構造函數注入
由于攔截器是由依賴注入容器創建的,其構造函數中可以注入依賴服務。但是攔截器具有全局生命周期,所以我們不能將生命周期模式為Scoped的服務對象注入到構造函數中。我們可以利用一個簡單的實例來演示這一點。我們定義了如下一個攔截器類型FoobarInspector,其構造函數中注入了依賴服務FoobarSerivice。FoobarInspector被采用如下的方式利用InterceptorAttribute特性應用到Invoker類型的Invoke方法上。
public?class?FoobarInterceptor
{public?FoobarInterceptor(FoobarService?foobarService)=>?Debug.Assert(foobarService?!=?null);public?async??ValueTask?InvokeAsync(InvocationContext?invocationContext){Console.WriteLine($"[{GetType().Name}]:?Before?invoking");await?invocationContext.ProceedAsync();Console.WriteLine($"[{GetType().Name}]:?After?invoking");}
}public?class?FoobarService?{?}public?class?Invoker
{[Interceptor(typeof(FoobarInterceptor))]public?virtual?void?Invoke()?=>?Console.WriteLine("Invoker.Invoke()");
}
在如下的演示程序中,我們利用命令行參數(0,1,2)來指定依賴服務FoobarService采用的生命周期,然后將其作為參數調用輔助方法Invoke方法完成必要的服務注冊,利用構建的依賴注入容器提取Invoker對象,并調用應用了FoobarInspector攔截器的Invoke方法。
var?lifetime?=?(ServiceLifetime)int.Parse(args.FirstOrDefault()????"0");
Invoke(lifetime);static?void?Invoke(ServiceLifetime?lifetime)
{Console.WriteLine(lifetime);try{var?services?=?new?ServiceCollection().AddSingleton<Invoker>();services.Add(ServiceDescriptor.Describe(typeof(FoobarService),?typeof(FoobarService),?lifetime));var?invoker?=?services.BuildInterceptableServiceProvider().GetRequiredService<Invoker>();invoker.Invoke();}catch?(Exception?ex){Console.WriteLine(ex.Message);}
}
我們以命令行參數的形式啟動程序,并指定三種不同的生命周期模式。從輸出結果可以看出,如果注冊的FoobarService服務采用Scoped生命周期模式會拋出異常(源代碼)。
八、方法注入
如果FoobarInspector依賴一個Scoped服務,或者依賴的服務采用Transient生命周期模式,但是希望在每次調用的時候創建新的對象(如果將生命周期模式設置為Transient,實際上是希望采用這樣的服務消費方式)。此時可以利用InvocationContext的InvocationServices返回的IServiceProvider對象。在如下的實例演示中,我們定義了派生于ServiceBase 的三個將會注冊為對應生命周期的服務類型SingletonService 、ScopedService 和TransientService 。為了確定依賴服務實例被創建和釋放的時機,ServiceBase實現了IDisposable接口,并在構造函數和Dispose方法中輸出相應的文字。在攔截器類型FoobarInterceptor的InvokeAsync方法中,我們利用InvocationContext的InvocationServices返回的IServiceProvider對象兩次提取這三個服務實例。FoobarInterceptor依然應用到Invoker類型的Invoke方法中。
public?class?FoobarInterceptor
{public?async??ValueTask?InvokeAsync(InvocationContext?invocationContext){var?provider?=?invocationContext.InvocationServices;_?=?provider.GetRequiredService<SingletonService>();_?=?provider.GetRequiredService<SingletonService>();_?=?provider.GetRequiredService<ScopedService>();_?=?provider.GetRequiredService<ScopedService>();_?=?provider.GetRequiredService<TransientService>();_?=?provider.GetRequiredService<TransientService>();Console.WriteLine($"[{GetType().Name}]:?Before?invoking");await?invocationContext.ProceedAsync();Console.WriteLine($"[{GetType().Name}]:?After?invoking");}
}public?class?ServiceBase?:?IDisposable
{public?ServiceBase()=>Console.WriteLine($"{GetType().Name}.new()");public?void?Dispose()?=>?Console.WriteLine($"{GetType().Name}.Dispose()");
}public?class?SingletonService?:?ServiceBase?{?}
public?class?ScopedService?:?ServiceBase?{?}
public?class?TransientService?:?ServiceBase?{?}public?class?Invoker
{[Interceptor(typeof(FoobarInterceptor))]public?virtual?void?Invoke()?=>?Console.WriteLine("Invoker.Invoke()");
}
在如下的演示程序中,我們將三個服務按照對應的生命周期模式添加到創建的ServiceCollection集合中。在構建出作為依賴注入容器的IServiceProvider對象后,我們利用它提取出Invoker對象,并先后兩次調用應用了攔截器的Invoke方法。為了釋放所有由ISerivceProvider對象提供的服務實例,我們調用了它的Dispose方法。
var?provider?=?new?ServiceCollection().AddSingleton<SingletonService>().AddScoped<ScopedService>().AddTransient<TransientService>().AddSingleton<Invoker>().BuildInterceptableServiceProvider();
using?(provider?as?IDisposable)
{var?invoker?=?provider?.GetRequiredService<Invoker>();invoker.Invoke();Console.WriteLine();invoker.Invoke();
}
程序運行后會在控制臺上輸出如下的結果,可以看出SingletonService 對象只會創建一次,并最終在作為跟容器的ISerivceProvider對象被釋放時隨之被釋放。ScopedSerivce對象每次方法調用都會創建一次,并在調用后自動被釋放。每次提取TransientService 都會創建一個新的實例,它們會在方法調用后與ScopedSerivce對象一起被釋放(源代碼)。
其實利用InvocationServices提取所需的依賴服務并不是我們推薦的編程方式,更好的方式是以如下的方式將依賴服務注入攔截器的InvokeAsync方法中。上面演示程序的FoobarInterceptor改寫成如下的方式后,執行后依然會輸出如上的結果(源代碼)。
public?class?FoobarInterceptor
{public?async??ValueTask?InvokeAsync(InvocationContext?invocationContext,SingletonService?singletonService1,?SingletonService?singletonService2,ScopedService?scopedService1,?ScopedService?scopedService2,TransientService?transientService1,?TransientService?transientService2){Console.WriteLine($"[{GetType().Name}]:?Before?invoking");await?invocationContext.ProceedAsync();Console.WriteLine($"[{GetType().Name}]:?After?invoking");}
}
九、ASP.NET Core應用的適配
對于上面演示實例來說,Scoped服務所謂的“服務范圍”被綁定為單次方法調用,但是在ASP.NET Core應用應該綁定為當前的請求上下文,Dora.Interception對此做了相應的適配。我們將上面定義的FoobarInterceptor和Invoker對象應用到一個ASP.NET Core MVC程序中。為此我們定義了如下這個HomeController,其Action方法Index中注入了Invoker對象,并先后兩次調用了它的Invoke方法。
public?class?HomeController
{[HttpGet("/")]public?string?Index([FromServices]?Invoker?invoker){invoker.Invoke();Console.WriteLine();invoker.Invoke();return?"OK";}
}
MVC應用的啟動程序如下。
var?builder?=?WebApplication.CreateBuilder(args);
builder.Host.UseInterception();
builder.Services.AddLogging(logging=>logging.ClearProviders()).AddSingleton<Invoker>().AddSingleton<SingletonService>().AddScoped<ScopedService>().AddTransient<TransientService>().AddControllers();
var?app?=?builder.Build();
app.UseRouting().UseEndpoints(endpint?=>?endpint.MapControllers());
app.Run();
啟動程序后針對根路徑“/”(只想HomeController的Index方法)的請求(非初次請求)會在服務端控制臺上輸出如下的結果,可以看出ScopedSerivce對象針對每次請求只會被創建一次。