當前位置: 妍妍網 > 碼農

.NET 8 依賴註入

2024-02-16碼農


依賴註入(Dependency Injection,簡稱DI)是一種設計模式,用於解耦元件(服務)之間的依賴關系。它透過將依賴關系的建立和管理交給外部容器來實作,而不是在元件(服務)內部直接建立依賴物件。

咱就是透過 IServiceCollection IServiceProvider 來實作的,他們直接被收入到了runtime libraries,在整個.NET平台下通用!

一、ServiceCollection

IServiceCollection 本質是一個 ServiceDescriptor ServiceDescriptor 則是用於描述服務型別,實作和生命周期

publicinterfaceIServiceCollection : 
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>();

二、ServiceProvider

IServiceProvider 定義了一個方法 GetService ,幫助我們透過給定的服務型別,獲取其服務例項

publicinterfaceIServiceProvider
{
object? GetService(Type serviceType);
}

下面是 GetService 的預設實作(如果不給定engine scope,則預設是root)

publicobject? GetService(Type serviceType) => GetService(ServiceIdentifier.FromServiceType(serviceType), Root);

也就是

internalobject? 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 isnull || CallSiteFactory.IsService(serviceIdentifier));
return result;
}

其中,服務識別元 ServiceIdentifier 其實就是包了一下服務型別,和服務Key(為了.NET8的鍵化服務)

internalreadonlystruct ServiceIdentifier : IEquatable<ServiceIdentifier>
{
publicobject? 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)
{
// 直接拿緩存內容
objectvalue = CallSiteRuntimeResolver.Instance.Resolve(callSite, Root);
returnnew ServiceAccessor { CallSite = callSite, RealizedService = scope => value };
}
// 透過引擎解析
Func<ServiceProviderEngineScope, object?> realizedService = _engine.RealizeService(callSite);
returnnew ServiceAccessor { CallSite = callSite, RealizedService = realizedService };
}
// 如果呼叫點為空,則它的實作服務函式總是返回 null。
returnnew ServiceAccessor { CallSite = callSite, RealizedService = _ => null };
}




1、ServiceProviderEngine

ServiceProviderEngine 是服務商解析服務的執行引擎,它在服務商被初始化時建立。有兩種引擎,分別是 動態引擎 執行時引擎 ,在 NETFRAMEWORK || NETSTANDARD2_0 預設使用動態引擎。

private ServiceProviderEngine GetEngine()
{
ServiceProviderEngine engine;
RAMEWORK || NETSTANDARD2_0
engine = CreateDynamicEngine();
if (RuntimeFeature.IsDynamicCodeCompiled && !DisableDynamicEngine)
{
engine = CreateDynamicEngine();
}
else
{
// Don't try to compile Expressions/IL if they are going to get interpreted
engine = RuntimeServiceProviderEngine.Instance;
}
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環境中可行的解決方案。

我們展開動態引擎來看看它是如何解析服務的。

publicoverride 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;
};
}

這個實作的關鍵思想是,第一次解析服務時使用一個簡單的執行時解析器,這樣可以快速返回服務例項。然後,當服務再被解析,它會在後台執行緒上啟動一個編譯過程,生成一個更高效的服務解析委托。一旦編譯完成,新的委托會替換掉原來的委托,以後的服務解析將使用這個新的、更高效的委托。這種方法可以在不影響應用程式啟動時間的情況下,逐漸最佳化服務解析的效能。

2、ServiceProviderEngineScope

ServiceProviderEngineScope 閃亮登場,他是我們服務商的代言人,從定義不難看出他對外提供了服務商所具備的一切能力

internalsealed classServiceProviderEngineScope : IServiceScopeIServiceProviderIKeyedServiceProvider, IAsyncDisposableIServiceScopeFactory
{
// 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中他們是唯一的。

publicobject? GetService(Type serviceType)
{
if (_disposed)
{
ThrowHelper.ThrowObjectDisposedException();
}
return RootProvider.GetService(ServiceIdentifier.FromServiceType(serviceType), this);
}

再來看另一個核心的方法: CaptureDisposable ,實作disposable的服務會被添加到 _disposables。

internalobject? 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();

三、ServiceCallSite

ServiceCallSite 的主要職責是封裝服務解析的邏輯,它可以代表一個建構函式呼叫、內容註入、工廠方法呼叫等。DI系統使用這個抽象來表示服務的各種解析策略,並且可以透過它來生成服務例項。

internalabstract classServiceCallSite
{
protectedServiceCallSite(ResultCache cache)
{
Cache = cache;
}
publicabstract Type ServiceType { get; }
publicabstract Type? ImplementationType { get; }
publicabstract CallSiteKind Kind { get; }
public ResultCache Cache { get; }
publicobject? Value { getset; }
publicobject? Key { getset; }
publicbool CaptureDisposable => ImplementationType == null ||
typeof(IDisposable).IsAssignableFrom(ImplementationType) ||
typeof(IAsyncDisposable).IsAssignableFrom(ImplementationType);
}

1、ResultCache

其中 ResultCache 定義了我們如何緩存解析後的結果

public CallSiteResultCacheLocation Location { getset; } // 緩存位置
public ServiceCacheKey Key { getset; } // 服務key(鍵化服務用的)

CallSiteResultCacheLocation 是一個列舉,定義了幾個值

1、 Root :表示服務例項應該在根級別的 IServiceProvider 中緩存。這通常意味著服務例項是單例的(Singleton),在整個應用程式的生命周期內只會建立一次,並且在所有請求中共享。

2、 Scope :表示服務例項應該在當前作用域(Scope)中緩存。對於作用域服務(Scoped),例項會在每個作用域中建立一次,並在該作用域內的所有請求中共享。

3、 Dispose :盡管這個名稱可能會讓人誤解,但在 ResultCache 的上下文中, Dispose 表示著服務是瞬態的(每次請求都建立新例項)。

4、 None :表示沒有緩存服務例項。

ServiceCacheKey 結構體就是包了一下服務識別元和一個slot,用於適配多實作的

internalreadonlystruct ServiceCacheKey : IEquatable<ServiceCacheKey>
{
public ServiceIdentifier ServiceIdentifier { get; }
publicint Slot { get; } // 那最後一個實作的slot是0
}

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 _ => newobject());
lock (callsiteLock)
{
// 檢查當前服務識別元是否會導致迴圈依賴
callSiteChain.CheckCircularDependency(serviceIdentifier);
// 首先嘗試建立精確匹配的服務呼叫站點,如果失敗,則嘗試建立開放泛型服務呼叫站點,如果還是失敗,則嘗試建立列舉服務呼叫站點。如果所有嘗試都失敗了,callSite將為null。
ServiceCallSite? callSite = TryCreateExact(serviceIdentifier, callSiteChain) ??
TryCreateOpenGeneric(serviceIdentifier, callSiteChain) ??
TryCreateEnumerable(serviceIdentifier, callSiteChain);
return callSite;
}
}

