.NET全局靜態(tài)可訪問(wèn)IServiceProvider的過(guò)程詳解(支持Blazor)
DependencyInjection.StaticAccessor
前言
如何在靜態(tài)方法中訪問(wèn)DI容器長(zhǎng)期以來(lái)一直都是一個(gè)令人苦惱的問(wèn)題,特別是對(duì)于熱愛(ài)編寫擴(kuò)展方法的朋友。之所以會(huì)為這個(gè)問(wèn)題苦惱,是因?yàn)橐粋€(gè)特殊的服務(wù)生存期——范圍內(nèi)(Scoped),所謂的Scoped就是范圍內(nèi)單例,最常見(jiàn)的WebAPI/MVC中一個(gè)請(qǐng)求對(duì)應(yīng)一個(gè)范圍,所有注冊(cè)為Scoped的對(duì)象在同一個(gè)請(qǐng)求中是單例的。如果僅僅用一個(gè)靜態(tài)字段存儲(chǔ)應(yīng)用啟動(dòng)時(shí)創(chuàng)建出的IServiceProvider對(duì)象,那么在一個(gè)請(qǐng)求中通過(guò)該字段是無(wú)法正確獲取當(dāng)前請(qǐng)求中創(chuàng)建的Scoped對(duì)象的。
在早些時(shí)候有針對(duì)肉夾饃(Rougamo)訪問(wèn)DI容器發(fā)布了一些列NuGet,由于肉夾饃不僅能應(yīng)用到實(shí)例方法上還能夠應(yīng)用到靜態(tài)方法上,所以肉夾饃訪問(wèn)DI容器的根本問(wèn)題就是如何在靜態(tài)方法中訪問(wèn)DI容器??紤]到靜態(tài)方法訪問(wèn)DI容器是一個(gè)常見(jiàn)的公共問(wèn)題,所以現(xiàn)在將核心邏輯抽離成一系列單獨(dú)的NuGet包,方便不使用肉夾饃的朋友使用。
快速開(kāi)始
啟動(dòng)項(xiàng)目引用DependencyInjection.StaticAccessor.Hosting
dotnet add package DependencyInjection.StaticAccessor.Hosting
非啟動(dòng)項(xiàng)目引用DependencyInjection.StaticAccessor
dotnet add package DependencyInjection.StaticAccessor
// 1. 初始化。這里用通用主機(jī)進(jìn)行演示,其他類型項(xiàng)目后面將分別舉例
var builder = Host.CreateDefaultBuilder();
builder.UsePinnedScopeServiceProvider(); // 僅此一步完成初始化
var host = builder.Build();
host.Run();
// 2. 在任何地方獲取
class Test
{
public static void M()
{
var yourService = PinnedScope.ScopedServices.GetService<IYourService>();
}
}如上示例,通過(guò)靜態(tài)屬性PinnedScope.ScopedServices即可獲取當(dāng)前Scope的IServiceProvider對(duì)象,如果當(dāng)前不在任何一個(gè)Scope中時(shí),該屬性返回根IServiceProvider。
版本說(shuō)明
由于DependencyInjection.StaticAccessor的實(shí)現(xiàn)包含了通過(guò)反射訪問(wèn)微軟官方包非public成員,官方的內(nèi)部實(shí)現(xiàn)隨著版本的迭代也在不斷地變化,所以針對(duì)官方包不同版本發(fā)布了對(duì)應(yīng)的版本。DependencyInjection.StaticAccessor的所有NuGet包都采用語(yǔ)義版本號(hào)格式(SemVer),其中主版本號(hào)與Microsoft.Extensions.*相同,次版本號(hào)為功能發(fā)布版本號(hào),修訂號(hào)為BUG修復(fù)及微小改動(dòng)版本號(hào)。請(qǐng)各位在安裝NuGet包時(shí)選擇與自己引用的Microsoft.Extensions.*主版本號(hào)相同的最新版本。
另外需要說(shuō)明的是,由于我本地創(chuàng)建blazor項(xiàng)目時(shí)只能選擇.NET8.0,所以blazor相關(guān)包僅提供了8.0版本,如果確實(shí)有低版本的需求,可以到github中提交issue。
WebAPI/MVC初始化示例
啟動(dòng)項(xiàng)目引用DependencyInjection.StaticAccessor.Hosting
dotnet add package DependencyInjection.StaticAccessor.Hosting
非啟動(dòng)項(xiàng)目引用DependencyInjection.StaticAccessor
dotnet add package DependencyInjection.StaticAccessor
var builder = WebApplication.CreateBuilder(); builder.Host.UsePinnedScopeServiceProvider(); // 唯一初始化步驟 var app = builder.Build(); app.Run();
Blazor使用示例
Blazor的DI Scope是一個(gè)特殊的存在,在WebAssembly模式下Scoped等同于單例;而在Server模式下,Scoped對(duì)應(yīng)一個(gè)SignalR連接。針對(duì)Blazor的這種特殊的Scope場(chǎng)景,除了初始化操作,還需要一些額外操作。
我們知道,Blazor項(xiàng)目在創(chuàng)建時(shí)可以選擇交互渲染模式,除了Server模式外,其他的模式都會(huì)創(chuàng)建兩個(gè)項(xiàng)目,多出來(lái)的這個(gè)項(xiàng)目的名稱以.Client結(jié)尾。這里我稱.Client項(xiàng)目為Client端項(xiàng)目,另一個(gè)項(xiàng)目為Server端項(xiàng)目(Server模式下唯一的那個(gè)項(xiàng)目也稱為Server端項(xiàng)目)。
Server端項(xiàng)目
安裝NuGet
啟動(dòng)項(xiàng)目引用
DependencyInjection.StaticAccessor.Blazordotnet add package DependencyInjection.StaticAccessor.Blazor
非啟動(dòng)項(xiàng)目引用
DependencyInjection.StaticAccessordotnet add package DependencyInjection.StaticAccessor
初始化
var builder = WebApplication.CreateBuilder(); builder.Host.UsePinnedScopeServiceProvider(); // 唯一初始化步驟 var app = builder.Build(); app.Run();
頁(yè)面繼承PinnedScopeComponentBase
推薦直接在_Imports.razor中聲明。
// _Imports.razor @inherits DependencyInjection.StaticAccessor.Blazor.PinnedScopeComponentBase
Client端項(xiàng)目
與Server端步驟基本一致,只是引用的NuGet有所區(qū)別:
安裝NuGet
啟動(dòng)項(xiàng)目引用
DependencyInjection.StaticAccessor.Blazor.WebAssemblydotnet add package DependencyInjection.StaticAccessor.Blazor.WebAssembly
非啟動(dòng)項(xiàng)目引用
DependencyInjection.StaticAccessordotnet add package DependencyInjection.StaticAccessor
初始化
var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.UsePinnedScopeServiceProvider(); await builder.Build().RunAsync();
頁(yè)面繼承PinnedScopeComponentBase
推薦直接在_Imports.razor中聲明。
// _Imports.razor @inherits DependencyInjection.StaticAccessor.Blazor.PinnedScopeComponentBase
已有自定義ComponentBase基類的解決方案
你可能會(huì)使用其他包定義的ComponentBase基類,由于C#不支持多繼承,所以這里提供了不繼承PinnedScopeComponentBase的解決方案。
// 假設(shè)你現(xiàn)在使用的ComponentBase基類是ThirdPartyComponentBase
// 定義新的基類繼承ThirdPartyComponentBase
public class YourComponentBase : ThirdPartyComponentBase, IHandleEvent, IServiceProviderHolder
{
private IServiceProvider _serviceProvider;
[Inject]
public IServiceProvider ServiceProvider
{
get => _serviceProvider;
set
{
PinnedScope.Scope = new FoolScope(value);
_serviceProvider = value;
}
}
Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
{
return this.PinnedScopeHandleEventAsync(callback, arg);
}
}
// _Imports.razor
@inherits YourComponentBase其他ComponentBase基類
除了PinnedScopeComponentBase,還提供了PinnedScopeOwningComponentBase和PinnedScopeLayoutComponentBase,后續(xù)會(huì)根據(jù)需要可能會(huì)加入更多類型。如有需求,也歡迎反饋和提交PR.
注意事項(xiàng)
避免通過(guò)PinnedScope直接操作IServiceScope
雖然你可以通過(guò)PinnedScope.Scope獲取當(dāng)前的DI Scope,但最好不要通過(guò)該屬性直接操作IServiceScope對(duì)象,比如調(diào)用Dispose方法,你應(yīng)該通過(guò)你創(chuàng)建Scope時(shí)創(chuàng)建的變量進(jìn)行操作。
不支持非通常Scope
一般日常開(kāi)發(fā)時(shí)不需要關(guān)注這個(gè)問(wèn)題的,通常的AspNetCore項(xiàng)目也不會(huì)出現(xiàn)這樣的場(chǎng)景,而Blazor就是官方項(xiàng)目類型中一個(gè)非通常DI Scope的案例。
在解釋什么是非通常Scope前,我先聊聊通常的Scope模式。我們知道DI Scope是可以嵌套的,在通常情況下,嵌套的Scope呈現(xiàn)的是一種棧的結(jié)構(gòu),后創(chuàng)建的scope先釋放,井然有序。
using (var scope11 = serviceProvider.CreateScope()) // push scope11. [scope11]
{
using (var scope21 = scope11.ServiceProvider.CreateScope()) // push scope21. [scope11, scope21]
{
using (var scope31 = scope21.ServiceProvider.CreateScope()) // push scope31. [scope11, scope21, scope31]
{
} // pop scope31. [scope11, scope21]
using (var scope32 = scope21.ServiceProvider.CreateScope()) // push scope32. [scope11, scope21, scope32]
{
} // pop scope32. [scope11, scope21]
} // pop scope21. [scope11]
using (var scope22 = scope11.ServiceProvider.CreateScope()) // push scope22. [scope11, scope22]
{
} // pop scope22. [scope22]
} // pop scope11. []了解了非通常Scope,那么就很好理解非通常Scope了,只要是不按照這種井然有序的棧結(jié)構(gòu)的,那就是非通常Scope。比較常見(jiàn)的就是Blazor的這種情況:
我們知道,Blazor SSR通過(guò)SignalR實(shí)現(xiàn)SPA,一個(gè)SignalR連接對(duì)應(yīng)一個(gè)DI Scope,界面上的各種事件(點(diǎn)擊、獲取焦點(diǎn)等)通過(guò)SignalR通知服務(wù)端回調(diào)事件函數(shù),而這個(gè)回調(diào)便是從外部橫插一腳與SignalR進(jìn)行交互的,在不進(jìn)行特殊處理的情況下,回調(diào)事件所屬的Scope是當(dāng)前回調(diào)事件新創(chuàng)建的Scope,但我們?cè)诨卣{(diào)事件中與之交互的Component是SignalR所屬Scope創(chuàng)建的,這就出現(xiàn)了Scope交叉交互的情況。PinnedScopeComponentBase所做的便是在執(zhí)行回調(diào)函數(shù)之前,將PinnedScope.Scope重設(shè)回SignalR對(duì)應(yīng)Scope。
肉夾饃相關(guān)應(yīng)用
正如前面所說(shuō),DependencyInjection.StaticAccessor的核心邏輯是從肉夾饃的DI擴(kuò)展中抽離出來(lái)的,抽離后肉夾饃DI擴(kuò)展將依賴于DependencyInjection.StaticAccessor?,F(xiàn)在你可以直接引用DependencyInjection.StaticAccessor,然后直接通過(guò)PinnedScope.Scope與DI進(jìn)行交互,但還是推薦通過(guò)肉夾饃DI擴(kuò)展進(jìn)行交互,DI擴(kuò)展提供了一些額外的功能,稍后將一一介紹。
DI擴(kuò)展包變化
Autofac相關(guān)包未發(fā)生重大變化,后續(xù)介紹的擴(kuò)展包都是官方DependencyInjection的相關(guān)擴(kuò)展包
本次不僅僅是一個(gè)簡(jiǎn)單的代碼抽離,代碼的核心實(shí)現(xiàn)上也有更新,更新后移出了擴(kuò)展方法CreateResolvableScope,直接支持官方的CreateScope和CreateAsyncScope方法。同時(shí)擴(kuò)展包Rougamo.Extensions.DependencyInjection.AspNetCore和Rougamo.Extensions.DependencyInjection.GenericHost合并為Rougamo.Extensions.DependencyInjection.Microsoft。
Rougamo.Extensions.DependencyInjection.Microsoft
僅定義切面類型的項(xiàng)目需要引用Rougamo.Extensions.DependencyInjection.Microsoft,啟動(dòng)項(xiàng)目根據(jù)項(xiàng)目類型引用DependencyInjection.StaticAccessor相關(guān)包即可,初始化也是僅需要完成DependencyInjection.StaticAccessor初始化即可。
更易用的擴(kuò)展
Rougamo.Extensions.DependencyInjection.Microsoft針對(duì)MethodContext提供了豐富的DI擴(kuò)展方法,簡(jiǎn)化代碼編寫。
public class TestAttribute : AsyncMoAttribute
{
public override ValueTask OnEntryAsync(MethodContext context)
{
context.GetService<ITestService>();
context.GetRequiredService(typeof(ITestService));
context.GetServices<ITestService>();
}
}從當(dāng)前宿主類型實(shí)例中獲取IServiceProvider
DependencyInjection.StaticAccessor提供的是一種常用場(chǎng)景下獲取當(dāng)前Scope的IServiceProvider解決方案,但在千奇百怪的開(kāi)發(fā)需求中,總會(huì)出現(xiàn)一些不尋常的DI Scope場(chǎng)景,比如前面介紹的非通常Scope,再比如Blazor。針對(duì)這種場(chǎng)景,肉夾饃DI擴(kuò)展雖然不能幫你獲取到正確的IServiceProvider對(duì)象,但如果你自己能夠提供獲取方式,肉夾饃DI擴(kuò)展可以方便的集成該獲取方式。
下面以Blazor為例,雖然已經(jīng)針對(duì)Blazor特殊的DI Scope提供了通用解決方案,但Blazor還存在著自己的特殊場(chǎng)景。我們知道Blazor SSR服務(wù)生存期是整個(gè)SignalR的生存期,這個(gè)生存期可能非常長(zhǎng),一個(gè)生存期期間可能會(huì)創(chuàng)建多個(gè)頁(yè)面(ComponentBase),這多個(gè)頁(yè)面也將共享注冊(cè)為Scoped的對(duì)象,這在某些場(chǎng)景下可能會(huì)存在問(wèn)題(比如共享EF DBContext),所以微軟提供了OwningComponentBase,它提供了更短的服務(wù)生存期,集成該類可以通過(guò)ScopedServices屬性訪問(wèn)IServiceProvider對(duì)象。
// 1. 定義前鋒類型,針對(duì)OwningComponentBase返回ScopedServices屬性
public class OwningComponentScopeForward : SpecificPropertyFoolScopeProvider, IMethodBaseScopeForward
{
public override string PropertyName => "ScopedServices";
}
// 2. 初始化
var builder = WebApplication.CreateBuilder();
// 初始化DependencyInjection.StaticAccessor
builder.Host.UsePinnedScopeServiceProvider();
// 注冊(cè)前鋒類型
builder.Services.AddMethodBaseScopeForward<OwningComponentScopeForward>();
var app = builder.Build();
app.Run();
// 3. 使用
public class TestAttribute : AsyncMoAttribute
{
public override ValueTask OnEntryAsync(MethodContext context)
{
// 當(dāng)TestAttribute應(yīng)用到OwningComponentBase子類方法上時(shí),ITestService將從OwningComponentBase.ScopedServices中獲取
context.GetService<ITestService>();
}
}除了上面示例中提供的OwningComponentScopeForward,還有根據(jù)字段名稱獲取的SpecificFieldFoolScopeProvider,根據(jù)宿主類型通過(guò)lambda表達(dá)式獲取的TypedFoolScopeProvider<>,這里就不一一舉例了,如果你的獲取邏輯更加復(fù)雜,可以直接實(shí)現(xiàn)先鋒類型接口IMethodBaseScopeForward。
除了前鋒類型接口IMethodBaseScopeForward,還提供了守門員類型接口IMethodBaseScopeGoalie,在調(diào)用GetService系列擴(kuò)展方法時(shí),內(nèi)部實(shí)現(xiàn)按 [先鋒類型 -> PinnedScope.Scope.ServiceProvider -> 守門員類型 -> PinnedScope.RootServices] 的順序嘗試獲取IServiceProvider對(duì)象。
完整示例
完整示例請(qǐng)?jiān)L問(wèn):https://github.com/inversionhourglass/Rougamo.DI/tree/master/samples
到此這篇關(guān)于.NET全局靜態(tài)可訪問(wèn)IServiceProvider(支持Blazor)的文章就介紹到這了,更多相關(guān).NET全局靜態(tài)可訪問(wèn)IServiceProvider內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- ASP.NET Core如何實(shí)現(xiàn)簡(jiǎn)單的靜態(tài)網(wǎng)站滾動(dòng)更新
- .net core 靜態(tài)類獲取appsettings的方法
- ASP.NET Core靜態(tài)文件的使用方法
- .Net Core中間件之靜態(tài)文件(StaticFiles)示例詳解
- .NET全局靜態(tài)可訪問(wèn)IServiceProvider的過(guò)程詳解(支持Blazor)
- ASP.NET?Core在Task中使用IServiceProvider的問(wèn)題解析
- ASP.NET Core 3框架揭秘之 異步線程無(wú)法使用IServiceProvider問(wèn)題
相關(guān)文章
Asp.net MVC SignalR來(lái)做實(shí)時(shí)Web聊天實(shí)例代碼
本篇文章主要介紹了Asp.net SignalR來(lái)做實(shí)時(shí)Web聊天實(shí)例代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06
asp.net url傳遞后地址欄亂碼(中文超過(guò)兩個(gè)漢字)
asp.net 頁(yè)面?zhèn)髦形某^(guò)兩個(gè)漢字后面就亂碼,編碼編好的url是正確的,可傳到另一個(gè)頁(yè)面就會(huì)出錯(cuò),在地址欄就已經(jīng)亂碼了,本文介紹詳細(xì)的解決方法,感興趣的朋友可以了解下,或許對(duì)你學(xué)習(xí)asp.net有所幫助2013-02-02
.NET?中實(shí)現(xiàn)?List?集合去重的方法
DistinctBy方法是.NET 6和.NET7中LINQ 的一個(gè)非常實(shí)用的新特性,我們?cè)?nbsp;LINQ 查詢中根據(jù)指定的鍵對(duì)集合進(jìn)行去重,簡(jiǎn)化了代碼并提高了開(kāi)發(fā)效率,本文給大家介紹.NET中快速實(shí)現(xiàn)List集合去重的方法,感興趣的朋友跟隨小編一起看看吧2026-02-02
.NET Framework集成Quartz的實(shí)現(xiàn)示例
本文主要介紹了.NET Framework集成Quartz的實(shí)現(xiàn)示例,Quartz 主要用于定時(shí)執(zhí)行任務(wù)方面,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-03-03
ASP.NET web.config中 數(shù)據(jù)庫(kù)連接字符串加密解密
本文主要介紹利用aspnet_regiis.exe工具對(duì)web.config中connectionStrings節(jié)點(diǎn)進(jìn)行加密和解密的過(guò)程,希望對(duì)大家有所幫助。2016-05-05
asp.net core3.1cookie和jwt混合認(rèn)證授權(quán)實(shí)現(xiàn)多種身份驗(yàn)證方案
身份驗(yàn)證是確定用戶身份的過(guò)程。 授權(quán)是確定用戶是否有權(quán)訪問(wèn)資源的過(guò)程。本文主要介紹了asp.net core3.1cookie和jwt混合認(rèn)證授權(quán)實(shí)現(xiàn)多種身份驗(yàn)證方案,感興趣的可以了解一下2021-09-09

