写点什么

.NET8 依赖注入

作者:EquatorCoco
  • 2023-12-04
    福建
  • 本文字数:11021 字

    阅读完需:约 36 分钟

依赖注入(Dependency Injection,简称 DI)是一种设计模式,用于解耦组件(服务)之间的依赖关系。它通过将依赖关系的创建和管理交给外部容器来实现,而不是在组件(服务)内部直接创建依赖对象。

​ 咱就是通过 IServiceCollection 和 IServiceProvider 来实现的,他们直接被收入到了 runtime libraries,在整个.NET 平台下通用!


3.1 ServiceCollection


​ IServiceCollection 本质是一个 ServiceDescriptor 而 ServiceDescriptor 则是用于描述服务类型,实现和生命周期


public interface IServiceCollection :     IList<ServiceDescriptor>,    ICollection<ServiceDescriptor>,    IEnumerable<ServiceDescriptor>,    IEnumerable;
复制代码


官方提供一些列拓展帮助我们向服务容器中添加服务描述,具体在 ServiceCollectionServiceExtensions


builder.Services.AddTransient<StudentService>();builder.Services.AddKeyedTransient<IStudentRepository, StudentRepository>("a");builder.Services.AddKeyedTransient<IStudentRepository, StudentRepository2>("b");builder.Services.AddTransient<TransientService>();builder.Services.AddScoped<ScopeService>();builder.Services.AddSingleton<SingletonService>();
复制代码


3.2 ServiceProvider


​ IServiceProvider 定义了一个方法 GetService,帮助我们通过给定的服务类型,获取其服务实例


public interface IServiceProvider{  object? GetService(Type serviceType);}
复制代码

下面是 GetService 的默认实现(如果不给定 engine scope,则默认是 root)


public object? GetService(Type serviceType) => GetService(ServiceIdentifier.FromServiceType(serviceType), Root);
复制代码


也就是