那服務點的建立過程我就簡單概述一下啦

1、尋找呼叫點緩存,存在就直接返回啦

2、服務識別元會被轉成服務描述符 ServiceDescriptor (key化服務不指定key預設取last)

3、計算

ServiceCallSite

,依次是:

1、TryCreateExact

計算 ResultCache

如果已經有實作例項了,則返回 ConstantCallSite :表示直接返回已經建立的例項的呼叫點。

如果有實作工廠,則返回 FactoryCallSite :表示透過工廠方法建立服務例項的呼叫點。

如果有實作型別,則返回 ConstructorCallSite :表示透過建構函式建立服務例項的呼叫點。

2、TryCreateOpenGeneric

根據泛型定義獲取服務描述符 ServiceDescriptor

計算 ResultCache

使用服務識別元中的具體泛型參數來構造實作的閉合型別

AOT相容性測試(因為不能保證值型別泛型的程式碼已經生成)

如果成功閉合,則返回 ConstructorCallSite :表示透過建構函式建立服務例項的呼叫點。

3、TryCreateEnumerable

確定型別是 IEnumerable<T>

AOT相容性測試(因為不能保證值型別陣列的程式碼已經生成)

如果 T 不是泛型型別,並且可以找到對應的服務描述符集合,則迴圈 TryCreateExact

否則,方向迴圈 TryCreateExact,然後方向迴圈 TryCreateOpenGeneric

四、CallSiteVisitor

好了,有了上面的了解我們可以開始探索服務解析的內幕了。服務解析說白了就是引擎圍著 CallSiteVisitor 轉圈圈,所謂的解析服務,其實就是存取呼叫點了。

protectedvirtual 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:
thrownew ArgumentOutOfRangeException();
}
}

為了方便展示,我們這裏的解析器都拿執行時來說,因為內部是反射,而emit、expression實在是難以觀看。

1、VisitRootCache

那我們來看看單例的情況下,是如何存取的:

protectedoverrideobject? VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)
{
if (callSite.Value isobjectvalue)
{
// Value already calculated, return it directly
returnvalue;
}
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 isobject 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 時呢已經講的很清楚啦,咱對號入座就行了

protectedvirtual 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:
thrownew NotSupportedException(SR.Format(SR.CallSiteTypeNotSupported, callSite.GetType()));
}
}

我們就看看最經典的透過建構函式建立服務例項的呼叫點 ConstructorCallSite ,很顯然就是new嘛,只不過可能構造中依賴其它服務,那就遞迴建立唄。easy,其它幾種太簡單了大家自己去看看吧。

protectedoverrideobjectVisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
{
object?[] parameterValues;
if (constructorCallSite.ParameterCallSites.Length == 0)
{
parameterValues = Array.Empty<object>();
}
else
{
parameterValues = newobject?[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);
}

2、 VisitScopeCache

在存取單例緩存的時候呢,僅僅透過了一個double check lock就搞定了,因為人家全域的嘛,咱再來看看存取作用域緩存,會不會有什麽不一樣

protectedoverrideobject? 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 特別罕見。

internalobject? 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中獨占服務點。

    privateobject? 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, outobject? 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、VisitDisposeCache

    我們看最後一個,也就是 Transient case

    protectedoverrideobject? VisitDisposeCache(ServiceCallSite transientCallSite, RuntimeResolverContext context)
    {
    return context.Scope.CaptureDisposable(VisitCallSiteMain(transientCallSite, context));
    }

    異常的簡單,我們沿用了scope的設計,但是我們沒有進行任何緩存行為。即,每次都去存取呼叫點。

    轉自:xiaolipro

    連結:cnblogs.com/xiaolipro/p/17873575.html#31-servicecollection

    - EOF -


    復制 搜一搜 分享 收藏