.NET全局靜態(tài)可訪問IServiceProvider的過程詳解(支持Blazor)
DependencyInjection.StaticAccessor
前言
如何在靜態(tài)方法中訪問DI容器長期以來一直都是一個令人苦惱的問題,特別是對于熱愛編寫擴(kuò)展方法的朋友。之所以會為這個問題苦惱,是因為一個特殊的服務(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)用到實例方法上還能夠應(yīng)用到靜態(tài)方法上,所以肉夾饃訪問DI容器的根本問題就是如何在靜態(tài)方法中訪問DI容器。考慮到靜態(tài)方法訪問DI容器是一個常見的公共問題,所以現(xiàn)在將核心邏輯抽離成一系列單獨的NuGet包,方便不使用肉夾饃的朋友使用。
快速開始
啟動項目引用DependencyInjection.StaticAccessor.Hosting
dotnet add package DependencyInjection.StaticAccessor.Hosting
非啟動項目引用DependencyInjection.StaticAccessor
dotnet add package DependencyInjection.StaticAccessor
// 1. 初始化。這里用通用主機(jī)進(jìn)行演示,其他類型項目后面將分別舉例
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的實現(xiàn)包含了通過反射訪問微軟官方包非public成員,官方的內(nèi)部實現(xiàn)隨著版本的迭代也在不斷地變化,所以針對官方包不同版本發(fā)布了對應(yīng)的版本。DependencyInjection.StaticAccessor的所有NuGet包都采用語義版本號格式(SemVer),其中主版本號與Microsoft.Extensions.*相同,次版本號為功能發(fā)布版本號,修訂號為BUG修復(fù)及微小改動版本號。請各位在安裝NuGet包時選擇與自己引用的Microsoft.Extensions.*主版本號相同的最新版本。
另外需要說明的是,由于我本地創(chuàng)建blazor項目時只能選擇.NET8.0,所以blazor相關(guān)包僅提供了8.0版本,如果確實有低版本的需求,可以到github中提交issue。
WebAPI/MVC初始化示例
啟動項目引用DependencyInjection.StaticAccessor.Hosting
dotnet add package DependencyInjection.StaticAccessor.Hosting
非啟動項目引用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項目在創(chuàng)建時可以選擇交互渲染模式,除了Server模式外,其他的模式都會創(chuàng)建兩個項目,多出來的這個項目的名稱以.Client結(jié)尾。這里我稱.Client項目為Client端項目,另一個項目為Server端項目(Server模式下唯一的那個項目也稱為Server端項目)。
Server端項目
安裝NuGet
啟動項目引用
DependencyInjection.StaticAccessor.Blazordotnet add package DependencyInjection.StaticAccessor.Blazor
非啟動項目引用
DependencyInjection.StaticAccessordotnet 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端項目
與Server端步驟基本一致,只是引用的NuGet有所區(qū)別:
安裝NuGet
啟動項目引用
DependencyInjection.StaticAccessor.Blazor.WebAssemblydotnet add package DependencyInjection.StaticAccessor.Blazor.WebAssembly
非啟動項目引用
DependencyInjection.StaticAccessordotnet 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.
注意事項
避免通過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àn)這樣的場景,而Blazor就是官方項目類型中一個非通常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實現(xiàn)SPA,一個SignalR連接對應(yīng)一個DI Scope,界面上的各種事件(點擊、獲取焦點等)通過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。現(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ò)展包
本次不僅僅是一個簡單的代碼抽離,代碼的核心實現(xiàn)上也有更新,更新后移出了擴(kuò)展方法CreateResolvableScope,直接支持官方的CreateScope和CreateAsyncScope方法。同時擴(kuò)展包Rougamo.Extensions.DependencyInjection.AspNetCore和Rougamo.Extensions.DependencyInjection.GenericHost合并為Rougamo.Extensions.DependencyInjection.Microsoft。
Rougamo.Extensions.DependencyInjection.Microsoft
僅定義切面類型的項目需要引用Rougamo.Extensions.DependencyInjection.Microsoft,啟動項目根據(jù)項目類型引用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)前宿主類型實例中獲取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ù)雜,可以直接實現(xiàn)先鋒類型接口IMethodBaseScopeForward。
除了前鋒類型接口IMethodBaseScopeForward,還提供了守門員類型接口IMethodBaseScopeGoalie,在調(diào)用GetService系列擴(kuò)展方法時,內(nèi)部實現(xiàn)按 [先鋒類型 -> PinnedScope.Scope.ServiceProvider -> 守門員類型 -> PinnedScope.RootServices] 的順序嘗試獲取IServiceProvider對象。
完整示例
完整示例請訪問: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 SignalR來做實時Web聊天實例代碼
本篇文章主要介紹了Asp.net SignalR來做實時Web聊天實例代碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-06-06
.NET Framework集成Quartz的實現(xiàn)示例
本文主要介紹了.NET Framework集成Quartz的實現(xiàn)示例,Quartz 主要用于定時執(zhí)行任務(wù)方面,文中通過示例代碼介紹的非常詳細(xì),需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-03-03
ASP.NET web.config中 數(shù)據(jù)庫連接字符串加密解密
本文主要介紹利用aspnet_regiis.exe工具對web.config中connectionStrings節(jié)點進(jìn)行加密和解密的過程,希望對大家有所幫助。2016-05-05
asp.net core3.1cookie和jwt混合認(rèn)證授權(quán)實現(xiàn)多種身份驗證方案
身份驗證是確定用戶身份的過程。 授權(quán)是確定用戶是否有權(quán)訪問資源的過程。本文主要介紹了asp.net core3.1cookie和jwt混合認(rèn)證授權(quán)實現(xiàn)多種身份驗證方案,感興趣的可以了解一下2021-09-09