internal object? GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope){    if (_disposed)    {        ThrowHelper.ThrowObjectDisposedException();    }    // 获取服务标识符对应的服务访问器    ServiceAccessor serviceAccessor = _serviceAccessors.GetOrAdd(serviceIdentifier, _createServiceAccessor);    // 执行解析时的hock    OnResolve(serviceAccessor.CallSite, serviceProviderEngineScope);    DependencyInjectionEventSource.Log.ServiceResolved(this, serviceIdentifier.ServiceType);    // 通过服务访问器提供的解析服务,得到服务实例    object? result = serviceAccessor.RealizedService?.Invoke(serviceProviderEngineScope);    System.Diagnostics.Debug.Assert(result is null || CallSiteFactory.IsService(serviceIdentifier));    return result;}
复制代码

其中,服务标识符 ServiceIdentifier 其实就是包了一下服务类型,和服务 Key(为了.NET8 的键化服务)


internal readonly struct ServiceIdentifier : IEquatable<ServiceIdentifier>{    public object? ServiceKey { get; }    public Type ServiceType { get; }}
复制代码

显而易见的,我们的服务解析是由 serviceAccessor.RealizedService 提供,而创建服务访问器 serviceAccessor 只有一个实现 CreateServiceAccessor


private ServiceAccessor CreateServiceAccessor(ServiceIdentifier serviceIdentifier){    // 通过 CallSiteFactory 获取服务的调用点(CallSite),这是服务解析的一个表示形式。    ServiceCallSite? callSite = CallSiteFactory.GetCallSite(serviceIdentifier, new CallSiteChain());        // 如果调用站点不为空,则继续构建服务访问器。    if (callSite != null)    {        DependencyInjectionEventSource.Log.CallSiteBuilt(this, serviceIdentifier.ServiceType, callSite);                // 触发创建调用站点的相关事件。        OnCreate(callSite);
// 如果调用站点的缓存位置是根(Root),即表示这是一个单例服务。 if (callSite.Cache.Location == CallSiteResultCacheLocation.Root) { // 直接拿缓存内容 object? value = CallSiteRuntimeResolver.Instance.Resolve(callSite, Root); return new ServiceAccessor { CallSite = callSite, RealizedService = scope => value }; }
// 通过引擎解析 Func<ServiceProviderEngineScope, object?> realizedService = _engine.RealizeService(callSite); return new ServiceAccessor { CallSite = callSite, RealizedService = realizedService }; } // 如果调用点为空,则它的实现服务函数总是返回 null。 return new ServiceAccessor { CallSite = callSite, RealizedService = _ => null };}
复制代码


3.2.1 ServiceProviderEngine


​ ServiceProviderEngine 是服务商解析服务的执行引擎,它在服务商被初始化时建立。有两种引擎,分别是动态引擎运行时引擎,在 NETFRAMEWORK || NETSTANDARD2_0 默认使用动态引擎。


        private ServiceProviderEngine GetEngine()        {            ServiceProviderEngine engine;
#if NETFRAMEWORK || NETSTANDARD2_0 engine = CreateDynamicEngine();#else if (RuntimeFeature.IsDynamicCodeCompiled && !DisableDynamicEngine) { engine = CreateDynamicEngine(); } else { // Don't try to compile Expressions/IL if they are going to get interpreted engine = RuntimeServiceProviderEngine.Instance; }#endif return engine;
[UnconditionalSuppressMessage("AotAnalysis", "IL3050:RequiresDynamicCode", Justification = "CreateDynamicEngine won't be called when using NativeAOT.")] // see also https://github.com/dotnet/linker/issues/2715 ServiceProviderEngine CreateDynamicEngine() => new DynamicServiceProviderEngine(this); }
复制代码


由于.NET Aot 技术与 dynamic 技术冲突,因此 Aot 下只能使用运行时引擎,但动态引擎在大多情况下仍然是默认的。


动态引擎使用了 emit 技术,这是一个动态编译技术,而 aot 的所有代码都需要在部署前编译好,因此运行时无法生成新的代码。那运行时引擎主要使用反射,目标是在不牺牲太多性能的情况下,提供一个在 aot 环境中可行的解决方案。


​ 我们展开动态引擎来看看它是如何解析服务的。


public override Func<ServiceProviderEngineScope, object?> RealizeService(ServiceCallSite callSite){    // 定义一个局部变量来跟踪委托被调用的次数    int callCount = 0;    return scope =>    {        // 当委托被调用时,先使用CallSiteRuntimeResolver.Instance.Resolve方法来解析服务。这是一个同步操作,确保在编译优化之前,服务可以被正常解析。        var result = CallSiteRuntimeResolver.Instance.Resolve(callSite, scope);        // 委托第二次被调用,通过UnsafeQueueUserWorkItem在后台线程上启动编译优化        if (Interlocked.Increment(ref callCount) == 2)        {            // 将一个工作项排队到线程池,但不捕获当前的执行上下文。            _ = ThreadPool.UnsafeQueueUserWorkItem(_ =>            {                try                {                    // 用编译优化后的委托替换当前的服务访问器,主要用到emit/expression技术                    _serviceProvider.ReplaceServiceAccessor(callSite, base.RealizeService(callSite));                }                catch (Exception ex)                {                    DependencyInjectionEventSource.Log.ServiceRealizationFailed(ex, _serviceProvider.GetHashCode());                    Debug.Fail($"We should never get exceptions from the background compilation.{Environment.NewLine}{ex}");                }            },            null);        }        return result;    };}
复制代码


这个实现的关键思想是,第一次解析服务时使用一个简单的运行时解析器,这样可以快速返回服务实例。然后,当服务再被解析,它会在后台线程上启动一个编译过程,生成一个更高效的服务解析委托。一旦编译完成,新的委托会替换掉原来的委托,以后的服务解析将使用这个新的、更高效的委托。这种方法可以在不影响应用程序启动时间的情况下,逐渐优化服务解析的性能。


3.2.2 ServiceProviderEngineScope


​ ServiceProviderEngineScope 闪亮登场,他是我们服务商的代言人,从定义不难看出他对外提供了服务商所具备的一切能力


internal sealed class ServiceProviderEngineScope : IServiceScope, IServiceProvider, IKeyedServiceProvider, 			IAsyncDisposable, IServiceScopeFactory{    // this scope中所有实现IDisposable or IAsyncDisposable的服务    private List<object>? _disposables;    // 解析过的服务缓存(其实就是scope生命周期的服务缓存,singleton生命周期的服务缓存都直接挂在调用点上了)    internal Dictionary<ServiceCacheKey, object?> ResolvedServices { get; }    // 实锤服务商代言人    public IServiceProvider ServiceProvider => this;    // 没错啦,通过root scope我们又能继续创建无数个scope,他们彼此独立    public IServiceScope CreateScope() => RootProvider.CreateScope();}
复制代码


我们来观察他获取服务的逻辑,可以发现他就是很朴实无华的用着我们根服务商 ServiceProvider,去解析服务,那 engine scope 呢,就是 this。现在我们已经隐约可以知道 engine scope,就是为了满足 scope 生命周期而生。而 ResolvedServices 中存的呢,就是该 scope 中的所有 scope 生命周期服务实例啦,在这个 scope 中他们是唯一的。


public object? GetService(Type serviceType){    if (_disposed)    {        ThrowHelper.ThrowObjectDisposedException();    }    return RootProvider.GetService(ServiceIdentifier.FromServiceType(serviceType), this);}
复制代码


再来看另一个核心的方法:CaptureDisposable,实现 disposable 的服务会被添加到 _disposables。


internal object? CaptureDisposable(object? service){    // 如果服务没有实现 IDisposable or IAsyncDisposable,那么不需要捕获,直接原路返回    if (ReferenceEquals(this, service) || !(service is IDisposable || service is IAsyncDisposable))    {        return service;    }    bool disposed = false;    lock (Sync)    {        if (_disposed) // 如果scope已经销毁则进入销毁流程        {            disposed = true;        }        else        {            _disposables ??= new List<object>();            _disposables.Add(service);        }    }    // Don't run customer code under the lock    if (disposed) // 这表示我们在试图捕获可销毁服务时,scope就已经被销毁    {        if (service is IDisposable disposable)        {            disposable.Dispose();        }        else        {            // sync over async, for the rare case that an object only implements IAsyncDisposable and may end up starving the thread pool.            object? localService = service; // copy to avoid closure on other paths            Task.Run(() => ((IAsyncDisposable)localService).DisposeAsync().AsTask()).GetAwaiter().GetResult();        }        // 这种case会抛出一个ObjectDisposedException        ThrowHelper.ThrowObjectDisposedException();    }    return service;}
复制代码


在 engine scope 销毁时,其作用域中所有 scope 生命周期且实现了 disposable 的服务(其实就是_disposable)呢,也会被一同的销毁。


public ValueTask DisposeAsync(){    List<object>? toDispose = BeginDispose(); // 获取_disposable    if (toDispose != null)    {        // 从后往前,依次销毁服务    }}
复制代码


​ 那么有同学可能就要问了:单例实例既然不存在 root scope 中,而是单独丢到了调用点上,那他是咋销毁的?压根没看到啊,那不得泄露了?


​ 其实呀,同学们并不用担心这个问题。首先,单例服务的实例确实是缓存在调用点上,但 disable 服务仍然会被 scope 捕获呀(在下文会详细介绍)。在 BeginDispose 中的,我们会去判断,如果是 singleton case,且 root scope 没有被销毁过,我们会主动去销毁喔~


if (IsRootScope && !RootProvider.IsDisposed()) RootProvider.Dispose();


3.3 ServiceCallSite


​ ServiceCallSite 的主要职责是封装服务解析的逻辑,它可以代表一个构造函数调用、属性注入、工厂方法调用等。DI 系统使用这个抽象来表示服务的各种解析策略,并且可以通过它来生成服务实例。


internal abstract class ServiceCallSite{    protected ServiceCallSite(ResultCache cache)	 {	     Cache = cache;	 }    public abstract Type ServiceType { get; }	 public abstract Type? ImplementationType { get; }	 public abstract CallSiteKind Kind { get; }	 public ResultCache Cache { get; }	 public object? Value { get; set; }	 public object? Key { get; set; }	 public bool CaptureDisposable => ImplementationType == null ||    	typeof(IDisposable).IsAssignableFrom(ImplementationType) ||    	typeof(IAsyncDisposable).IsAssignableFrom(ImplementationType);}
复制代码


3.3.1 ResultCache


​ 其中 ResultCache 定义了我们如何缓存解析后的结果


public CallSiteResultCacheLocation Location { get; set; } // 缓存位置public ServiceCacheKey Key { get; set; } // 服务key(键化服务用的)
复制代码

​ 

CallSiteResultCacheLocation 是一个枚举,定义了几个值


  1. Root:表示服务实例应该在根级别的 IServiceProvider 中缓存。这通常意味着服务实例是单例的(Singleton),在整个应用程序的生命周期内只会创建一次,并且在所有请求中共享。


  1. Scope:表示服务实例应该在当前作用域(Scope)中缓存。对于作用域服务(Scoped),实例会在每个作用域中创建一次,并在该作用域内的所有请求中共享。


  1. Dispose:尽管这个名称可能会让人误解,但在 ResultCache 的上下文中,Dispose 表示着服务是瞬态的(每次请求都创建新实例)。


  1. None:表示没有缓存服务实例。


​ ServiceCacheKey 结构体就是包了一下服务标识符和一个 slot,用于适配多实现的


internal readonly struct ServiceCacheKey : IEquatable<ServiceCacheKey>{    public ServiceIdentifier ServiceIdentifier { get; }    public int Slot { get; } // 那最后一个实现的slot是0}
复制代码


3.3.2 CallSiteFactory.GetCallSite


​ 那我们来看看调用点是怎么创建的吧,其实上面已经出现过一次了:


private ServiceCallSite? CreateCallSite(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain){    if (!_stackGuard.TryEnterOnCurrentStack()) // 防止栈溢出    {        return _stackGuard.RunOnEmptyStack(CreateCallSite, serviceIdentifier, callSiteChain);    }    // 获取服务标识符对应的锁,以确保在创建调用点时的线程安全。    // 是为了保证并行解析下的调用点也只会被创建一次,例如:    // C -> D -> A    // E -> D -> A    var callsiteLock = _callSiteLocks.GetOrAdd(serviceIdentifier, static _ => new object());    lock (callsiteLock)    {        // 检查当前服务标识符是否会导致循环依赖        callSiteChain.CheckCircularDependency(serviceIdentifier);        // 首先尝试创建精确匹配的服务调用站点,如果失败,则尝试创建开放泛型服务调用站点,如果还是失败,则尝试创建枚举服务调用站点。如果所有尝试都失败了,callSite将为null。        ServiceCallSite? callSite = TryCreateExact(serviceIdentifier, callSiteChain) ??                                   TryCreateOpenGeneric(serviceIdentifier, callSiteChain) ??                                   TryCreateEnumerable(serviceIdentifier, callSiteChain);        return callSite;    }}
复制代码


那服务点的创建过程我就简单概述一下啦


  1. 查找调用点缓存,存在就直接返回啦


  1. 服务标识符会被转成服务描述符 ServiceDescriptor (key 化服务不指定 key 默认取 last)


  1. 计算ServiceCallSite,依次是:


  • TryCreateExact


计算 ResultCache


如果已经有实现实例了,则返回 ConstantCallSite:表示直接返回已经创建的实例的调用点。


如果有实现工厂,则返回 FactoryCallSite:表示通过工厂方法创建服务实例的调用点。


如果有实现类型,则返回 ConstructorCallSite:表示通过构造函数创建服务实例的调用点。


  • TryCreateOpenGeneric


根据泛型定义获取服务描述符 ServiceDescriptor


计算 ResultCache


使用服务标识符中的具体泛型参数来构造实现的闭合类型


AOT 兼容性测试(因为不能保证值类型泛型的代码已经生成)


如果成功闭合,则返回 ConstructorCallSite:表示通过构造函数创建服务实例的调用点。


  • TryCreateEnumerable


确定类型是 IEnumerable<T>


AOT 兼容性测试(因为不能保证值类型数组的代码已经生成)


如果 T 不是泛型类型,并且可以找到对应的服务描述符集合,则循环 TryCreateExact


否则,方向循环 TryCreateExact,然后方向循环 TryCreateOpenGeneric


3.4 CallSiteVisitor


​ 好了,有了上面的了解我们可以开始探索服务解析的内幕了。服务解析说白了就是引擎围着 CallSiteVisitor 转圈圈,所谓的解析服务,其实就是访问调用点了。


protected virtual TResult VisitCallSite(ServiceCallSite callSite, TArgument argument){    if (!_stackGuard.TryEnterOnCurrentStack()) // 一些校验,分栈啥的    {        return _stackGuard.RunOnEmptyStack(VisitCallSite, callSite, argument);    }    switch (callSite.Cache.Location)    {        case CallSiteResultCacheLocation.Root: // 单例            return VisitRootCache(callSite, argument);        case CallSiteResultCacheLocation.Scope: // 作用域            return VisitScopeCache(callSite, argument);        case CallSiteResultCacheLocation.Dispose: // 瞬态            return VisitDisposeCache(callSite, argument);        case CallSiteResultCacheLocation.None: // 不缓存(ConstantCallSite)            return VisitNoCache(callSite, argument);        default:            throw new ArgumentOutOfRangeException();    }}
复制代码


为了方便展示,我们这里的解析器都拿运行时来说,因为内部是反射,而 emit、expression 实在是难以观看。

3.4.1 VisitRootCache


那我们来看看单例的情况下,是如何访问的:


protected override object? VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context){    if (callSite.Value is object value)    {        // Value already calculated, return it directly        return value;    }    var lockType = RuntimeResolverLock.Root;    // 单例都是直接放根作用域的    ServiceProviderEngineScope serviceProviderEngine = context.Scope.RootProvider.Root;    lock (callSite)    {        // 这里搞了个双检锁来确保在多线程环境中,同一时间只有一个线程可以执行接下来的代码块。        // Lock the callsite and check if another thread already cached the value        if (callSite.Value is object callSiteValue)        {            return callSiteValue;        }        object? resolved = VisitCallSiteMain(callSite, new RuntimeResolverContext        {            Scope = serviceProviderEngine,            AcquiredLocks = context.AcquiredLocks | lockType        });        // 捕获可销毁的服务        serviceProviderEngine.CaptureDisposable(resolved);        // 缓存解析结果到调用点上        callSite.Value = resolved;        return resolved;    }}
复制代码


好,可以看到真正解析调用点的主角出来了 VisitCallSiteMain,那这里的 CallSiteKind 上面计算 ServiceCallSite 时呢已经讲的很清楚啦,咱对号入座就行了


protected virtual TResult VisitCallSiteMain(ServiceCallSite callSite, TArgument argument){    switch (callSite.Kind)    {        case CallSiteKind.Factory:            return VisitFactory((FactoryCallSite)callSite, argument);        case  CallSiteKind.IEnumerable:            return VisitIEnumerable((IEnumerableCallSite)callSite, argument);        case CallSiteKind.Constructor:            return VisitConstructor((ConstructorCallSite)callSite, argument);        case CallSiteKind.Constant:            return VisitConstant((ConstantCallSite)callSite, argument);        case CallSiteKind.ServiceProvider:            return VisitServiceProvider((ServiceProviderCallSite)callSite, argument);        default:            throw new NotSupportedException(SR.Format(SR.CallSiteTypeNotSupported, callSite.GetType()));    }}
复制代码


我们就看看最经典的通过构造函数创建服务实例的调用点 ConstructorCallSite,很显然就是 new 嘛,只不过可能构造中依赖其它服务,那就递归创建呗。easy,其它几种太简单了大家自己去看看吧。


protected override object VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context){    object?[] parameterValues;    if (constructorCallSite.ParameterCallSites.Length == 0)    {        parameterValues = Array.Empty<object>();    }    else    {        parameterValues = new object?[constructorCallSite.ParameterCallSites.Length];        for (int index = 0; index < parameterValues.Length; index++)        {            // 递归构建依赖的服务            parameterValues[index] = VisitCallSite(constructorCallSite.ParameterCallSites[index], context);        }    }    // new (xxx)    return constructorCallSite.ConstructorInfo.Invoke(BindingFlags.DoNotWrapExceptions, binder: null, parameters: parameterValues, culture: null);}
复制代码


3.4.2 VisitScopeCache


在访问单例缓存的时候呢,仅仅通过了一个 double check lock 就搞定了,因为人家全局的嘛,咱再来看看访问作用域缓存,会不会有什么不一样


protected override object? VisitScopeCache(ServiceCallSite callSite, RuntimeResolverContext context){    // Check if we are in the situation where scoped service was promoted to singleton    // and we need to lock the root    return context.Scope.IsRootScope ?        VisitRootCache(callSite, context) :        VisitCache(callSite, context, context.Scope, RuntimeResolverLock.Scope);}
复制代码


哈哈,它果然很不一般啊,上来就来检查我们是否是 root scope。如果是这种 case 呢,则走 VisitRootCache。但是奇怪啊,为什么访问 scope cache,所在 engine scope 能是 root scope?

​ 还记得 ServiceProvider 获取的服务实例的核心方法吗?engine scope 他是传进来的,如果我们给一个 root scope,自然就出现的这种 case,只是这种 case 特别罕见。


internal object? GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
复制代码

​ 

VisitCache 的同步模型写的实在是酷,我们看 RuntimeResolverLock 的枚举就两个:Scope = 1 和 Root = 2


  • AcquiredLocks=Scope 时


  • 那 AcquiredLocks&false==0 显然成立,申请锁,也就是尝试独占改作用域的 ResolvedServices


  • 申请成功进入同步块,重新计算 AcquiredLocks|true=1


  • 如此,在该 engine scope 中这条链路上的调用点都被占有,直到结束


  • AcquiredLocks=Root 时


  • 那显然 engine scope 也应该是 root scope,那么走 VisitRootCache case


  • 在 VisitRootCache 通过 DCL 锁住 root scope 上链路涉及的服务点,直至结束


​至此我们应该不难看出这个设计的精妙之处,即在非 root scope(scope 生命周期)中,scope 之间是互相隔离的,没有必要像 root scope(singleton 生命周期)那样,在所有 scope 中独占服务点。


private object? VisitCache(ServiceCallSite callSite, RuntimeResolverContext context, ServiceProviderEngineScope serviceProviderEngine{    bool lockTaken = false;    object sync = serviceProviderEngine.Sync;    Dictionary<ServiceCacheKey, object?> resolvedServices = serviceProviderEngine.ResolvedServices;
if ((context.AcquiredLocks & lockType) == 0) { Monitor.Enter(sync, ref lockTaken); } try { // Note: This method has already taken lock by the caller for resolution and access synchronization. // For scoped: takes a dictionary as both a resolution lock and a dictionary access lock. if (resolvedServices.TryGetValue(callSite.Cache.Key, out object? resolved)) { return resolved; } // scope服务的解析结果是放在engine scope的ResolvedServices上的,而非调用点 resolved = VisitCallSiteMain(callSite, new RuntimeResolverContext { Scope = serviceProviderEngine, AcquiredLocks = context.AcquiredLocks | lockType }); serviceProviderEngine.CaptureDisposable(resolved); resolvedServices.Add(callSite.Cache.Key, resolved); return resolved; } finally { if (lockTaken) { Monitor.Exit(sync); } }}
复制代码


3.4.3 VisitDisposeCache


​ 我们看最后一个,也就是 Transient case


protected override object? VisitDisposeCache(ServiceCallSite transientCallSite, RuntimeResolverContext context){    return context.Scope.CaptureDisposable(VisitCallSiteMain(transientCallSite, context));}
复制代码

异常的简单,我们沿用了 scope 的设计,但是我们没有进行任何缓存行为。即,每次都去访问调用点。


文章转载自:xiaolipro

原文链接:https://www.cnblogs.com/xiaolipro/p/17873575.html

用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
.NET8 依赖注入_.net_EquatorCoco_InfoQ写作社区