依賴注入 (DI) 是.NET中一個非常重要的軟體設計模式,它可以幫助我們更好地管理和組織元件,提高程式碼的可讀性,擴充套件性和可測試性。在日常工作中,我們一定遇見過這些問題或者疑惑。
雖然我們可能已經知道了答案,但本文將通過閱讀CLR原始碼的方式來學習DI實現機制,同時也更加深入地理解上述問題。如果您不想閱讀原始碼,可以直接跳至文末的解決方案。
理論篇可以先看一下,防止在下文程式碼不知道這些物件的作用。如果有些概念不是很清晰可以先記著,帶入下文原始碼,應該就可以理解
ServiceProvider: ServiceProvider(依賴注入容器)不僅對外提供GetService()、GetRequiredService()方法,還可以方便地註冊和管理應用程式需要的各種服務。
通過建立ServiceProvider的方式,我們可以更好地理解管理和控制服務範例的生命週期和依賴關係。
應用程式級別的根級ServiceProvider
.NET Core應用程式通常會使用一個應用程式級別的根級ServiceProvider,它是全域性唯一的,並且負責維護所有單例服務的範例。這個範例通常是由WebHostBuilder、HostBuilder或ServiceCollection等類建立和設定的,可以通過IServiceProvider介面來存取。
每個請求的作用域級別的ServiceProvider
除了根級ServiceProvider之外,在.NET Core中還可以建立每個請求的作用域級別的ServiceProvider,它通常用於管理Scoped和Transient服務的生命週期和依賴關係。每個作用域級別的ServiceProvider都有自己獨立的作用域,可以通過IServiceScopeFactory建立,同時也繼承了根級ServiceProvider中註冊的所有單例服務的範例。
自定義級別的ServiceProvider
在某些情況下,我們可能需要自定義級別的ServiceProvider來滿足特定的業務需求,例如,將多個ServiceProvider組合起來以提供更高階別的服務解析和管理功能。此時,我們可以通過實現IServiceProviderFactory介面和IServiceProviderBuilder介面來建立和設定自定義級別的ServiceProvider,從而實現更靈活、可延伸的依賴注入框架。
生命週期管理: 我們可以將依賴注入容器看作一個樹形結構,其中root節點的子節點是Scoped節點,每個Scoped節點的子節點是Transient節點(如果存在)。在容器初始化時,會在root節點下建立和快取所有單例服務的範例,以及建立第一個Scoped節點。每個Scoped節點下都有一個獨立的作用域,用於管理Scoped服務的生命週期和依賴關係,同時還繼承了父級節點(即root或其他Scoped節點)的所有單例服務的範例。
在處理每個新的請求時,依賴注入容器會建立一個新的Scoped節點,並在該節點下建立和快取該請求所需的所有Scoped服務的範例。在完成請求處理後,該Scoped節點及其下屬的服務範例也將被銷燬,從而確保Scoped服務範例的生命週期與請求的作用域相對應。
重要物件
IServiceCollection: 用於註冊應用程式所需的服務範例,並將其新增到依賴注入容器中。
IServiceScopeFactory: 用於建立依賴注入作用域(IServiceScope)的工廠類。每個IServiceScope都可以獨立地管理Scoped和Transient型別的服務範例,並在作用域結束時釋放所有資源。IServiceScope通過ServiceProvider屬性來存取該作用域內的服務範例
ServiceProvider: 可以看作是一個服務容器,它可以方便地註冊、提供和管理應用程式需要的各種服務。還支援建立依賴注入作用域(IServiceScope),可以更好地管理和控制服務範例的生命週期和依賴關係
IServiceProviderFactory: 建立最終的依賴注入容器(IServiceProvider),提供預設的DefaultServiceProviderFactory(也就是官方自帶的IOC),也支援自定義的,比如autofac的AutofacServiceProviderFactory工廠。
ServiceProviderEngineScope: 實現了IServiceProvider和IDisposable介面,用於建立和管理依賴注入作用域(Scope)。通過使用ServiceProviderEngineScope,我們可以存取依賴注入作用域中的服務範例,並實現Scoped和Transient型別的服務範例的生命週期管理。作用域機制可以幫助我們更好地管理和控制應用程式的各個元件之間的依賴關係
CallSiteFactory: 通常由依賴注入容器(如ServiceProvider)在服務解析過程中使用。當容器需要解析某個服務時,它會建立一個CallSiteFactory物件,並使用其中的靜態方法來建立對應的ServiceCallSite物件。然後,容器會將這些ServiceCallSite物件組合成一個樹形結構,最終構建出整個服務範例的解析樹。
ServiceCallSite: 表示服務的解析過程。它包含了服務型別、服務的生命週期、以及從容器中獲取服務範例的方法等資訊
CallSiteVisitor: 通常由依賴注入容器(如ServiceProvider)在服務解析過程中使用。當容器需要解析某個服務時,它會建立一個ServiceCallSite的物件圖,並將其傳遞給CallSiteVisitor進行遍歷和存取。CallSiteVisitor通過呼叫不同節點的虛擬方法,將每個節點的資訊收集起來,並最終構建出服務範例的解析樹。
CallSiteValidator 通常由依賴注入容器(如ServiceProvider)在服務解析過程中使用,用於驗證ServiceCallSite物件圖的正確性。它提供了一組檢查方法,可以檢測ServiceCallSite物件圖中可能存在的迴圈依賴、未註冊的服務型別和生命週期問題等。
以下是原始碼的部分刪減和修改,以便於更好地理解
為了更好地理解依賴注入的整個流程,可以根據依賴注入容器將其簡單理解為以下兩個模組:
設定ConfigureServices,將服務物件(ServiceDescriptor)註冊到了IServiceCollection集合,構建Host主機的時候,會呼叫BuildServiceProvide()方法建立IServiceProvider,並獲取相關服務。
public IWebHost Build()
{
var hostingServices = BuildCommonServices(out var hostingStartupErrors);// 構建WebHost通用服務
var hostingServiceProvider = GetProviderFromFactory(hostingServices);
// 獲取ServiceProvider
IServiceProvider GetProviderFromFactory(IServiceCollection collection)
{
// 構建IServiceProvider物件
var provider = collection.BuildServiceProvider();
// 獲取服務
var factory = provider.GetService<IServiceProviderFactory<IServiceCollection>>();
// 是否使用預設的DefaultServiceProviderFactory類
if (factory != null && !(factory is DefaultServiceProviderFactory))
{
using (provider)
{
return factory.CreateServiceProvider(factory.CreateBuilder(collection));
}
}
return provider;
}
}
BuildServiceProvider是IServiceCollection介面的擴充套件方法。該方法用於建立一個IServiceProvider介面範例,並將已註冊到IServiceCollection容器中的服務物件注入到該範例中。
public static ServiceProvider BuildServiceProvider(this IServiceCollection services, ServiceProviderOptions options)
{
// 生成ServiceProvider物件
return new ServiceProvider(services, options);
}
ServiceProvider類別建構函式,建立依賴注入容器,並將服務描述資訊載入到容器中
internal ServiceProvider(ICollection<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options)
{
// 建立一個根級別的服務引擎作用域
Root = new ServiceProviderEngineScope(this, isRootScope: true);
// 獲取服務引擎用於解析依賴關係
_engine = GetEngine();
// 存取器,動態建立服務
_createServiceAccessor = CreateServiceAccessor;
// 快取已經解析出來的服務範例(執行緒安全)
_realizedServices = new ConcurrentDictionary<Type, Func<ServiceProviderEngineScope, object?>>();
// CallSiteFactory用於建立和快取服務的呼叫站點(ServiceCallSite)
CallSiteFactory = new CallSiteFactory(serviceDescriptors);
// 新增內建的服務
CallSiteFactory.Add(typeof(IServiceProvider), new ServiceProviderCallSite());
CallSiteFactory.Add(typeof(IServiceScopeFactory), new ConstantCallSite(typeof(IServiceScopeFactory), Root));
CallSiteFactory.Add(typeof(IServiceProviderIsService), new ConstantCallSite(typeof(IServiceProviderIsService), CallSiteFactory));
// ValidateScopes屬性為true,表示需要驗證服務範圍
if (options.ValidateScopes)
{
_callSiteValidator = new CallSiteValidator();
}
// ValidateOnBuild屬性為true,需要檢查所有服務是否能夠成功建立
if (options.ValidateOnBuild)
{
List<Exception>? exceptions = null;
foreach (ServiceDescriptor serviceDescriptor in serviceDescriptors)
{
ValidateService(serviceDescriptor);
}
}
}
當ValidateOnBuild屬性為true時,進入到ValidateService方法中。ValidateService方法使用CallSiteFactory物件的GetCallSite方法來獲取對應的ServiceCallSite物件,並將其儲存到callSite變數中。如果callSite不為null,表示該服務可以被成功建立,則呼叫OnCreate方法。
此時我們需要知道並理解ServiceCallSite 物件
ServiceCallSite記錄著從根呼叫站點到當前服務範例的一條依賴鏈。在DI容器中,每一個已註冊的服務都對應一個ServiceCallSite,而所有的CallSite又組成了整個 DI 系統的拓撲結構。在 DI 系統初始化時,容器會通過遞迴呼叫ServiceCallSite上的資訊,來完成整個DI容器的設定和初始化。
internal abstract class ServiceCallSite
{
protected ServiceCallSite(ResultCache cache)
{
Cache = cache;
}
// 服務型別
public abstract Type ServiceType { get; }
// 實現型別
public abstract Type ImplementationType { get; }
// 呼叫鏈型別(Scope、Singleton、Factory、Constructor、CreateInstance等)
public abstract CallSiteKind Kind { get; }
public ResultCache Cache { get; }
// 是否需要捕獲可釋放資源,類似IDisposable介面
public bool CaptureDisposable =>
ImplementationType == null ||
typeof(IDisposable).IsAssignableFrom(ImplementationType) ||
typeof(IAsyncDisposable).IsAssignableFrom(ImplementationType);
}
有沒有發現,ServiceCallSite和ServiceDescriptor有幾分相似。那麼他們有什麼關係和區別呢?
ServiceDescriptor用於描述一個服務範例的資訊,包括服務型別、實現型別、生命週期等。在容器註冊服務時使用的。
ServiceCallSite則表示服務呼叫鏈節點,是ServiceDescriptor的執行時表示形式,即在Resolve服務時,ServiceDescriptor會被轉換為相應的ServiceCallSite。ServiceCallSite包含了解析服務所需要的全部資訊,包括服務型別、實現工廠、參數列等,它能夠通過遞迴存取自己的子節點來構建出完整的服務呼叫鏈。
ValidateService方法驗證服務是否能夠正常建立
private void ValidateService(ServiceDescriptor descriptor)
{
// 這個方法中出現了迴圈依賴和多建構函式
ServiceCallSite callSite = CallSiteFactory.GetCallSite(descriptor, new CallSiteChain());
if (callSite != null)
{
// 這個方法中進行依賴校驗
OnCreate(callSite);
}
}
我們先看GetCallSite方法,GetCallSite嘗試從快取中獲取,如果快取中不存在,則建立CreateCallSite。
internal ServiceCallSite GetCallSite(Type serviceType, CallSiteChain callSiteChain) =>
_callSiteCache.TryGetValue(new ServiceCacheKey(serviceType, DefaultSlot), out ServiceCallSite site) ? site :
CreateCallSite(serviceType, callSiteChain);
CreateCallSite是非常重要的方法,它負責建立和快取CallSite物件,併為整個依賴注入容器的服務解析提供了基礎支援
private ServiceCallSite CreateCallSite(Type serviceType, CallSiteChain callSiteChain)
{
var callsiteLock = _callSiteLocks.GetOrAdd(serviceType, static _ => new object());
// 保證對CallSite快取的執行緒安全。由於多個服務之間可能存在依賴關係,因此需要確保同一時間只有一個服務的CallSite被建立和快取
lock (callsiteLock)
{
// 哦吼,出現了 檢查是否存在迴圈依賴關係,以避免產生無限遞迴呼叫
callSiteChain.CheckCircularDependency(serviceType);
// 依次嘗試建立精確型別、開放泛型型別和IEnumerable型別的CallSite物件,並返回第一個成功建立的物件
ServiceCallSite callSite = TryCreateExact(serviceType, callSiteChain) ??
TryCreateOpenGeneric(serviceType, callSiteChain) ??
TryCreateEnumerable(serviceType, callSiteChain);
return callSite;
}
}
此時,我們發現了判斷迴圈依賴的方法,他是如何實現的呢?我們就要看一下callSiteChain物件了。callSiteChain用於描述服務呼叫站點(CallSite)之間的依賴關係。callSiteChain使用DIctionary容器儲存當前鏈路上的CallSite。如果容器存在當前服務,說明存在迴圈依賴。
舉個栗子: A->B B->A
public CallSiteChain()
{
_callSiteChain = new Dictionary<Type, ChainItemInfo>();
}
public void CheckCircularDependency(Type serviceType)
{
if (_callSiteChain.ContainsKey(serviceType))
{
throw new InvalidOperationException(CreateCircularDependencyExceptionMessage(serviceType));
}
}
我們選擇TryCreateExact方法,進入CreateConstructorCallSite方法,該方法建立和快取ConstructorCallSite物件。此時您就看見多個構造引數是如何進行選擇的啦!如果存在多個建構函式,但其中某個建構函式的引數型別是其他建構函式的子集,則返回該建構函式對應的ConstructorCallSite物件
private ServiceCallSite CreateConstructorCallSite(
ResultCache lifetime,
Type serviceType,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type implementationType,
CallSiteChain callSiteChain)
{
try
{
// 將當前服務加入callSiteChain,以便後續的依賴解析過程中使用
callSiteChain.Add(serviceType, implementationType);
// 獲取所有建構函式
ConstructorInfo[] constructors = implementationType.GetConstructors();
ServiceCallSite[] parameterCallSites = null;
// 0個建構函式
if (constructors.Length == 0)
{
throw new InvalidOperationException(SR.Format(SR.NoConstructorMatch, implementationType));
}
// 1個建構函式
else if (constructors.Length == 1)
{
ConstructorInfo constructor = constructors[0];
ParameterInfo[] parameters = constructor.GetParameters();
if (parameters.Length == 0)
{
return new ConstructorCallSite(lifetime, serviceType, constructor);
}
parameterCallSites = CreateArgumentCallSites(
implementationType,
callSiteChain,
parameters,
throwIfCallSiteNotFound: true);
return new ConstructorCallSite(lifetime, serviceType, constructor, parameterCallSites);
}
// 多個建構函式如何選擇,終於等到你,還好我沒放棄0.0
Array.Sort(constructors,
(a, b) => b.GetParameters().Length.CompareTo(a.GetParameters().Length));
ConstructorInfo bestConstructor = null;
HashSet<Type> bestConstructorParameterTypes = null;
for (int i = 0; i < constructors.Length; i++)
{
ParameterInfo[] parameters = constructors[i].GetParameters();
ServiceCallSite[] currentParameterCallSites = CreateArgumentCallSites(
implementationType,
callSiteChain,
parameters,
throwIfCallSiteNotFound: false);
if (currentParameterCallSites != null)
{
if (bestConstructor == null)
{
bestConstructor = constructors[i];
parameterCallSites = currentParameterCallSites;
}
else
{
// Since we're visiting constructors in decreasing order of number of parameters,
// we'll only see ambiguities or supersets once we've seen a 'bestConstructor'.
//由於我們以引數數量遞減的順序存取建構函式,
//只有在看到「最佳建構函式」後,我們才會看到歧義或超集。
if (bestConstructorParameterTypes == null)
{
bestConstructorParameterTypes = new HashSet<Type>();
foreach (ParameterInfo p in bestConstructor.GetParameters())
{
bestConstructorParameterTypes.Add(p.ParameterType);
}
}
foreach (ParameterInfo p in parameters)
{
if (!bestConstructorParameterTypes.Contains(p.ParameterType))
{
// Ambiguous match exception
throw new InvalidOperationException(string.Join(
Environment.NewLine,
SR.Format(SR.AmbiguousConstructorException, implementationType),
bestConstructor,
constructors[i]));
}
}
}
}
}
if (bestConstructor == null)
{
throw new InvalidOperationException(
SR.Format(SR.UnableToActivateTypeException, implementationType));
}
else
{
Debug.Assert(parameterCallSites != null);
return new ConstructorCallSite(lifetime, serviceType, bestConstructor, parameterCallSites);
}
}
finally
{
callSiteChain.Remove(serviceType);
}
}
看到這裡,我們已經解決了兩個問題:
Singleton服務不能依賴Scoped服務,是如何校驗的?我們回到剛才OnCreate的地方繼續閱讀。
private void OnCreate(ServiceCallSite callSite)
{
_callSiteValidator?.ValidateCallSite(callSite);
}
ValidateCallSite方法,用於驗證指定的ServiceCallSite物件是否正確,並將其中包含的作用域服務新增到_scopedServices字典中。
在ValidateCallSite方法中,我們首先使用VisitCallSite方法遍歷整個ServiceCallSite物件,並返回其中所包含的作用域服務型別。如果ServiceCallSite物件中存在作用域服務,則將其新增到_scopedServices字典中,以便後續的依賴解析過程中使用。
public void ValidateCallSite(ServiceCallSite callSite)
{
Type scoped = VisitCallSite(callSite, default);
if (scoped != null)
{
_scopedServices[callSite.ServiceType] = scoped;
}
}
ValidateCallSite存在VisitScopeCache方法,該方法首先判斷當前ServiceCallSite物件是否是IServiceScopeFactory型別,如果是,則直接返回null。否則,我們檢查state.Singleton屬性是否為null,如果不為null,則說明當前ServiceCallSite物件屬於單例服務,並且其中包含作用域服務的注入,此時將丟擲InvalidOperationException異常,提示使用者檢查服務依賴關係是否正確;否則,我們繼續遞迴遍歷ServiceCallSite物件圖。
protected override Type VisitScopeCache(ServiceCallSite scopedCallSite, CallSiteValidatorState state)
{
// We are fine with having ServiceScopeService requested by singletons
if (scopedCallSite.ServiceType == typeof(IServiceScopeFactory))
{
return null;
}
// ScopedInSingletonException異常!
if (state.Singleton != null)
{
throw new InvalidOperationException(SR.Format(SR.ScopedInSingletonException,
scopedCallSite.ServiceType,
state.Singleton.ServiceType,
nameof(ServiceLifetime.Scoped).ToLowerInvariant(),
nameof(ServiceLifetime.Singleton).ToLowerInvariant()
));
}
VisitCallSiteMain(scopedCallSite, state);
return scopedCallSite.ServiceType;
}
獲取服務
以上我們可以歸納為構建IServiceProvider,然後我們通過GetService()方法,看下如何獲取服務。
從快取中獲取指定型別的服務,如果快取中不存在,則呼叫_createServiceAccessor委託建立一個新的範例,並將其新增到快取中
internal object GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
{
// 使用了ConcurrentDictionary來快取已經解析的服務
Func<ServiceProviderEngineScope, object> realizedService = _realizedServices.GetOrAdd(serviceType, _createServiceAccessor);
OnResolve(serviceType, serviceProviderEngineScope);
// 服務的實際實現
var result = realizedService.Invoke(serviceProviderEngineScope);
return result;
}
當快取中不存在的時候,我們使用_createServiceAccessor建立一個新的範例(和上文獲取callSite流程一致)。
private Func<ServiceProviderEngineScope, object> CreateServiceAccessor(Type serviceType)
{
// 取給定服務型別的CallSite物件
ServiceCallSite callSite = CallSiteFactory.GetCallSite(serviceType, new CallSiteChain());
if (callSite != null)
{
OnCreate(callSite);
// 服務具有Singleton生命週期,可以優化處理,避免每次獲取服務範例時都需要重新建立
if (callSite.Cache.Location == CallSiteResultCacheLocation.Root)
{
object value = CallSiteRuntimeResolver.Instance.Resolve(callSite, Root);
return scope => value;
}
// 服務具有Transient或Scoped生命週期,需要建立並返回一個新的服務範例存取器
return _engine.RealizeService(callSite);
}
return _ => null;
}
建立一個生命週期為單例的SingletonService和另一個生命週期為作用域的ScopedService,SingletonService服務依賴ScopedService服務。就會報錯:
Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: MyTest.Service.ISingletonService Lifetime: Singleton ImplementationType: MyTest.Service.SingletonService': Cannot consume scoped service 'MyTest.Service.IScopedService' from singleton 'MyTest.Service.ISingletonService'.)
解決方法
public class SingletonService : ISingletonService
{
private readonly IScopedService _scopedService;
public SingletonService(IServiceScopeFactory serviceScopeFactory)
{
_scopedService = serviceScopeFactory.CreateScope().ServiceProvider.GetRequiredService<IScopedService>();
}
}
public class SingletonService : ISingletonService
{
private readonly IScopedService _scopedService;
public SingletonService(IServiceProvider serviceProvider)
{
_scopedService = serviceProvider.CreateScope().ServiceProvider.GetRequiredService<IScopedService>();
}
}
如果實現類沒有建構函式,則丟擲NoConstructorMatch異常。
如果實現類只有一個建構函式,判斷該建構函式是否無參。如果是無參建構函式,則直接返回ConstructorCallSite;否則,對建構函式的引數建立對應的parameterCallSites,並返回ConstructorCallSite。
多個建構函式的邏輯
如果不是很理解選擇邏輯,可以結合上文中的CreateConstructorCallSite方法,觀看程式碼會更加直接,便於理解。
參考:
http://misko.hevery.com/2008/08/01/circular-dependency-in-constructors-and-dependency-injection/
我認為出現迴圈依賴,是我們程式碼結構設計有問題,根本解決方案是將依賴關係分解成更小的部分,從而避免出現迴圈依賴的情況,同時使個程式碼結構更加清晰、簡單。
在這種情況下,真實原因是兩個物件中的一個隱藏了另一個物件 C。A 包含 C 或 B 包含 C。我們假設B包含了C。
class A {
final B b;
A(B b){
this.b = b;
}
}
class B {
final A a;
B(A a){
this.a = a;
}
}
+---------+ +---------+
| A |<-----| B |
| | | | +-+ |
| | | +->|C| |
| |------+---->| | |
| | | +-+ |
+---------+ +---------+
我們將C單獨抽出來,作為一個服務,讓A和B都依賴於C,這樣就可以解決迴圈依賴的問題。
+---------+
+---------+ | B |
| A |<-------------| |
| | | |
| | +---+ | |
| |--->| C |<----| |
| | +---+ +---------+
+---------+
class C {
C(){
}
}
class A {
final C c;
A(C c){
this.c = c;
}
}
class B {
final A a;
final C c;
B(A a, C c){
this.a = a;
this.c = c;
}
}
class C : IC
{
private readonly IServiceProvider _services;
public C(IServiceProvider services)
{
_services = services;
}
public void Bar()
{
...
var a = _services.GetRequiredService<IA>();
a.Foo();
...
}
}
public static IServiceCollection AddLazyResolution(this IServiceCollection services)
{
return services.AddTransient(
typeof(Lazy<>),
typeof(LazilyResolved<>));
}
private class LazilyResolved<T> : Lazy<T>
{
public LazilyResolved(IServiceProvider serviceProvider)
: base(serviceProvider.GetRequiredService<T>)
{
}
}
然後再 Startup.cs 中的 ConfigureServices 方法中這樣寫
services.AddLazyResolution();
在依賴的類中IA,注入Lazy,當您需要使用時IA,只需存取lazy的值 Value 即可:
class C : IC
{
private readonly Lazy<IA> _a;
public C(Lazy<IA> a)
{
_a = a;
}
public void Bar()
{
...
_a.Value.Foo();
...
}
}
注意:不要存取建構函式中的值,儲存Lazy即可 ,在建構函式中存取該值,這將導致我們試圖解決的相同問題。
這個解決方案不是完美的,但是它解決了最初的問題卻沒有太多麻煩,並且依賴項仍然在建構函式中明確宣告,我可以看到類之間的依賴關係。
如果您覺得這篇文章有所收穫,還請點個贊並關注。如果您有任何建議或意見,歡迎在評論區留言,非常感謝您的支援和指導!