注冊攔截器旨在解決如何將攔截器應用到目標方法的問題。在我看來,針對攔截器的注冊應該是明確而精准的,也就是我們提供的注冊方式應該讓攔截器准確地應用到期望的目標方法上,不能多也不能少。如果注冊的方式過於模糊,很容易將攔截器應用到非目標方法上。按照這個原則,一些AOP框架提供的針對類型命名空間、類型或者成員名稱前(后)綴的攔截器映射策略其實都是不嚴謹的。Dora.Interception只提供兩種嚴謹的攔截器注冊方式,一種前面介紹的針對特性標注的方式,另一種就是本篇介紹的針對策略的方式。
一、AddPolicy
攔截策略表達的是:將一個提供攔截器的IInterceptorProvider對象應用到某個目標類型的某一個或者多個方法或者屬性成員上。如下所示的是在《編程體驗》中定義的攔截策略,它表達的意圖是:將CacheReturnValueAttribute應用到SystemClock類型的GetCurrentTime方法上,並且將Order屬性設置為1。
public class Program { public static void Main(string[] args) { Host.CreateDefaultBuilder() .UseInterceptableServiceProvider(configure: Configure) .ConfigureWebHostDefaults(buider => buider.UseStartup<Startup>()) .Build() .Run(); static void Configure(InterceptionBuilder interceptionBuilder) { interceptionBuilder.AddPolicy(policyBuilder => policyBuilder .For<CacheReturnValueAttribute>(order: 1, cache => cache .To<SystemClock>(target => target .IncludeMethod(clock => clock.GetCurrentTime(default))))); } } }
通過上面的代碼片段可以看出,攔截策略是通過調用InterceptionBuilder 的AddPolicy擴展方法注冊的。如下面的代碼片段所示,該方法具有一個Action<IInterceptionPolicyBuilder>類型的參數,具體的攔截策略最終是利用IInterceptionPolicyBuilder對象來定義的。
public static partial class InterceptionBuilderExtensions { public static InterceptionBuilder AddPolicy(this InterceptionBuilder builder, Action<IInterceptionPolicyBuilder> configure); }
二、IInterceptionPolicyBuilder
Dora.Interception最終利用InterceptionPolicy對象來表示攔截策略,如下面的代碼片段所示,IInterceptionPolicyBuilder的Build方法最終會生成該對象。具體針對攔截策略的定義體現在針對For<TInterceptorProvider>方法的調用上。攔截策略是以提供攔截器的IInterceptorProvider對象來基礎,For<TInterceptorProvider>方法直接利用泛型參數來提供具體的IInterceptorProvider類型。
public interface IInterceptionPolicyBuilder { IServiceProvider ServiceProvider { get; } InterceptionPolicy Build(); IInterceptionPolicyBuilder For<TInterceptorProvider>(int order, Action<IInterceptorProviderPolicyBuilder> configureTargets, params object[] arguments) where TInterceptorProvider: IInterceptorProvider; }
For<TInterceptorProvider>方法的Order屬性表示提供攔截器在最終攔截器管道的位置,最終的arguments參數用來提供創建攔截器對象時所需的參數列表。如果構造函數的所有參數都可以利用依賴注入容器(對應於IInterceptionPolicyBuilder的ServiceProvider屬性)來提供,這個參數是可以缺省的。For<TInterceptorProvider>方法的核心是作為第二個參數的Action<IInterceptorProviderPolicyBuilder> 對象,它決定了指定的IInterceptorProvider應該應用到那個類型的哪些成員上。
三、IInterceptorProviderPolicyBuilder
IInterceptorProviderPolicyBuilder的To<TTarget>方法會將當前指定的IInterceptorProvider應用到通過泛型參數表示的目標類型上,至於具體應用到哪些方法或者屬性成員上,則由提供的Action<ITargetPolicyBuilder<TTarget>> 對象作進一步設置。
public interface IInterceptorProviderPolicyBuilder { InterceptorProviderPolicy Build(); IInterceptorProviderPolicyBuilder To<TTarget>(Action<ITargetPolicyBuilder<TTarget>> configure); }
四、ITargetPolicyBuilder<T>
ITargetPolicyBuilder<T>(泛型類型表示IInterceptorProvider應用的目標類型)旨在解決成員選擇的問題。我們可以調用IncludeMethod或者IncludeProperty<TValue>顯式指定目標方法或者屬性。如果我們需要應用到所有可被攔截的方法和屬性,可以調用IncludeAllMembers方法,如果需要排除少數幾個方法或者屬性成員,可以調用ExcludeMethod或者ExcludeMethod<TValue>方法。由於這些方法利用表達式而不是名稱來選擇目標成員,所以它不但能夠避免方法名稱寫錯的情況,還能解決方法重載的問題。
public interface ITargetPolicyBuilder<T> { TargetTypePolicy Build(); ITargetPolicyBuilder<T> IncludeAllMembers(); ITargetPolicyBuilder<T> IncludeMethod(Expression<Action<T>> methodInvocation); ITargetPolicyBuilder<T> ExcludeMethod(Expression<Action<T>> methodInvocation); ITargetPolicyBuilder<T> IncludeProperty<TValue>(Expression<Func<T, TValue>> propertyAccessor, PropertyMethod propertyMethod); ITargetPolicyBuilder<T> ExcludeProperty<TValue>(Expression<Func<T, TValue>> propertyAccessor, PropertyMethod propertyMethod); } [Flags] public enum PropertyMethod { Get = 1, Set = 2, Both = 3 }
五、一個完整的攔截策略
通過上面Dora.Interception提供的API,基本上能夠將任何請問的攔截器注冊需求定義成相應的攔截策略。如下所示的攔截策略綜合使用了上述所有的方法。
public static void Main(string[] args) { Host.CreateDefaultBuilder() .UseInterceptableServiceProvider(configure: Configure) .ConfigureWebHostDefaults(buider => buider.UseStartup<Startup>()) .Build() .Run(); static void Configure(InterceptionBuilder buidler) => buidler.AddPolicy(policy => policy .For<FooInterceptorAttribute>(1, interceptor => interceptor .To<FoobarService>(target => target .IncludeMethod(foobar => foobar.InterceptableInvokeAsync()) .IncludeProperty(foobar => foobar.Both, PropertyMethod.Both) .IncludeProperty(foobar => foobar.Get, PropertyMethod.Get) .IncludeProperty(foobar => foobar.Set, PropertyMethod.Set)) .To<FoobazService>(targetBuilder => targetBuilder .IncludeMethod(foobar => foobar.InterceptableInvokeAsync()) .IncludeProperty(foobar => foobar.Both, PropertyMethod.Both) .IncludeProperty(foobar => foobar.Get, PropertyMethod.Get) .IncludeProperty(foobar => foobar.Set, PropertyMethod.Set))) .For<BarInterceptorAttribute>(2, interceptor => interceptor .To<FoobarService>(target => target .IncludeMethod(foobar => foobar.InterceptableInvokeAsync()) .IncludeProperty(foobar => foobar.Both, PropertyMethod.Both) .IncludeProperty(foobar => foobar.Get, PropertyMethod.Get) .IncludeProperty(foobar => foobar.Set, PropertyMethod.Set)) .To<BarbazService>(targetBuilder => targetBuilder .IncludeMethod(foobar => foobar.InterceptableInvokeAsync()) .IncludeProperty(foobar => foobar.Both, PropertyMethod.Both) .IncludeProperty(foobar => foobar.Get, PropertyMethod.Get) .IncludeProperty(foobar => foobar.Set, PropertyMethod.Set))) .For<BazInterceptorAttribute>(3, interceptor => interceptor .To<FoobazService>(target => target .IncludeAllMembers() .ExcludeMethod(foobar => foobar.NonInterceptableInvokeAsync()) .ExcludeProperty(foobar => foobar.NonInterceptable, PropertyMethod.Both) .ExcludeProperty(foobar => foobar.Get, PropertyMethod.Set) .ExcludeProperty(foobar => foobar.Set, PropertyMethod.Get)) .To<BarbazService>(targetBuilder => targetBuilder .IncludeMethod(foobar => foobar.InterceptableInvokeAsync()) .IncludeProperty(foobar => foobar.Both, PropertyMethod.Both) .IncludeProperty(foobar => foobar.Get, PropertyMethod.Get) .IncludeProperty(foobar => foobar.Set, PropertyMethod.Set)))); }
六、策略腳本化
考慮到攔截策略可能需要動態調整,但是我們又不希望對應用進行重新編譯和發布,所以我們可以考慮將攔截策略定義在配置文件中。但是配置文件在表達“目標成員選擇”方面會很繁瑣,因為如果好標識某個方法,不僅需要指定方法名稱,還需要指定所有參數列表類型。我們最終將攔截策略定義成C#腳本來解決這個問題。如果已經將攔截策略定義在一個C#腳本文件中,我們可以調用InterceptionBuilder如下這個AddPolicy擴展方法重載。
public static partial class InterceptionBuilderExtensions { public static InterceptionBuilder AddPolicy(this InterceptionBuilder builder, string fileName, Action<PolicyFileBuilder> configure = null); }
除了指定作為策略文件的路徑之外,我們還可以提供一個Action<PolicyFileBuilder>對象。如下面的代碼片段所示,PolicyFileBuilder提供了三個方法,SetFileProvider方法用來設置用來讀取攔截策略文件的IFileProvider對象(默認為針對當前目錄的PhysicalFileProvider),AddReferences和AddImports方法則用來添加程序集引用和導入命名空間。
public sealed class PolicyFileBuilder { public IFileProvider FileProvider {get; } public Assembly[] References {get; } public string[] Imports {get; } public PolicyFileBuilder SetFileProvider(IFileProvider fileProvider); public PolicyFileBuilder AddReferences(params Assembly[] references); public PolicyFileBuilder AddImports(params string[] namespaces); public string ReadAllText(string fileName); }
當我們在定義攔截策略腳本的時候,它可以獲取一個用來構建攔截器策略的IInterceptionPolicyBuilder對象的全局變量,該全局變量被命名為policyBuilder。對於上面一節中定義的攔截策略,我們可以采用如下的方式將它腳本話,兩者的內容幾乎是完全一致的。
policyBuilder .For<FooInterceptorAttribute>(1, interceptor => interceptor .To<FoobarService>(target => target .IncludeMethod(foobar => foobar.InterceptableInvokeAsync()) .IncludeProperty(foobar => foobar.Both, PropertyMethod.Both) .IncludeProperty(foobar => foobar.Get, PropertyMethod.Get) .IncludeProperty(foobar => foobar.Set, PropertyMethod.Set)) .To<FoobazService>(targetBuilder => targetBuilder .IncludeMethod(foobar => foobar.InterceptableInvokeAsync()) .IncludeProperty(foobar => foobar.Both, PropertyMethod.Both) .IncludeProperty(foobar => foobar.Get, PropertyMethod.Get) .IncludeProperty(foobar => foobar.Set, PropertyMethod.Set))) .For<BarInterceptorAttribute>(2, interceptor => interceptor .To<FoobarService>(target => target .IncludeMethod(foobar => foobar.InterceptableInvokeAsync()) .IncludeProperty(foobar => foobar.Both, PropertyMethod.Both) .IncludeProperty(foobar => foobar.Get, PropertyMethod.Get) .IncludeProperty(foobar => foobar.Set, PropertyMethod.Set)) .To<BarbazService>(targetBuilder => targetBuilder .IncludeMethod(foobar => foobar.InterceptableInvokeAsync()) .IncludeProperty(foobar => foobar.Both, PropertyMethod.Both) .IncludeProperty(foobar => foobar.Get, PropertyMethod.Get) .IncludeProperty(foobar => foobar.Set, PropertyMethod.Set))) .For<BazInterceptorAttribute>(3, interceptor => interceptor .To<FoobazService>(target => target .IncludeAllMembers() .ExcludeMethod(foobar => foobar.NonInterceptableInvokeAsync()) .ExcludeProperty(foobar => foobar.NonInterceptable, PropertyMethod.Both) .ExcludeProperty(foobar => foobar.Get, PropertyMethod.Set) .ExcludeProperty(foobar => foobar.Set, PropertyMethod.Get)) .To<BarbazService>(targetBuilder => targetBuilder .IncludeMethod(foobar => foobar.InterceptableInvokeAsync()) .IncludeProperty(foobar => foobar.Both, PropertyMethod.Both) .IncludeProperty(foobar => foobar.Get, PropertyMethod.Get) .IncludeProperty(foobar => foobar.Set, PropertyMethod.Set)));
AOP框架Dora.Interception 3.0 [1]: 編程體驗
AOP框架Dora.Interception 3.0 [2]: 實現原理
AOP框架Dora.Interception 3.0 [3]: 攔截器設計
AOP框架Dora.Interception 3.0 [4]: 基於特性的攔截器注冊
AOP框架Dora.Interception 3.0 [5]: 基於策略的攔截器注冊
AOP框架Dora.Interception 3.0 [6]: 自定義攔截器注冊方式