今天我先給大家出一道題:
public interface IDbContext
{
}
public class SqlServerDbContext : IDbContext
{
}
public class LongTermSerive : BackgroundService
{
private readonly IDbContext _context;
public LongTermSerive(IDbContext context)
{
_context = context;
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
return Task.CompletedTask;
}
}
builder.Services.AddScoped<IDbContext, SqlServerDbContext>();
builder.Services.AddHostedService<LongTermSerive>();
請(qǐng)問(wèn)以上服務(wù)的注冊(cè)有沒(méi)有問(wèn)題?
熟悉 .NET 的同學(xué)很快就會(huì)說(shuō):這當(dāng)然有問(wèn)題,IDbContext
是 Scope
生命周期,LongTermSerive
因?yàn)樽?cè)成了 HostedService
所以實(shí)際上它是 Singleton
生命周期。Singleton
不能持有 Scope 生命周期的服務(wù)。說(shuō)的更通用一點(diǎn)的話就是:生命周期長(zhǎng)的服務(wù)無(wú)法依賴生命周期比它的服務(wù)。
真的是這樣嗎???
以上回答只說(shuō)對(duì)了一半。這時(shí)候肯定馬上會(huì)有同學(xué)跳出來(lái)說(shuō),“這怎么會(huì)不對(duì)呢?我剛剛都試過(guò)了,VS直接報(bào)錯(cuò)了”。
System.AggregateException: 'Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Microsoft.Extensions.Hosting.IHostedService Lifetime: Singleton ImplementationType: DevelopmentTest.LongTermSerive': Cannot consume scoped service 'DevelopmentTest.IDbContext' from singleton
不要著急讓我們繼續(xù)分析下去。
Captive Dependency#
首先讓我們澄清一個(gè)概念。像以上這種情況:當(dāng)生命周期長(zhǎng)(Singleton)的服務(wù)持有生命周期短(Scope)的服務(wù)的時(shí)候我們叫做 "Captive Dependency"(Transient不在討論范圍內(nèi))。
不知道怎么翻譯成中文比較合適。微軟的文檔上翻譯作"捕獲依賴",個(gè)人認(rèn)為不太恰當(dāng)。
"Captive Dependency" 會(huì)帶來(lái)什么問(wèn)題?
- 生命周期短的服務(wù)會(huì)被 DI 容器及時(shí)釋放,比如調(diào)用了 Dispose 方法,導(dǎo)致后續(xù)的操作失敗。
- 非線程安全。Singleton 的對(duì)象很容易被多個(gè)線程共享,但 Scope 的話大多數(shù)情況都是非線程安全的。比如上面的 DbContext,當(dāng)在線程內(nèi)共享,發(fā)生并發(fā)操作的時(shí)候程序是無(wú)法保證正確運(yùn)行的。
.NET DI 支持 Captive Dependency 嗎?#
當(dāng)我們了解這個(gè)概念后,上面的問(wèn)題可以轉(zhuǎn)換成 " .NET DI 支持 Captive Dependency 嗎?"。
根據(jù)上一次我們的文章的內(nèi)容,我們知道 .NET DI 的行為是跟所在的環(huán)境有關(guān)系的。所以討論這個(gè)問(wèn)題我們還是要分開(kāi)來(lái)看待:
- Development 環(huán)境下,.NET DI 會(huì)在構(gòu)建 ServiceProvider 的時(shí)候去校驗(yàn)服務(wù)的依賴關(guān)系。這個(gè)時(shí)候就會(huì)像上面提到的一樣,直接報(bào)錯(cuò)。
- 非 Development 環(huán)境下在構(gòu)建 ServiceProvider 的時(shí)候不會(huì)校驗(yàn)服務(wù)間的依賴關(guān)系,程序有可能正確運(yùn)行。為什么說(shuō)是有可能呢?因?yàn)檫@個(gè)完全取決與你的代碼是怎么寫的。也許你短生命周期的服務(wù)在某些場(chǎng)景下正巧可以工作,又或者正巧不能工作。但是有一點(diǎn)是明確的,就是 Captive Dependency 是危險(xiǎn)的。因?yàn)楫?dāng)你注冊(cè)成 Scope 或者 Transient 的時(shí)候往往是帶了某種暗示。比如 Scope 對(duì)象是非線程安全的。顯然 Socpe 服務(wù)的編寫者沒(méi)有義務(wù)去考慮被 Singleton 服務(wù)依賴時(shí)候的問(wèn)題。
- 手動(dòng)開(kāi)啟
ValidateScopes = true
的時(shí)候不管什么環(huán)境下都會(huì)進(jìn)行依賴關(guān)系的校驗(yàn),類似 Development 環(huán)境下。
總結(jié)#
現(xiàn)在我們可以作一個(gè)總結(jié):
.NET DI 是支持 Captive Dependency 的,但是在 Development 環(huán)境下或者手動(dòng)開(kāi)啟 ValidateScopes = true 的時(shí)候它不支持,它會(huì)阻止 Captive Dependency。換句話說(shuō) .NET DI 在阻止 Captive Dependency 上只做了一半的工作,并不能 100% 確保不發(fā)生 Captive Dependency。開(kāi)發(fā)者們?cè)趯懘a的時(shí)候還是要自己注意了,不能完全依賴 .NET 的檢測(cè)。
關(guān)于這個(gè)問(wèn)題,我也在 .NET Runtime 的 Repository 下開(kāi)了一個(gè) ticket 進(jìn)行討論。微軟給出的理由是基于性能的考慮,生產(chǎn)環(huán)境這個(gè)校驗(yàn)?zāi)J(rèn)不開(kāi)啟。其實(shí)我個(gè)人覺(jué)得微軟應(yīng)該不管在什么環(huán)境下都默認(rèn)開(kāi)啟校驗(yàn),盡可能的避免 Captive Dependency。因?yàn)?90% 的項(xiàng)目其實(shí)并不在乎這點(diǎn)性能開(kāi)銷。如果你的應(yīng)用程序真的很在乎性能那么可以手動(dòng)關(guān)閉這個(gè)校驗(yàn),這個(gè)時(shí)候開(kāi)發(fā)者自己需要完全對(duì)這個(gè)依賴關(guān)系負(fù)責(zé)。
https://blog.ploeh.dk/2014/06/02/captive-dependency/
https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection-guidelines#captive-dependency
https://github.com/dotnet/runtime/discussions/109491