.NET全局靜態(tài)可訪問IServiceProvider的過程詳解(支持Blazor)
DependencyInjection.StaticAccessor
前言
如何在靜態(tài)方法中訪問DI容器長期以來一直都是一個令人苦惱的問題,特別是對于熱愛編寫擴(kuò)展方法的朋友。之所以會為這個問題苦惱,是因?yàn)橐粋€特殊的服務(wù)生存期——范圍內(nèi)(Scoped),所謂的Scoped就是范圍內(nèi)單例,最常見的WebAPI/MVC中一個請求對應(yīng)一個范圍,所有注冊為Scoped的對象在同一個請求中是單例的。如果僅僅用一個靜態(tài)字段存儲應(yīng)用啟動時創(chuàng)建出的IServiceProvider
對象,那么在一個請求中通過該字段是無法正確獲取當(dāng)前請求中創(chuàng)建的Scoped對象的。
在早些時候有針對肉夾饃(Rougamo)訪問DI容器發(fā)布了一些列NuGet,由于肉夾饃不僅能應(yīng)用到實(shí)例方法上還能夠應(yīng)用到靜態(tài)方法上,所以肉夾饃訪問DI容器的根本問題就是如何在靜態(tài)方法中訪問DI容器??紤]到靜態(tài)方法訪問DI容器是一個常見的公共問題,所以現(xiàn)在將核心邏輯抽離成一系列單獨(dú)的NuGet包,方便不使用肉夾饃的朋友使用。
快速開始
啟動項(xiàng)目引用DependencyInjection.StaticAccessor.Hosting
dotnet add package DependencyInjection.StaticAccessor.Hosting
非啟動項(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>(); } }
如上示例,通過靜態(tài)屬性PinnedScope.ScopedServices
即可獲取當(dāng)前Scope的IServiceProvider
對象,如果當(dāng)前不在任何一個Scope中時,該屬性返回根IServiceProvider
。
版本說明
由于DependencyInjection.StaticAccessor
的實(shí)現(xiàn)包含了通過反射訪問微軟官方包非public成員,官方的內(nèi)部實(shí)現(xiàn)隨著版本的迭代也在不斷地變化,所以針對官方包不同版本發(fā)布了對應(yīng)的版本。DependencyInjection.StaticAccessor
的所有NuGet包都采用語義版本號格式(SemVer),其中主版本號與Microsoft.Extensions.*
相同,次版本號為功能發(fā)布版本號,修訂號為BUG修復(fù)及微小改動版本號。請各位在安裝NuGet包時選擇與自己引用的Microsoft.Extensions.*
主版本號相同的最新版本。
另外需要說明的是,由于我本地創(chuàng)建blazor項(xiàng)目時只能選擇.NET8.0,所以blazor相關(guān)包僅提供了8.0版本,如果確實(shí)有低版本的需求,可以到github中提交issue。
WebAPI/MVC初始化示例
啟動項(xiàng)目引用DependencyInjection.StaticAccessor.Hosting
dotnet add package DependencyInjection.StaticAccessor.Hosting
非啟動項(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是一個特殊的存在,在WebAssembly模式下Scoped等同于單例;而在Server模式下,Scoped對應(yīng)一個SignalR連接。針對Blazor的這種特殊的Scope場景,除了初始化操作,還需要一些額外操作。
我們知道,Blazor項(xiàng)目在創(chuàng)建時可以選擇交互渲染模式,除了Server模式外,其他的模式都會創(chuàng)建兩個項(xiàng)目,多出來的這個項(xiàng)目的名稱以.Client
結(jié)尾。這里我稱.Client
項(xiàng)目為Client端項(xiàng)目,另一個項(xiàng)目為Server端項(xiàng)目(Server模式下唯一的那個項(xiàng)目也稱為Server端項(xiàng)目)。
Server端項(xiàng)目
安裝NuGet
啟動項(xiàng)目引用
DependencyInjection.StaticAccessor.Blazor
dotnet add package DependencyInjection.StaticAccessor.Blazor
非啟動項(xiàng)目引用
DependencyInjection.StaticAccessor
dotnet add package DependencyInjection.StaticAccessor
初始化
var builder = WebApplication.CreateBuilder(); builder.Host.UsePinnedScopeServiceProvider(); // 唯一初始化步驟 var app = builder.Build(); app.Run();
頁面繼承PinnedScopeComponentBase
推薦直接在_Imports.razor
中聲明。
// _Imports.razor @inherits DependencyInjection.StaticAccessor.Blazor.PinnedScopeComponentBase
Client端項(xiàng)目
與Server端步驟基本一致,只是引用的NuGet有所區(qū)別:
安裝NuGet
啟動項(xiàng)目引用
DependencyInjection.StaticAccessor.Blazor.WebAssembly
dotnet add package DependencyInjection.StaticAccessor.Blazor.WebAssembly
非啟動項(xiàng)目引用
DependencyInjection.StaticAccessor
dotnet add package DependencyInjection.StaticAccessor
初始化
var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.UsePinnedScopeServiceProvider(); await builder.Build().RunAsync();
頁面繼承PinnedScopeComponentBase
推薦直接在_Imports.razor
中聲明。
// _Imports.razor @inherits DependencyInjection.StaticAccessor.Blazor.PinnedScopeComponentBase
已有自定義ComponentBase基類的解決方案
你可能會使用其他包定義的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ù)會根據(jù)需要可能會加入更多類型。如有需求,也歡迎反饋和提交PR.
注意事項(xiàng)
避免通過PinnedScope直接操作IServiceScope
雖然你可以通過PinnedScope.Scope
獲取當(dāng)前的DI Scope,但最好不要通過該屬性直接操作IServiceScope
對象,比如調(diào)用Dispose方法,你應(yīng)該通過你創(chuàng)建Scope時創(chuàng)建的變量進(jìn)行操作。
不支持非通常Scope
一般日常開發(fā)時不需要關(guān)注這個問題的,通常的AspNetCore項(xiàng)目也不會出現(xiàn)這樣的場景,而Blazor就是官方項(xiàng)目類型中一個非通常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。比較常見的就是Blazor的這種情況:
我們知道,Blazor SSR通過SignalR實(shí)現(xiàn)SPA,一個SignalR連接對應(yīng)一個DI Scope,界面上的各種事件(點(diǎn)擊、獲取焦點(diǎn)等)通過SignalR通知服務(wù)端回調(diào)事件函數(shù),而這個回調(diào)便是從外部橫插一腳與SignalR進(jìn)行交互的,在不進(jìn)行特殊處理的情況下,回調(diào)事件所屬的Scope是當(dāng)前回調(diào)事件新創(chuàng)建的Scope,但我們在回調(diào)事件中與之交互的Component
是SignalR所屬Scope創(chuàng)建的,這就出現(xiàn)了Scope交叉交互的情況。PinnedScopeComponentBase
所做的便是在執(zhí)行回調(diào)函數(shù)之前,將PinnedScope.Scope
重設(shè)回SignalR對應(yīng)Scope。
肉夾饃相關(guān)應(yīng)用
正如前面所說,DependencyInjection.StaticAccessor
的核心邏輯是從肉夾饃的DI擴(kuò)展中抽離出來的,抽離后肉夾饃DI擴(kuò)展將依賴于DependencyInjection.StaticAccessor
?,F(xiàn)在你可以直接引用DependencyInjection.StaticAccessor
,然后直接通過PinnedScope.Scope
與DI進(jìn)行交互,但還是推薦通過肉夾饃DI擴(kuò)展進(jìn)行交互,DI擴(kuò)展提供了一些額外的功能,稍后將一一介紹。
DI擴(kuò)展包變化
Autofac相關(guān)包未發(fā)生重大變化,后續(xù)介紹的擴(kuò)展包都是官方DependencyInjection的相關(guān)擴(kuò)展包
本次不僅僅是一個簡單的代碼抽離,代碼的核心實(shí)現(xiàn)上也有更新,更新后移出了擴(kuò)展方法CreateResolvableScope
,直接支持官方的CreateScope
和CreateAsyncScope
方法。同時擴(kuò)展包Rougamo.Extensions.DependencyInjection.AspNetCore
和Rougamo.Extensions.DependencyInjection.GenericHost
合并為Rougamo.Extensions.DependencyInjection.Microsoft
。
Rougamo.Extensions.DependencyInjection.Microsoft
僅定義切面類型的項(xiàng)目需要引用Rougamo.Extensions.DependencyInjection.Microsoft
,啟動項(xiàng)目根據(jù)項(xiàng)目類型引用DependencyInjection.StaticAccessor
相關(guān)包即可,初始化也是僅需要完成DependencyInjection.StaticAccessor
初始化即可。
更易用的擴(kuò)展
Rougamo.Extensions.DependencyInjection.Microsoft
針對MethodContext
提供了豐富的DI擴(kuò)展方法,簡化代碼編寫。
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
提供的是一種常用場景下獲取當(dāng)前Scope的IServiceProvider
解決方案,但在千奇百怪的開發(fā)需求中,總會出現(xiàn)一些不尋常的DI Scope場景,比如前面介紹的非通常Scope,再比如Blazor。針對這種場景,肉夾饃DI擴(kuò)展雖然不能幫你獲取到正確的IServiceProvider
對象,但如果你自己能夠提供獲取方式,肉夾饃DI擴(kuò)展可以方便的集成該獲取方式。
下面以Blazor為例,雖然已經(jīng)針對Blazor特殊的DI Scope提供了通用解決方案,但Blazor還存在著自己的特殊場景。我們知道Blazor SSR服務(wù)生存期是整個SignalR的生存期,這個生存期可能非常長,一個生存期期間可能會創(chuàng)建多個頁面(ComponentBase),這多個頁面也將共享注冊為Scoped的對象,這在某些場景下可能會存在問題(比如共享EF DBContext),所以微軟提供了OwningComponentBase,它提供了更短的服務(wù)生存期,集成該類可以通過ScopedServices
屬性訪問IServiceProvider
對象。
// 1. 定義前鋒類型,針對OwningComponentBase返回ScopedServices屬性 public class OwningComponentScopeForward : SpecificPropertyFoolScopeProvider, IMethodBaseScopeForward { public override string PropertyName => "ScopedServices"; } // 2. 初始化 var builder = WebApplication.CreateBuilder(); // 初始化DependencyInjection.StaticAccessor builder.Host.UsePinnedScopeServiceProvider(); // 注冊前鋒類型 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子類方法上時,ITestService將從OwningComponentBase.ScopedServices中獲取 context.GetService<ITestService>(); } }
除了上面示例中提供的OwningComponentScopeForward
,還有根據(jù)字段名稱獲取的SpecificFieldFoolScopeProvider
,根據(jù)宿主類型通過lambda表達(dá)式獲取的TypedFoolScopeProvider<>
,這里就不一一舉例了,如果你的獲取邏輯更加復(fù)雜,可以直接實(shí)現(xiàn)先鋒類型接口IMethodBaseScopeForward
。
除了前鋒類型接口IMethodBaseScopeForward
,還提供了守門員類型接口IMethodBaseScopeGoalie
,在調(diào)用GetService
系列擴(kuò)展方法時,內(nèi)部實(shí)現(xiàn)按 [先鋒類型 -> PinnedScope.Scope.ServiceProvider -> 守門員類型 -> PinnedScope.RootServices] 的順序嘗試獲取IServiceProvider
對象。
完整示例
完整示例請?jiān)L問:https://github.com/inversionhourglass/Rougamo.DI/tree/master/samples
到此這篇關(guān)于.NET全局靜態(tài)可訪問IServiceProvider(支持Blazor)的文章就介紹到這了,更多相關(guān).NET全局靜態(tài)可訪問IServiceProvider內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Asp.net MVC利用knockoutjs實(shí)現(xiàn)登陸并記錄用戶的內(nèi)外網(wǎng)IP及所在城市(推薦)
這篇文章主要介紹了 Asp.net MVC利用knockoutjs實(shí)現(xiàn)登陸并記錄用戶的內(nèi)外網(wǎng)IP及所在城市(推薦),需要的朋友可以參考下2017-02-02Visual Studio 2017+OpenCV環(huán)境搭建教程
這篇文章主要為大家詳細(xì)介紹了Visual Studio 2017+OpenCV環(huán)境搭建教程,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03Asp.net中獲取應(yīng)用程序完整Url路徑的小例子
Asp.net中獲取應(yīng)用程序完整Url路徑的小例子,需要的朋友可以參考一下2013-06-06Asp.net Mvc 身份驗(yàn)證、異常處理、權(quán)限驗(yàn)證(攔截器)實(shí)現(xiàn)代碼
本問主要介紹asp.net的身份驗(yàn)證機(jī)制及asp.net MVC攔截器在項(xiàng)目中的運(yùn)用?,F(xiàn)在讓我們來模擬一個簡單的流程:用戶登錄》權(quán)限驗(yàn)證》異常處理2012-10-10.NET?6新特性試用之System.Text.Json功能改進(jìn)
這篇文章主要介紹了.NET?6新特性試用之System.Text.Json功能改進(jìn),2022-03-03Asp.net MVC下使用Bundle合并、壓縮js與css文件詳解
在web優(yōu)化中有一種手段,壓縮js,css文件,減少文件大小,合并js,css文件減少請求次數(shù)。asp.net mvc中為我們提供一種使用c#代碼壓縮合并js和css這類靜態(tài)文件的方法。這篇文章主要介紹了在Asp.net MVC下使用Bundle合并、壓縮js與css文件的方法,需要的朋友可以參考下。2017-03-03