全新升级的AOP框架Dora.Interception[6]: 框架设计和实现原理
本系列前面的五篇文章主要介绍 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基于约定的中间件定义方法类似,由于我们的拦截器委托比中间件委托要简洁,基于约定的拦截器自然比定义中间件要简单。中间件定义按照如下的约定即可:
- 将中间件定义成一个可以被依赖注入容器实例化的类型,一般定义成公共实例类型即可;
- 构造函数的选择由依赖注入容器决定,构造函数可以包含任意参数;
- 拦截操作定义在一个方法类型为ValueTask并被命名为InvokeAsync的异步方法中,该方法必须包含一个表示当前调用上下文的InvocationContext类型的参数,该参数在参数列表的位置可以任意指定。
- InvokeAsync方法可以注入任意能够从依赖注入容器提供的对象。
按照约定定义的中间件类型或者此类型的对象最终都需要转换成一个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); } }
- ASP.NET Core 6框架揭秘实例演示[32]:错误页面的集中呈现方式
- ASP.NET Core 6框架揭秘实例演示[31]:路由"高阶"用法
- 没有Kubernetes怎么玩Dapr?
- 全新升级的AOP框架Dora.Interception[6]: 框架设计和实现原理
- KestrelServer详解[2]: 网络链接的创建
- 一个简单的模拟实例说明Task及其调度问题
- ASP.NET Core 6框架揭秘实例演示[28]:自定义一个服务器
- ASP.NET Core 6 Minimal API的模拟实现
- ASP.NET Core 6框架揭秘实例演示[26]:跟踪应用接收的每一次请求
- ASP.NET Core 6框架揭秘实例演示[25]:配置与承载环境的应用
- ASP.NET Core 6框架揭秘实例演示[24]:中间件的多种定义方式
- ASP.NET Core 6框架揭秘实例演示[22]:如何承载你的后台服务[补充]
- ASP.NET Core 6框架揭秘实例演示[19]:数据加解密与哈希
- ASP.NET Core 6框架揭秘实例演示[12]:诊断跟踪的进阶用法
- ASP.NET Core 6框架揭秘实例演示[08]:配置的基本编程模式
- ASP.NET Core 6框架揭秘实例演示[05]:依赖注入基本编程模式
- ASP.NET Core 6框架揭秘实例演示[04]:自定义依赖注入框架
- ASP.NET Core 6框架揭秘实例演示[03]:Dapr初体验
- ASP.NET Core 6框架揭秘实例演示[02]:基于路由、MVC和gRPC的应用开发
- ASP.NET Core 6框架揭秘实例演示[01]: 编程初体验