依賴注入(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
是一個列舉,定義了幾個值
Root
:表示服務例項應該在根級別的IServiceProvider
中快取。這通常意味著服務例項是單例的(Singleton),在整個應用程式的生命週期內只會建立一次,並且在所有請求中共享。Scope
:表示服務例項應該在當前作用域(Scope)中快取。對於作用域服務(Scoped),例項會在每個作用域中建立一次,並在該作用域內的所有請求中共享。Dispose
:儘管這個名稱可能會讓人誤解,但在ResultCache
的上下文中,Dispose
表示著服務是瞬態的(每次請求都建立新例項)。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;
}
}
那服務點的建立過程我就簡單概述一下啦
- 查詢呼叫點快取,存在就直接返回啦
- 服務識別符號會被轉成服務描述符
ServiceDescriptor
(key化服務不指定key預設取last) - 計算
ServiceCallSite
,依次是:- TryCreateExact
- 計算
ResultCache
- 如果已經有實現例項了,則返回
ConstantCallSite
:表示直接返回已經建立的例項的呼叫點。 - 如果有實現工廠,則返回
FactoryCallSite
:表示透過工廠方法建立服務例項的呼叫點。 - 如果有實現型別,則返回
ConstructorCallSite
:表示透過建構函式建立服務例項的呼叫點。
- 計算
- TryCreateOpenGeneric
- 根據泛型定義獲取服務描述符
ServiceDescriptor
- 計算
ResultCache
- 使用服務識別符號中的具體泛型引數來構造實現的閉合型別
- AOT相容性測試(因為不能保證值型別泛型的程式碼已經生成)
- 如果成功閉合,則返回
ConstructorCallSite
:表示透過建構函式建立服務例項的呼叫點。
- 根據泛型定義獲取服務描述符
- TryCreateEnumerable
- 確定型別是
IEnumerable<T>
- AOT相容性測試(因為不能保證值型別陣列的程式碼已經生成)
- 如果
T
不是泛型型別,並且可以找到對應的服務描述符集合,則迴圈 TryCreateExact - 否則,方向迴圈 TryCreateExact,然後方向迴圈 TryCreateOpenGeneric
- 確定型別是
- TryCreateExact
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 上鍊路涉及的服務點,直至結束
- 那顯然 engine scope 也應該是 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的設計,但是我們沒有進行任何快取行為。即,每次都去訪問呼叫點。