在Dora.Interception(github地址,覺得不錯不妨給一顆星)中按照約定方式定義的攔截器可以採用多種方式註冊到目標方法上。本篇文章介紹最常用的基於「特性標註」的攔截器註冊方式,下一篇會介紹另一種基於(Lambda)表示式的註冊方式。如果原生定義的這兩種註冊方式不能滿足要求,利用框架提供的擴充套件,我們可以完成任何你想要的攔截器註冊手段。(拙著《ASP.NET Core 6框架揭祕》於日前上市,加入讀者群享6折優惠)
目錄
一、InterceptorAttribute 特性
二、指定構造攔截器的參數列
三、將攔截器型別定義成特性
四、合法性檢驗
五、針對型別、屬性的標註
六、攔截的遮蔽
攔截器型別可以利用如下這個InterceptorAttribute特性應用到標註的型別、屬性和方法上。除了通過Interceptor屬性指定攔截器型別之外,我們還可以利用Order屬性控制攔截器的執行順序,該屬性預設值為0。該特性的Arguments用來提供構造攔截器物件的引數。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true, Inherited = false)] public class InterceptorAttribute : Attribute { public Type Interceptor { get; } public object[] Arguments { get; } public int Order { get; set; } public InterceptorAttribute(params object[] arguments) : public InterceptorAttribute(Type? interceptor, params object[] arguments); }
攔截器物件是通過依賴注入容器提供的,容器能夠自動提供注入到建構函式中物件。如果建構函式包含額外的引數,對應的引數值就需要利用InterceptorAttribute 特性的Arguments屬性來提供,此屬性由建構函式的arguments引數提供。
public class FoobarInterceptor { public string Name { get; } public FoobarInterceptor(string name, IFoobar foobar) { Name = name; Debug.Assert(foobar is not null); } public ValueTask InvokeAsync(InvocationContext invocationContext) { Console.WriteLine($"FoobarInterceptor '{Name}' is invoked."); return invocationContext.ProceedAsync(); } } public interface IFoobar { } public class Foobar : IFoobar { }
對於如上這個攔截器型別FoobarInterceptor,其建構函式定義了一個字串的引數name用來指定攔截器的名稱,當我利用InterceptorAttribute 特性將此攔截器應用到Invoker型別的Invoke1和Invoke2方法上是,就需要按照如下的方式指定具體的名稱(Interceptor1和Interceptor2)。
public class Invoker { [FoobarInterceptor("Interceptor1")] public virtual void Invoke1() => Console.WriteLine("Invoker.Invoke1()"); [FoobarInterceptor("Interceptor2")] public virtual void Invoke2() => Console.WriteLine("Invoker.Invoke2()"); }
我們按照如下的方式呼叫Invoker物件的Invoke1和Invoke2方法。
var invoker = new ServiceCollection()
.AddSingleton<Invoker>()
.AddSingleton<IFoobar, Foobar>()
.BuildInterceptableServiceProvider()
.GetRequiredService<Invoker>();
invoker.Invoke1();
invoker.Invoke2();
程式執行後,攔截器會以如下的形式將自身的名稱輸出到控制檯上(原始碼)。
其實我們可以讓定義的攔截器型別派生於InterceptorAttribute 特性,這樣就可以直接將它標註到目標型別、屬性和方法上。比如上面這個FoobarInterceptor型別可以改寫成如下的形式。
public class FoobarInterceptorAttribute: InterceptorAttribute { public string Name { get; } public FoobarInterceptorAttribute(string name) => Name = name; public ValueTask InvokeAsync(InvocationContext invocationContext) { Console.WriteLine($"FoobarInterceptor '{Name}' is invoked."); return invocationContext.ProceedAsync(); } }
那麼它就可以按照如下的方式標註到Invoker型別的兩個方法上(原始碼)。
public class Invoker { [FoobarInterceptor("Interceptor1")] public virtual void Invoke1() => Console.WriteLine("Invoker.Invoke1()"); [FoobarInterceptor("Interceptor2")] public virtual void Invoke2() => Console.WriteLine("Invoker.Invoke2()"); }
只有介面方法和虛方法才能被攔截,Dora.Interception針對攔截器的應用提供瞭如下的驗證邏輯:
public class Foo { [FoobarInterceptor] public void M() { } } public class Bar { [FoobarInterceptor] public object? P { get; set; } } [FoobarInterceptor] public class Baz { public void M() { } }
對於上面定義的三個型別,Foo的M方法和Bar的P屬性均是無法被攔截,Baz型別並沒有可以被攔截的方法。我們採用如下的程式測試上述的檢驗邏輯。
GetService<Foo>(); GetService<Bar>(); GetService<Baz>(); static void GetService<T>() where T:class { try { Console.WriteLine($"{typeof(T).Name}:"); _ = new ServiceCollection() .AddSingleton<T>() .BuildInterceptableServiceProvider() .GetRequiredService<T>(); Console.WriteLine("OK"); } catch (Exception ex) { Console.WriteLine(ex.Message); } }
程式執行後會在控制檯上輸出如下的結果,可以看出只有將攔截器應用到不合法的方法和屬性上才會丟擲異常(原始碼)。
我們利用如下這個攔截器型別FoobarInterceptorAttribute 來演示將攔截器應用到型別和屬性上。該攔截器型別派生於InterceptorAttribute特性,並在執行的時候輸出當前的方法。
public class FoobarInterceptorAttribute : InterceptorAttribute { public ValueTask InvokeAsync(InvocationContext invocationContext) { var method = invocationContext.MethodInfo; Console.WriteLine($"{method.DeclaringType!.Name}.{method.Name} is intercepted."); return invocationContext.ProceedAsync(); } }
我們將FoobarInterceptorAttribute 特性標註到Foo型別上,後者定義的M1方法和P1屬性是可以被攔截的,但是M2方法和P2屬性則不能。FoobarInterceptorAttribute 特性還被應用到Bar型別的P1屬性以及P2屬性的Set方法上。
[FoobarInterceptor] public class Foo { public virtual void M1() { } public void M2() { } public virtual object? P1 { get; set; } public object? P2 { get; set; } } public class Bar { [FoobarInterceptor] public virtual object? P1 { get; set; } public virtual object? P2 { get; [FoobarInterceptor] set; } }
我們利用如下的程式來檢驗針對Foo和Bar物件所有方法和屬性的呼叫,那麼被攔截器攔截下來。
var provider = new ServiceCollection() .AddSingleton<Foo>() .AddSingleton<Bar>() .BuildInterceptableServiceProvider(); var foo = provider.GetRequiredService<Foo>(); var bar = provider.GetRequiredService<Bar>(); foo.M1(); foo.M2(); foo.P1 = null; _ = foo.P1; foo.P2 = null; _ = foo.P2; Console.WriteLine(); bar.P1 = null; _ = bar.P1; bar.P2 = null; _ = bar.P2;
程式執行之後會在控制檯上輸出如下的結果(原始碼)。
如果某個攔截器需要被應用大某個型別的絕大部分成員,我們可以選擇「排除法」:將攔截器應用到該型別上,將某些非目標成員遮蔽掉。還有一種情況下,如果我們確定某些型別或者方法不能被攔截(比如會在一個迴圈中頻繁呼叫),又擔心一些「模糊」的攔截器註冊方法將它們與某些攔截器錯誤地關聯在一起,此時我們可以選擇將其攔截功能顯式遮蔽掉。
針對攔截的遮蔽可以通過在型別、屬性、方法設定程式集上標註NonInterceptableAttribute特性。由於遮蔽功能具有最高優先順序,一旦將此特性應用到某個型別上,該型別上的所有成員均不會被攔截。如果被標註到屬性上,其Get和Set方法也不會被攔截。具有如下定義的Foo和Bar型別的所有方法和屬性都不會被攔截(原始碼)。
[FoobarInterceptor] public class Foo { [NonInterceptable] public virtual void M() { } [NonInterceptable] public virtual object? P1 { get; set; } public virtual object? P2 { [NonInterceptable] get; set; } } [NonInterceptable] public class Bar { [FoobarInterceptor] public virtual void M() { } [FoobarInterceptor] public virtual object? P { get; set; } }
全新升級的AOP框架Dora.Interception[1]: 程式設計體驗
全新升級的AOP框架Dora.Interception[2]: 基於約定的攔截器定義方式
全新升級的AOP框架Dora.Interception[3]: 基於「特性標註」的攔截器註冊方式
全新升級的AOP框架Dora.Interception[4]: 基於「Lambda表示式」的攔截器註冊方式
全新升級的AOP框架Dora.Interception[5]: 實現任意的攔截器註冊方式
全新升級的AOP框架Dora.Interception[6]: 框架設計和實現原理