本系列前面的五篇文章主要介紹Dora.Interception(github地址,覺得不錯不妨給一顆星)的程式設計模式以及對它的擴充套件客製化,現在我們來聊聊它的設計和實現原理。(拙著《ASP.NET Core 6框架揭祕》6折優惠,首印送簽名專屬書籤)。
目錄
一、呼叫鏈抽象
二、基於約定的攔截器定義
三、基於呼叫上下文的依賴注入容器
四、攔截器的提供
五、呼叫鏈的構建
六、方法攔截的實現原理
七、依賴注入框架的整合
八、看看生成的代理類
從設計模式來看,Dora.Interception採用了「職責鏈」模式。我們將應用到同一個方法的多個攔截器以及針對目標方法的呼叫構建成如下所示的「呼叫鏈」。呼叫鏈在執行過程中共用同一個「呼叫上下文」,後者提供當前呼叫的上下文資訊,比如目標物件、呼叫方法、輸出引數和返回值等。每個攔截器不僅可以利用這些上下文資訊執行對應的操作,還可以直接利用此上下文修改引數和返回值,並且自行決定是否繼續執行後續呼叫。
我們定義瞭如下這個抽象類InvocationContext來表示上述的呼叫上下文。對於引數/返回值的提取,我們設計成抽象方法以避免因裝箱/拆箱帶來的效能問題。攔截器針對其他服務的依賴是一個基本的需求,所以我們為InvocationContext定義了一個InvocationServices屬性來提供針對當前呼叫的IServiceProvider物件。在預設情況下,我們會為每次呼叫建立一個服務範圍,並利用此範圍的IServiceProvider物件作為這個InvocationServices屬性的值。但是對於ASP.NET Core應用,我們會直接使用針對當前請求的IServiceProvider物件。
public abstract class InvocationContext { public object Target { get; } = default!; 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); internal InvokeDelegate Next { get; set; } = default!; public ValueTask ProceedAsync() => Next.Invoke(this); }
既然有了這樣一個能夠體現當前方法呼叫上下文的InvocationContext型別,那麼上述的「呼叫量」就可以表示成如下這個InvokeDelegate委託。熟悉ASP.NET Core的讀者可以看出Dora.Interception的呼叫鏈設計與ASP.NET Core框架的「中介軟體管道」幾乎一致,InvocationContext和InvokeDelegate分別對應後者的HttpContext和RequestDelegate。
public delegate ValueTask InvokeDelegate(InvocationContext context);
既然將ASP.NET Core作為類比,Dora.Interception的攔截器自然就對應著ASP.NET Core的中介軟體了。我們知道後者體現為一個Func<RequestDelegate, RequestDelegate>委託,作為輸入的RequestDelegate代表由後續中介軟體構建的請求處理管道,每個中介軟體需要利用此物件將請求分發給後續管道進行處理。Dora.Interception採用了更為簡單的設計,我們將攔截器也表示成上述的InvokeDelegate委託,因為針對後續攔截器以及目標方法的呼叫可以利用代表呼叫上下文的InvocationContext物件的ProceedAsync方法來完成。如上面的程式碼片段所示,InvocationContext具有一個名為Next的內部屬性用來表示呼叫呼叫鏈的下一個InvokeDelegate物件,每個攔截器在執行之前,此屬性都會預先被設定,ProceedAsync方法呼叫的正式此屬性返回的InvokeDelegate物件。
雖然攔截器最終由一個InvokeDelegate委託來表示,但是將其定義成一個普通的型別具有更好的程式設計體驗。考慮到動態注入依賴服務的需要,我們並沒有為攔截器定義任何的介面和基礎類別,而是採用基於約定的定義方式。這一點與ASP.NET Core基於約定的中介軟體定義方法類似,由於我們的攔截器委託比中介軟體委託要簡潔,基於約定的攔截器自然比定義中介軟體要簡單。中介軟體定義按照如下的約定即可:
按照約定定義的中介軟體型別或者此型別的物件最終都需要轉換成一個InvokeDelegate物件,此項功能體現在IConventionalInterceptorFactory介面的兩個CreateInterceptor過載方法上。第一個過載的arguments將被作為呼叫建構函式的引數,對於依賴注入容器無法提供的引數必須在此指定。內部型別ConventionalInterceptorFactory以表示式樹的形式實現了這個介面,具體實現就不在這裡展示了,有興趣的朋友可以檢視原始碼。
public interface IConventionalInterceptorFactory { InvokeDelegate CreateInterceptor(Type interceptorType, params object[] arguments); InvokeDelegate CreateInterceptor(object interceptor); } internal sealed class ConventionalInterceptorFactory : IConventionalInterceptorFactory { public InvokeDelegate CreateInterceptor(Type interceptorType, params object[] arguments); public InvokeDelegate CreateInterceptor(object interceptor); }
InvocationContext的InvocationServices屬性返回針對當前呼叫上下文的依賴注入容器。在預設的情況下,我們會在建立InvocationContext上下文的時候建立一個服務範圍,並使用此範圍的IServiceProvider物件作為其InvocationServices屬性。注入到InvokeAsync方法中的依賴服務是在呼叫時利用此IServiceProvider物件動態提供的,我們也可以在實現的InvokeAsync方法中安全的使用此物件來提供所需的服務範例。由於服務範圍會在呼叫結束之後被自動終結,所以非單例服務範例能夠被正常回收。
如下所示的IInvocationServiceScopeFactory介面表示用來建立上述服務範圍的工廠,代表服務範圍的IServiceScope物件由其CreateInvocationScope方法建立,InvocationServiceScopeFactory是對該介面的預設實現。
public interface IInvocationServiceScopeFactory { IServiceScope CreateInvocationScope(); } internal class InvocationServiceScopeFactory : IInvocationServiceScopeFactory { private readonly IApplicationServicesAccessor _applicationServicesAccessor; public InvocationServiceScopeFactory(IApplicationServicesAccessor applicationServicesAccessor) => _applicationServicesAccessor = applicationServicesAccessor ?? throw new ArgumentNullException(nameof(applicationServicesAccessor)); public IServiceScope CreateInvocationScope()=> _applicationServicesAccessor.ApplicationServices.CreateScope(); }
如果在一個ASP.NET Core應用中,我們因為針對當前請求的IServiceProvider(RequestServices)物件作為呼叫上下文的InvocationServices也許更為適合,所以在ASP.NET Core應用中註冊的IInvocationServiceScopeFactory實現型別為如下這個RequestServiceScopeFactory 型別。
internal class RequestServiceScopeFactory : IInvocationServiceScopeFactory { private readonly InvocationServiceScopeFactory _factory; private readonly IHttpContextAccessor _httpContextAccessor; private NullServiceScope? _nullServiceScope; public RequestServiceScopeFactory(IServiceProvider serviceProvider, IHttpContextAccessor httpContextAccessor) { _factory = ActivatorUtilities.CreateInstance<InvocationServiceScopeFactory>(serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider))); _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor)); } public IServiceScope CreateInvocationScope() { _nullServiceScope ??= new NullServiceScope (_httpContextAccessor); return _httpContextAccessor.HttpContext == null? _factory.CreateInvocationScope(): _nullServiceScope; } private class NullServiceScope : IServiceScope { private readonly IHttpContextAccessor _httpContextAccessor; public NullServiceScope(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor; public IServiceProvider ServiceProvider => _httpContextAccessor.HttpContext?.RequestServices!; public void Dispose() { } } }
我們利用如下這個IInterceptorProvider介面來表示攔截器的「提供者」,它定義的GetInterceptors方法為指定型別的方法提供一組可供排序的攔截器,該方法返回一組Sortable<InvokeDelegate>物件,每個Sortable<InvokeDelegate>物件的Value屬性代表作為攔截器的InvokeDelegate委託,Order屬性用來對攔截器進行排序。
public interface IInterceptorProvider { bool CanIntercept(Type targetType, MethodInfo method, out bool suppressed); IEnumerable<Sortable<InvokeDelegate>> GetInterceptors(Type targetType, MethodInfo method); void Validate(Type targetType, Action<MethodInfo> methodValidator, Action<PropertyInfo> propertyValidator) ; } public sealed class Sortable<T> { public int Order { get; } public T Value { get; set; } public Sortable(int order, T value) { Order = order; Value = value; } }
IInterceptorProvider旨在為指定的方法提供攔截器,所以它體現的是針對攔截器的註冊,即採用怎樣的方式將攔截器應用到期望的目標方法上。根據Dora.Interception的實現原理,並不是每一個方法都能被攔截,所以我們為IInterceptorProvider定義了一個Validate方法用來驗證被應用到指定方法或者屬性上的攔截器是否有效。具體的驗證邏輯無需自行實現,只需要呼叫該方法提供的兩個作為驗證器的引數(methodValidator和propertyValidator)就可以了。這樣做的好處是今早確定我們針對某個方法的攔截意圖是否能夠生效,Dora.Interception提供的兩種原生的實現均實現了驗證功能,對於自定義的實現,可以根據需要決定是否需要驗證。
IInterceptorProvider介面還定義了CanIntercept方法用來確定指定型別的方法能否被攔截。一般來說,如果指定方法上沒有註冊攔截器,方法自然不會被攔截。但是很多時候我們需要顯式遮蔽掉某個方法、屬性甚至型別的攔截特性,我們認為這樣的設定具有最高優先順序,所以即使被註冊了攔截器(包括被其他IInterceptorProvider註冊)也不能被攔截,輸出引數suppressed的作用就體現在這裡。
由於攔截器大部分情況下都採用基於約定的型別來定義,所以針對攔截器的註冊對於終端使用者來說也應該針對攔截器型別或者範例進行,所以我們通過實現IInterceptorProvider介面定義瞭如下這個InterceptorProviderBase基礎類別,它利用InterceptorFactory屬性返回的IConventionalInterceptorFactory方便我們將按照約定定義的攔截器型別或對應的物件轉換成標InvokeDelegate。
public abstract class InterceptorProviderBase : IInterceptorProvider { public IConventionalInterceptorFactory InterceptorFactory { get; } protected InterceptorProviderBase(IConventionalInterceptorFactory interceptorFactory) => InterceptorFactory = interceptorFactory ?? throw new ArgumentNullException(nameof(interceptorFactory)); public abstract bool CanIntercept(Type targetType, MethodInfo method, out bool suppressed); public abstract IEnumerable<Sortable<InvokeDelegate>> GetInterceptors(Type targetType, MethodInfo method); }
Dora.Interception預設實現了兩種攔截器的註冊方法就是由對應的IInterceptorProvider實現型別達成的。具體來說,DataAnnotationInterceptorProvider實現了針對特性標註的攔截器註冊,基於Lambda表示式的攔截器註冊方式由ExpressionInterceptorProvider來完成。
如果我們能夠將針對目標方法的呼叫也轉換成一個InvokeDelegate委託,意味著針對整個攔截方法的呼叫鏈就由一系列InvokeDelegate委託構建而成,此項工作體現在如下這個IMethodInvokerBuilder介面的Build方法上,該方法旨在為指定的可攔截的方法建立一個代表方法呼叫鏈的InvokeDelegate物件,其三個引數分別代表目標型別、方法和用來完成目標方法呼叫的InvokeDelegate委託。IMethodInvokerBuilder介面還定義了一個CanIntercept方法用來確定指定的方法能否或者是否需要被攔截。
public interface IMethodInvokerBuilder { InvokeDelegate Build(Type targetType, MethodInfo method, InvokeDelegate targetMethodInvoker); bool CanIntercept(Type targetType, MethodInfo method); }
DefaultMethodInvokerBuilder實現了上面這個介面。如程式碼片段所示,註冊的所有IInterceptorProvider物件被注入建構函式之中。在實現了Build方法中,它利用這些IInterceptorProvider物件得到註冊到指定方法的所有攔截器,並按照順序構建成一個由InvokeDelegate委託表示的呼叫鏈。確保在攔截器執行之前對「下一個InvokeDelegate」進行設定也是在這裡完成的。另一個CanIntercept方法的實現就更簡單了,按照提供的邏輯:如果任何一個IInterceptorProvider物件將指定方法的攔截功能遮蔽掉,該方法就返回false,否則此方法的方法值體現的是是否由任何一個IInterceptorProvider物件決定攔截指定的方法。
internal class DefaultMethodInvokerBuilder : IMethodInvokerBuilder { private readonly IEnumerable<IInterceptorProvider> _interceptorProviders; private readonly Dictionary<Tuple<Type, MethodInfo>, Sortable<InvokeDelegate>[]> _cache = new(); public DefaultMethodInvokerBuilder(IEnumerable<IInterceptorProvider> interceptorProviders) { _interceptorProviders = interceptorProviders ?? throw new ArgumentNullException(nameof(interceptorProviders)); } public InvokeDelegate Build(Type targetType, MethodInfo method, InvokeDelegate targetMethodInvoker) { Guard.ArgumentNotNull(targetType); Guard.ArgumentNotNull(method); Guard.ArgumentNotNull(targetMethodInvoker); if (!CanIntercept(targetType, method)) { throw new InterceptionException($"The method '{method.Name}' of '{targetType}' cannot be interceptable."); } var key = new Tuple<Type, MethodInfo>(targetType, method); var interceptors = _cache.TryGetValue(key, out var value) ? value! : _cache[key] = _interceptorProviders.SelectMany(it => it.GetInterceptors(targetType, method)).OrderBy(it => it.Order).ToArray(); var length = interceptors.Length; interceptors = Enumerable.Range(0, length).Select(it => new Sortable<InvokeDelegate>(it, interceptors[it].Value)).ToArray(); Array.ForEach(interceptors, Wrap); return interceptors[0].Value; void Wrap(Sortable<InvokeDelegate> sortable) { var index = sortable.Order; var interceptor = sortable.Value; sortable.Value = context => { context.Next = index < length - 1 ? interceptors![index + 1].Value : targetMethodInvoker; return interceptor(context); }; } } public bool CanIntercept(Type targetType, MethodInfo method) { Guard.ArgumentNotNull(targetType); Guard.ArgumentNotNull(method); bool interceptable = false; foreach (var provider in _interceptorProviders) { if (provider.CanIntercept(targetType, method, out var suppressed)) { interceptable = true; } if (suppressed) { return false; } } return interceptable; } }
便於生成的程式碼使用這個IMethodInvokerBuilder,我們把應用當前使用的IMethodInvokerBuilder物件賦值給如下這個MethodInvokerBuilder型別的靜態屬性Instance。
public static class MethodInvokerBuilder { public static IMethodInvokerBuilder Instance { get; internal set; } = default!; }
實現AOP需要將應用到某個方法的攔截器「注入」到針對該方法的呼叫中,其注入方式大體分兩類,一種是靜態注入,另一種動態注入。靜態注入是在編譯的時候直接將針對攔截器的呼叫程式碼注入到目標方法中,這種注入方式對應用程式的執行不會帶來任何負擔,所以具有最好的效能,缺點就是無法應用一些動態的攔截策略。
動態注入則是在執行時注入攔截程式碼,它同樣具有多種實現方式。很早之前,我們利用基於.NET Remoting的TranparentPoxy/RealProxy的方法動態分發機制可以很容易地實現針對指定方法的攔截,但是這種實現方式的效能堪憂。目前使用得最多就是採用IL Emit,它在IL語言的層面生成可被攔截的動態代理類。這種方式由於是直接面向IL程式設計,所以對於大部分程式設計人員來說都是一個不小的挑戰,Dora.Interception之前的版本即是採用這種實現方式。動態編譯還具有另一種方式,那就是利用CLR Profiler直接修改JIT生成的機器程式碼,著名的APM框架DataDog就是利用這種方式實現針對各種元件的分散式跟蹤的。由於這種方式得用C++來寫,對.NET開發人員的要求就更高了。
最新版本的Dora.Interception放棄了基於IL Emit的實現方案,因為這樣的實現方式太過繁瑣,開發、維護、診斷和升級都是巨大的挑戰。一般來說,進行IL Emit程式設計都會先寫出生成程式碼的C#形式,然後再將其轉換成IL程式碼,如果我們能夠直接將C#程式碼編譯成IL程式碼,一切將會變得容易。實際上.NET的編譯平臺Roslyn本就可以將C#程式碼程式設計成對應的程式集,所以Dora.Interception直接利用了這個能力。
不論是上面提到的針對TranparentPoxy/RealProxy的實現,還是基於IL Emit,我們都需要利用一個「容器」來生成一個代理物件(如果直接使用目標型別的範例,其方法呼叫自然無法被攔截)。對於.NET (Core)來說,依賴注入容器無疑就是這個代理物件最好的建立者,所以Dora.Interception選擇建立在依賴注入框架之上。對於註冊到每個服務,如果目標型別的方法上註冊了攔截器,我們會為它生成相應的代理類,併為此代理類生成對應的服務註冊來替換原來的服務註冊。
如果服務註冊採用介面+實現型別(IFoobar/Foobar)的形式,程式碼生成器會採用如下的方式生成一個實現介面(IFoobar)同時封裝目標物件(Foobar)的代理類(FoobarProxy)。FoobarProxy會實現定義在介面中的所有成員,如果方法呼叫需要被攔截,針對攔截器的呼叫會實現在該方法中,否則它只需要直接呼叫封裝的物件即可。
如果服務註冊並未使用介面,那麼Flight.Interception只能採用方法重寫的方式實現對方法呼叫的攔截,這意味著被攔截的方法只能是虛方法。如下圖所示,如果給定服務註冊的服務型別和實現型別均為Foobar,程式碼生成器生成的代理類FoobarProxy是Foobar的子類,它會重寫需要攔截的方法來呼叫註冊的攔截器。
如果對於如下這個基於介面的服務註冊,如果某個需要攔截的方法並非介面方法。
public interface IFoobar { Task InvokeAsync(int x, int y); } public class Foobar : IFoobar { [Interceptor(typeof(Interceptor1))] public Task InvokeAsync(int x, int y) => InvokeCoreAsync(x, y); [Interceptor(typeof(Interceptor2))] protected virtual Task InvokeCoreAsync(int x, int y) => Task.CompletedTask; }
此時需要生成兩個代理類。其中FoobarProxy1派生於Foobar,利用重寫的InvokeCoreAsync方法解決針對非介面方法的攔截。FoobarProxy2實現IFoobar介面,並封裝FoobarProxy1物件,解決介面方法的攔截。
我們為代理型別的生成定義瞭如下這個ICodeGenerator介面作為程式碼生成器。該介面的TryGenerate會為指定的服務註冊(ServiceDescriptor)生成的代理型別。如果需要生成代理類(可被攔截的方法上被註冊了任意攔截器)該方法返回True,生成的C#程式碼寫入代表程式碼生成上下文的CodeGenerationContext 物件,輸出引數proxyTypeNames返回生成的一個或者兩個代理類的全名。至於名一個RegiserProxyType方法則使用針對生成的代理型別來替換現有的服務註冊。
public interface ICodeGenerator { bool TryGenerate(ServiceDescriptor serviceDescriptor, CodeGenerationContext codeGenerationContext, out string[]? proxyTypeNames); void RegisterProxyType(IServiceCollection services, ServiceDescriptor serviceDescriptor, Type[] proxyTypes); }
public sealed class CodeGenerationContext { public ISet<Assembly> References { get; } public int IndentLevel { get; private set; } public string SourceCode { get; } public CodeGenerationContext WriteLines(params string[] lines); public IDisposable CodeBlock(string? start = null, string? end = null) ; public IDisposable Indent() ; }
Dora.Interception提供了針對ICodeGenerator介面的兩個內部實現型別(InterfaceProxyGenerator和VirtualMethodProxyGenerator),正式它們幫助我們生成了針對介面和虛方法的代理型別。由於涉及的實現比較繁瑣,具體實現就不再這裡提供了,有興趣的朋友可以檢視原始碼。
我們知道依賴注入框架可以利用自定義的IServiceProviderFactory實現型別整合第三方依賴注入框架,Dora.Interception針對依賴注入框架的整合也是基於這樣的實現。具體的實現型別就是如下這個InterceptableServiceProviderFactory 。
internal class InterceptableServiceProviderFactory : IServiceProviderFactory<InterceptableContainerBuilder> { private readonly ServiceProviderOptions _options; private readonly Action<InterceptionBuilder>? _setup; public InterceptableServiceProviderFactory(ServiceProviderOptions options, Action<InterceptionBuilder>? setup) { _options = options ?? throw new ArgumentNullException(nameof(options)); _setup = setup; } public InterceptableContainerBuilder CreateBuilder(IServiceCollection services) => new(services, _options, _setup); public IServiceProvider CreateServiceProvider(InterceptableContainerBuilder containerBuilder) => containerBuilder.CreateServiceProvider(); }
InterceptableServiceProviderFactory 實現了IServiceProviderFactory<InterceptableContainerBuilder>介面,具體的實現體現作為泛型適配型別的InterceptableContainerBuilder型別上,如下就是該型別的定義。如程式碼片段所示,在建立最終的IServiceProvider物件之前,InterceptableContainerBuilder會利用提供的ICodeGenerator針對每個服務註冊進行可攔截代理型別的生成。如果某個ICodeGenerator真正生成相應的代理型別,它最終還會負責完成該代理型別的註冊。最終的IServiceProvider物件根據調整好的服務註冊構建而成。
public sealed class InterceptableContainerBuilder { private readonly IServiceCollection _services; private readonly ServiceProviderOptions _serviceProviderOptions; public InterceptableContainerBuilder(IServiceCollection services, ServiceProviderOptions serviceProviderOptions, Action<InterceptionBuilder>? setup) { _services = Guard.ArgumentNotNull(services); services.AddInterception(setup); services.AddSingleton<IServiceLifetimeProvider>(new ServiceLifetimeProvider(services)); _serviceProviderOptions = serviceProviderOptions ?? throw new ArgumentNullException(nameof(serviceProviderOptions)); } public IServiceProvider CreateServiceProvider() { var provider = _services.BuildServiceProvider(); try { var applicationServiceAccessor = provider.GetRequiredService<IApplicationServicesAccessor>(); ((ApplicationServicesAccessor) applicationServiceAccessor).ApplicationServices = provider; MethodInvokerBuilder.Instance = provider.GetRequiredService<IMethodInvokerBuilder>(); var logger = provider.GetRequiredService<ILogger<InterceptableContainerBuilder>>(); var log4GenerateCode = LoggerMessage.Define<string>(LogLevel.Information, 0, "Interceptable proxy classes are generated. " + Environment.NewLine + Environment.NewLine + "{0}"); var codeGenerators = provider.GetServices<ICodeGenerator>(); return CreateServiceProviderCore(codeGenerators, logger, log4GenerateCode); } finally { (provider as IDisposable)?.Dispose(); } } private IServiceProvider CreateServiceProviderCore(IEnumerable<ICodeGenerator> codeGenerators, ILogger logger, Action<ILogger, string, Exception> log4GenerateCode) { var generatedTypes = new List<GeneratedTypeEntry>(); var generationContext = new CodeGenerationContext(); generationContext.WriteLines("using System;"); generationContext.WriteLines("using System.Reflection;"); generationContext.WriteLines("using System.Threading.Tasks;"); generationContext.WriteLines("using Microsoft.Extensions.DependencyInjection;"); generationContext.WriteLines(""); generationContext.WriteLines("namespace Dora.Interception.CodeGeneration"); using (generationContext.CodeBlock()) { foreach (var service in _services) { foreach (var generator in codeGenerators) { if (generator.TryGenerate(service, generationContext, out var proxyTypeNames)) { generatedTypes.Add(new GeneratedTypeEntry(service, proxyTypeNames!, generator)); break; } } } } log4GenerateCode(logger, generationContext.SourceCode, null!); if (generatedTypes.Any()) { var compilation = CSharpCompilation.Create("Dora.Interception.CodeGeneration") .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, optimizationLevel: OptimizationLevel.Release)) .AddSyntaxTrees(SyntaxFactory.ParseSyntaxTree(generationContext.SourceCode)) .AddReferences(generationContext.References.Select(it => MetadataReference.CreateFromFile(it.Location))); Assembly outputAssembly; using var stream = new MemoryStream(); var compilationResult = compilation.Emit(stream); if (!compilationResult.Success) { var error = string.Join(Environment.NewLine, compilationResult.Diagnostics); throw new InterceptionException($"It fails to generate proxy class. \n {error}"); } var bytes = stream.ToArray(); outputAssembly = Assembly.Load(bytes); foreach (var entry in generatedTypes) { var proxyTypes = entry.ProxyTypeNames.Select(it => outputAssembly.GetType(it)).ToArray(); entry.CodeGenerator.RegisterProxyType(_services, entry.ServiceDescriptor, proxyTypes!); } } _services.Replace(ServiceDescriptor.Singleton<IServiceLifetimeProvider> (new ServiceLifetimeProvider(_services))); var serviceProvider = _services.BuildServiceProvider(_serviceProviderOptions); ((ApplicationServicesAccessor)serviceProvider.GetRequiredService<IApplicationServicesAccessor>()).ApplicationServices = serviceProvider; MethodInvokerBuilder.Instance = serviceProvider.GetRequiredService<IMethodInvokerBuilder>(); return serviceProvider; } private class GeneratedTypeEntry { public ServiceDescriptor ServiceDescriptor { get; } public string[] ProxyTypeNames { get; } public ICodeGenerator CodeGenerator { get; } public GeneratedTypeEntry(ServiceDescriptor serviceDescriptor, string[] proxyTypeNames, ICodeGenerator codeGenerator) { ServiceDescriptor = serviceDescriptor; ProxyTypeNames = proxyTypeNames; CodeGenerator = codeGenerator; } } }
前面廣泛使用的BuildInterceptableServiceProvider擴充套件方法定義如下,它直接使用了InterceptionBuilder物件來建立返回的IServiceProvider物件。
public static class ServiceCollectionExtensions { public static IServiceCollection AddInterception(this IServiceCollection services, Action<InterceptionBuilder>? setup = null); public static IServiceProvider BuildInterceptableServiceProvider(this IServiceCollection services, Action<InterceptionBuilder>? setup = null) => BuildInterceptableServiceProvider(services, new ServiceProviderOptions(), setup); public static IServiceProvider BuildInterceptableServiceProvider(this IServiceCollection services, ServiceProviderOptions serviceProviderOptions, Action<InterceptionBuilder>? setup = null) { Guard.ArgumentNotNull(services); Guard.ArgumentNotNull(serviceProviderOptions); var factory = new InterceptableServiceProviderFactory(serviceProviderOptions, setup); var builder = factory.CreateBuilder(services); return builder.CreateServiceProvider(); } } public sealed class InterceptionBuilder { public IServiceCollection Services { get; } }
用於整合ASP.NET Core的UseInterception擴充套件方法定義如下,它註冊了上述的InterceptionServiceProviderFactory 型別,同時使用RequestServiceScopeFactory替換了預設的InvocationServiceScopeFactory,實現了將針對請求的IServiceProvider物件作為呼叫上下文的依賴注入容器。
public static class HostBuilderExtensions { public static IHostBuilder UseInterception(this IHostBuilder hostBuilder, Action<InterceptionBuilder>? setup = null) => UseInterception(hostBuilder, new ServiceProviderOptions(), setup); public static IHostBuilder UseInterception(this IHostBuilder hostBuilder, ServiceProviderOptions serviceProviderOptions, Action<InterceptionBuilder>? setup = null) { if (hostBuilder == null) throw new ArgumentNullException(nameof(hostBuilder)); if (serviceProviderOptions == null) throw new ArgumentNullException(nameof(serviceProviderOptions)); hostBuilder.ConfigureServices((_, services) => services.AddHttpContextAccessor()); Action<InterceptionBuilder> configure = builder => { builder.Services.Replace(ServiceDescriptor.Singleton<IInvocationServiceScopeFactory, RequestServiceScopeFactory>()); setup?.Invoke(builder); }; return hostBuilder.UseServiceProviderFactory(new InterceptionServiceProviderFactory(serviceProviderOptions ?? new ServiceProviderOptions(), configure)); } }
我們現在看看Dora.Interception生成的代理型別是攔截目標方法並執行註冊的攔截器的。目標型別或者方法是否為泛型、及方法的返回型別(Void、一般型別、Task、Value、Task<TResult>和Value<TResult>)以及是否包含ref/in/out引數都會影響最終生成的代理型別,所以這裡我們之談論簡單的形式。我們先來看看針對介面的服務註冊最終會生成怎樣的代理型別。如下面的程式碼片段所示,Foobar型別實現了IFoobar介面,對於實現的兩個方法,InvokeAsync方法上註冊了一個攔截器,Invoke方法則沒有。
var foobar = new ServiceCollection() .AddSingleton<IFoobar, Foobar>() .BuildInterceptableServiceProvider() .GetRequiredService<IFoobar>(); public interface IFoobar { Task InvokeAsync(int x, string y); void Invoke(int x, int y); } public class Foobar : IFoobar { [FakeInterceptor] public virtual Task InvokeAsync(int x, string y) => throw new NotImplementedException(); public void Invoke(int x, int y) => throw new NotImplementedException(); }
對於如上基於IFoobar/Foobar的服務註冊,最終會生成如下的代理型別FoobarProxy1479038137 (「1479038137」為隨機生成的確保命名不會重讀的字尾)。該型別實現了IFoobar介面,並利用封裝的Foobar物件來實現該介面的兩個方法。對於需要被攔截的InvokeAsync方法,會生成對應的方法呼叫上下文型別InvokeAsyncContext351732220,具體的實現方法上述的MethodInvokerBuilder物件生成與方法對應的呼叫管道來完成針對註冊攔截器和目標方法的呼叫。至於另一個不需要被攔截的Invoke方法,直接呼叫目標物件Foobar對應的方法即可。生成的FoobarProxy1479038137 型別的服務註冊將會覆蓋原來的服務註冊。
using Microsoft.Extensions.DependencyInjection; using System.Reflection; namespace Dora.Interception.CodeGeneration { public class FoobarProxy1479038137 : App.IFoobar, IInterfaceProxy { private readonly App.IFoobar _target; private readonly IInvocationServiceScopeFactory _scopeFactory; private static readonly Lazy<MethodInfo> _methodOfInvokeAsync607503395 = new Lazy<MethodInfo>(() => ProxyHelper.GetMethodInfo<App.Foobar>(100663305)); private static readonly Lazy<InvokeDelegate> _invokerOfInvokeAsync951608024 = new Lazy<InvokeDelegate>(() => MethodInvokerBuilder.Instance.Build(typeof(App.Foobar), ProxyHelper.GetMethodInfo<App.Foobar>(100663305), InvokeAsync791452913)); public FoobarProxy1479038137(IServiceProvider provider, IInvocationServiceScopeFactory scopeFactory) { _target = ActivatorUtilities.CreateInstance<App.Foobar>(provider); _scopeFactory = scopeFactory; } public System.Threading.Tasks.Task InvokeAsync(System.Int32 x, System.String y) { using var scope = _scopeFactory.CreateInvocationScope(); var method = _methodOfInvokeAsync607503395.Value; var context = new InvokeAsyncContext351732220(_target, x, y, method, scope.ServiceProvider); var valueTask = _invokerOfInvokeAsync951608024.Value.Invoke(context); return valueTask.AsTask(); } public static ValueTask InvokeAsync791452913(InvocationContext invocationContext) { var context = (InvokeAsyncContext351732220)invocationContext; var target = (App.IFoobar)invocationContext.Target; var returnValue = target.InvokeAsync(context._x, context._y); context._returnValue = returnValue; return new ValueTask(returnValue); } public void Invoke(System.Int32 x, System.Int32 y) => _target.Invoke(x, y); private class InvokeAsyncContext351732220 : InvocationContext { internal System.Int32 _x; internal System.String _y; internal System.Threading.Tasks.Task _returnValue; public override MethodInfo MethodInfo { get; } public override IServiceProvider InvocationServices { get; } public InvokeAsyncContext351732220(object target, System.Int32 x, System.String y, MethodInfo method, IServiceProvider invocationServices) : base(target) { _x = x; _y = y; MethodInfo = method; InvocationServices = invocationServices; } public override TArgument GetArgument<TArgument>(string name) { return name switch { "x" => ProxyHelper.GetArgumentOrReturnValue<System.Int32, TArgument>(_x), "y" => ProxyHelper.GetArgumentOrReturnValue<System.String, TArgument>(_y), _ => throw new ArgumentException($"Invalid argument name {name}.", nameof(name)) }; } public override TArgument GetArgument<TArgument>(int index) { return index switch { 0 => ProxyHelper.GetArgumentOrReturnValue<System.Int32, TArgument>(_x), 1 => ProxyHelper.GetArgumentOrReturnValue<System.String, TArgument>(_y), _ => throw new ArgumentOutOfRangeException(nameof(index)) }; } public override InvocationContext SetArgument<TArgument>(string name, TArgument value) { return name switch { "x" => ProxyHelper.SetArgumentOrReturnValue<InvokeAsyncContext351732220, System.Int32, TArgument>(this, value, (ctx, val) => ctx._x = val), "y" => ProxyHelper.SetArgumentOrReturnValue<InvokeAsyncContext351732220, System.String, TArgument>(this, value, (ctx, val) => ctx._y = val), _ => throw new ArgumentException($"Invalid argument name {name}.", nameof(name)) }; } public override InvocationContext SetArgument<TArgument>(int index, TArgument value) { return index switch { 0 => ProxyHelper.SetArgumentOrReturnValue<InvokeAsyncContext351732220, System.Int32, TArgument>(this, value, (ctx, val) => ctx._x = val), 1 => ProxyHelper.SetArgumentOrReturnValue<InvokeAsyncContext351732220, System.String, TArgument>(this, value, (ctx, val) => ctx._y = val), _ => throw new ArgumentOutOfRangeException(nameof(index)) }; } public override TReturnValue GetReturnValue<TReturnValue>() => ProxyHelper.GetArgumentOrReturnValue<System.Threading.Tasks.Task, TReturnValue>(_returnValue); public override InvocationContext SetReturnValue<TReturnValue>(TReturnValue value) => ProxyHelper.SetArgumentOrReturnValue<InvokeAsyncContext351732220, System.Threading.Tasks.Task, TReturnValue>(this, value, (ctx, val) => ctx._returnValue = val); } } }
現在我們將針對介面的服務註冊替換成基於Foobar型別自身的註冊。
var foobar = new ServiceCollection()
.AddSingleton< Foobar>()
.AddLogging(logging=>logging.AddConsole())
.BuildInterceptableServiceProvider()
.GetRequiredService<Foobar>();
這次會生成如下所示的代理類FoobarProxy1224512711 。該型別將Foobar作為基礎類別,通過重寫InvokeAsync方法完成針對攔截器和目標方法的執行。至於不需要被攔截的Invoke方法則不需要考慮。最終針對Foobar/FoobarProxy1224512711的服務註冊將用來替換掉現有針對Foobar的服務註冊。
using System.Reflection; namespace Dora.Interception.CodeGeneration { public sealed class FoobarProxy1224512711 : App.Foobar, IVirtualMethodProxy { private readonly IInvocationServiceScopeFactory _scopeFactory; private static readonly Lazy<MethodInfo> _methodOfInvokeAsync1812877040 = new Lazy<MethodInfo>(() => ProxyHelper.GetMethodInfo<App.Foobar>(100663305)); private readonly Lazy<InvokeDelegate> _invokerOfInvokeAsync11225071; public FoobarProxy1224512711(IInvocationServiceScopeFactory scopeFactory) : base() { _scopeFactory = scopeFactory; _invokerOfInvokeAsync11225071 = new Lazy<InvokeDelegate>(() => MethodInvokerBuilder.Instance.Build(typeof(App.Foobar), ProxyHelper.GetMethodInfo<App.Foobar>(100663305), InvokeAsync243545362)); } public override System.Threading.Tasks.Task InvokeAsync(System.Int32 x, System.String y) { using var scope = _scopeFactory.CreateInvocationScope(); var method = _methodOfInvokeAsync1812877040.Value; var context = new InvokeAsyncContext1151816636(this, x, y, method, scope.ServiceProvider); var valueTask = _invokerOfInvokeAsync11225071.Value.Invoke(context); return valueTask.AsTask(); } public ValueTask InvokeAsync243545362(InvocationContext invocationContext) { var context = (InvokeAsyncContext1151816636)invocationContext; var returnValue = base.InvokeAsync(context._x, context._y); context._returnValue = returnValue; return new ValueTask(returnValue); } private class InvokeAsyncContext1151816636 : InvocationContext { internal System.Int32 _x; internal System.String _y; internal System.Threading.Tasks.Task _returnValue; public override MethodInfo MethodInfo { get; } public override IServiceProvider InvocationServices { get; } public InvokeAsyncContext1151816636(object target, System.Int32 x, System.String y, MethodInfo method, IServiceProvider invocationServices) : base(target) { _x = x; _y = y; MethodInfo = method; InvocationServices = invocationServices; } public override TArgument GetArgument<TArgument>(string name) { return name switch { "x" => ProxyHelper.GetArgumentOrReturnValue<System.Int32, TArgument>(_x), "y" => ProxyHelper.GetArgumentOrReturnValue<System.String, TArgument>(_y), _ => throw new ArgumentException($"Invalid argument name {name}.", nameof(name)) }; } public override TArgument GetArgument<TArgument>(int index) { return index switch { 0 => ProxyHelper.GetArgumentOrReturnValue<System.Int32, TArgument>(_x), 1 => ProxyHelper.GetArgumentOrReturnValue<System.String, TArgument>(_y), _ => throw new ArgumentOutOfRangeException(nameof(index)) }; } public override InvocationContext SetArgument<TArgument>(string name, TArgument value) { return name switch { "x" => ProxyHelper.SetArgumentOrReturnValue<InvokeAsyncContext1151816636, System.Int32, TArgument>(this, value, (ctx, val) => ctx._x = val), "y" => ProxyHelper.SetArgumentOrReturnValue<InvokeAsyncContext1151816636, System.String, TArgument>(this, value, (ctx, val) => ctx._y = val), _ => throw new ArgumentException($"Invalid argument name {name}.", nameof(name)) }; } public override InvocationContext SetArgument<TArgument>(int index, TArgument value) { return index switch { 0 => ProxyHelper.SetArgumentOrReturnValue<InvokeAsyncContext1151816636, System.Int32, TArgument>(this, value, (ctx, val) => ctx._x = val), 1 => ProxyHelper.SetArgumentOrReturnValue<InvokeAsyncContext1151816636, System.String, TArgument>(this, value, (ctx, val) => ctx._y = val), _ => throw new ArgumentOutOfRangeException(nameof(index)) }; } public override TReturnValue GetReturnValue<TReturnValue>() => ProxyHelper.GetArgumentOrReturnValue<System.Threading.Tasks.Task, TReturnValue>(_returnValue); public override InvocationContext SetReturnValue<TReturnValue>(TReturnValue value) => ProxyHelper.SetArgumentOrReturnValue<InvokeAsyncContext1151816636, System.Threading.Tasks.Task, TReturnValue>(this, value, (ctx, val) => ctx._returnValue = val); } } }
對於針對介面的服務註冊,如果攔截器被應用的方法並沒有定義定義在該介面中,此時會綜合應用上述兩種程式碼生成方案。為了演示我們對IFoobar和Foobar型別做了如下的修改。
public interface IFoobar { Task InvokeAsync(int x, string y); } [FakeInterceptor] public class Foobar : IFoobar { public virtual Task InvokeAsync(int x, string y) => throw new NotImplementedException(); public virtual void Invoke(int x, int y) => throw new NotImplementedException(); }
由於Foobar的兩個方法都註冊了攔截器,但是Invoke屬於Foobar獨有的方法,此時會生成如下兩個代理類FoobarProxy1796625286和FoobarProxy1493741432 ,前者繼承Foobar型別,後者實現IFoobar介面。基於IFoobar/FoobarProxy1493741432 的服務註冊會用來替換現有的服務註冊。
public sealed class FoobarProxy1796625286 : App.Foobar, IVirtualMethodProxy { private readonly IInvocationServiceScopeFactory _scopeFactory; private static readonly Lazy<MethodInfo> _methodOfInvoke717038218 = new Lazy<MethodInfo>(() => ProxyHelper.GetMethodInfo<App.Foobar>(100663305)); private readonly Lazy<InvokeDelegate> _invokerOfInvoke1480842411; public FoobarProxy1796625286(IInvocationServiceScopeFactory scopeFactory) : base() { _scopeFactory = scopeFactory; _invokerOfInvoke1480842411 = new Lazy<InvokeDelegate>(() => MethodInvokerBuilder.Instance.Build(typeof(App.Foobar), ProxyHelper.GetMethodInfo<App.Foobar>(100663305), Invoke880402619)); } public override void Invoke(System.Int32 x, System.Int32 y) { using var scope = _scopeFactory.CreateInvocationScope(); var method = _methodOfInvoke717038218.Value; var context = new InvokeContext307659875(this, x, y, method, scope.ServiceProvider); var valueTask = _invokerOfInvoke1480842411.Value.Invoke(context); valueTask.GetAwaiter().GetResult(); } public ValueTask Invoke880402619(InvocationContext invocationContext) { var context = (InvokeContext307659875)invocationContext; base.Invoke(context._x, context._y); return ValueTask.CompletedTask; } private class InvokeContext307659875 : InvocationContext { internal System.Int32 _x; internal System.Int32 _y; public override MethodInfo MethodInfo { get; } public override IServiceProvider InvocationServices { get; } public InvokeContext307659875(object target, System.Int32 x, System.Int32 y, MethodInfo method, IServiceProvider invocationServices) : base(target) { _x = x; _y = y; MethodInfo = method; InvocationServices = invocationServices; } public override TArgument GetArgument<TArgument>(string name) { return name switch { "x" => ProxyHelper.GetArgumentOrReturnValue<System.Int32, TArgument>(_x), "y" => ProxyHelper.GetArgumentOrReturnValue<System.Int32, TArgument>(_y), _ => throw new ArgumentException($"Invalid argument name {name}.", nameof(name)) }; } public override TArgument GetArgument<TArgument>(int index) { return index switch { 0 => ProxyHelper.GetArgumentOrReturnValue<System.Int32, TArgument>(_x), 1 => ProxyHelper.GetArgumentOrReturnValue<System.Int32, TArgument>(_y), _ => throw new ArgumentOutOfRangeException(nameof(index)) }; } public override InvocationContext SetArgument<TArgument>(string name, TArgument value) { return name switch { "x" => ProxyHelper.SetArgumentOrReturnValue<InvokeContext307659875, System.Int32, TArgument>(this, value, (ctx, val) => ctx._x = val), "y" => ProxyHelper.SetArgumentOrReturnValue<InvokeContext307659875, System.Int32, TArgument>(this, value, (ctx, val) => ctx._y = val), _ => throw new ArgumentException($"Invalid argument name {name}.", nameof(name)) }; } public override InvocationContext SetArgument<TArgument>(int index, TArgument value) { return index switch { 0 => ProxyHelper.SetArgumentOrReturnValue<InvokeContext307659875, System.Int32, TArgument>(this, value, (ctx, val) => ctx._x = val), 1 => ProxyHelper.SetArgumentOrReturnValue<InvokeContext307659875, System.Int32, TArgument>(this, value, (ctx, val) => ctx._y = val), _ => throw new ArgumentOutOfRangeException(nameof(index)) }; } public override TReturnValue GetReturnValue<TReturnValue>() => default; public override InvocationContext SetReturnValue<TReturnValue>(TReturnValue value) => this; } } public class FoobarProxy1493741432 : App.IFoobar, IInterfaceProxy { private readonly App.IFoobar _target; private readonly IInvocationServiceScopeFactory _scopeFactory; private static readonly Lazy<MethodInfo> _methodOfInvokeAsync209693810 = new Lazy<MethodInfo>(() => ProxyHelper.GetMethodInfo<App.Foobar>(100663304)); private static readonly Lazy<InvokeDelegate> _invokerOfInvokeAsync2048425446 = new Lazy<InvokeDelegate>(() => MethodInvokerBuilder.Instance.Build(typeof(App.Foobar), ProxyHelper.GetMethodInfo<App.Foobar>(100663304), InvokeAsync1286715673)); public FoobarProxy1493741432(IServiceProvider provider, IInvocationServiceScopeFactory scopeFactory) { _target = ActivatorUtilities.CreateInstance<Dora.Interception.CodeGeneration.FoobarProxy1796625286>(provider); _scopeFactory = scopeFactory; } public System.Threading.Tasks.Task InvokeAsync(System.Int32 x, System.String y) { using var scope = _scopeFactory.CreateInvocationScope(); var method = _methodOfInvokeAsync209693810.Value; var context = new InvokeAsyncContext1177601686(_target, x, y, method, scope.ServiceProvider); var valueTask = _invokerOfInvokeAsync2048425446.Value.Invoke(context); return valueTask.AsTask(); } public static ValueTask InvokeAsync1286715673(InvocationContext invocationContext) { var context = (InvokeAsyncContext1177601686)invocationContext; var target = (App.IFoobar)invocationContext.Target; var returnValue = target.InvokeAsync(context._x, context._y); context._returnValue = returnValue; return new ValueTask(returnValue); } private class InvokeAsyncContext1177601686 : InvocationContext { internal System.Int32 _x; internal System.String _y; internal System.Threading.Tasks.Task _returnValue; public override MethodInfo MethodInfo { get; } public override IServiceProvider InvocationServices { get; } public InvokeAsyncContext1177601686(object target, System.Int32 x, System.String y, MethodInfo method, IServiceProvider invocationServices) : base(target) { _x = x; _y = y; MethodInfo = method; InvocationServices = invocationServices; } public override TArgument GetArgument<TArgument>(string name) { return name switch { "x" => ProxyHelper.GetArgumentOrReturnValue<System.Int32, TArgument>(_x), "y" => ProxyHelper.GetArgumentOrReturnValue<System.String, TArgument>(_y), _ => throw new ArgumentException($"Invalid argument name {name}.", nameof(name)) }; } public override TArgument GetArgument<TArgument>(int index) { return index switch { 0 => ProxyHelper.GetArgumentOrReturnValue<System.Int32, TArgument>(_x), 1 => ProxyHelper.GetArgumentOrReturnValue<System.String, TArgument>(_y), _ => throw new ArgumentOutOfRangeException(nameof(index)) }; } public override InvocationContext SetArgument<TArgument>(string name, TArgument value) { return name switch { "x" => ProxyHelper.SetArgumentOrReturnValue<InvokeAsyncContext1177601686, System.Int32, TArgument>(this, value, (ctx, val) => ctx._x = val), "y" => ProxyHelper.SetArgumentOrReturnValue<InvokeAsyncContext1177601686, System.String, TArgument>(this, value, (ctx, val) => ctx._y = val), _ => throw new ArgumentException($"Invalid argument name {name}.", nameof(name)) }; } public override InvocationContext SetArgument<TArgument>(int index, TArgument value) { return index switch { 0 => ProxyHelper.SetArgumentOrReturnValue<InvokeAsyncContext1177601686, System.Int32, TArgument>(this, value, (ctx, val) => ctx._x = val), 1 => ProxyHelper.SetArgumentOrReturnValue<InvokeAsyncContext1177601686, System.String, TArgument>(this, value, (ctx, val) => ctx._y = val), _ => throw new ArgumentOutOfRangeException(nameof(index)) }; } public override TReturnValue GetReturnValue<TReturnValue>() => ProxyHelper.GetArgumentOrReturnValue<System.Threading.Tasks.Task, TReturnValue>(_returnValue); public override InvocationContext SetReturnValue<TReturnValue>(TReturnValue value) => ProxyHelper.SetArgumentOrReturnValue<InvokeAsyncContext1177601686, System.Threading.Tasks.Task, TReturnValue>(this, value, (ctx, val) => ctx._returnValue = val); } }
全新升級的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]: 框架設計和實現原理