写点什么

.NET 全局静态可访问 IServiceProvider(支持 Blazor)

  • 2024-09-20
    福建
  • 本文字数:5474 字

    阅读完需:约 18 分钟

前言


如何在静态方法中访问 DI 容器长期以来一直都是一个令人苦恼的问题,特别是对于热爱编写扩展方法的朋友。之所以会为这个问题苦恼,是因为一个特殊的服务生存期——范围内(Scoped),所谓的 Scoped 就是范围内单例,最常见的 WebAPI/MVC 中一个请求对应一个范围,所有注册为 Scoped 的对象在同一个请求中是单例的。如果仅仅用一个静态字段存储应用启动时创建出的IServiceProvider对象,那么在一个请求中通过该字段是无法正确获取当前请求中创建的 Scoped 对象的。


在早些时候有针对肉夹馍(Rougamo)访问 DI 容器发布了一些列NuGet,由于肉夹馍不仅能应用到实例方法上还能够应用到静态方法上,所以肉夹馍访问 DI 容器的根本问题就是如何在静态方法中访问 DI 容器。考虑到静态方法访问 DI 容器是一个常见的公共问题,所以现在将核心逻辑抽离成一系列单独的 NuGet 包,方便不使用肉夹馍的朋友使用。


快速开始


启动项目引用DependencyInjection.StaticAccessor.Hosting


dotnet add package DependencyInjection.StaticAccessor.Hosting


非启动项目引用DependencyInjection.StaticAccessor


dotnet add package DependencyInjection.StaticAccessor


// 1. 初始化。这里用通用主机进行演示,其他类型项目后面将分别举例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>(); }}
复制代码


如上示例,通过静态属性PinnedScope.ScopedServices即可获取当前 Scope 的IServiceProvider对象,如果当前不在任何一个 Scope 中时,该属性返回根IServiceProvider


版本说明


由于DependencyInjection.StaticAccessor的实现包含了通过反射访问微软官方包非 public 成员,官方的内部实现随着版本的迭代也在不断地变化,所以针对官方包不同版本发布了对应的版本。DependencyInjection.StaticAccessor的所有 NuGet 包都采用语义版本号格式(SemVer),其中主版本号与Microsoft.Extensions.*相同,次版本号为功能发布版本号,修订号为 BUG 修复及微小改动版本号。请各位在安装 NuGet 包时选择与自己引用的Microsoft.Extensions.*主版本号相同的最新版本。


另外需要说明的是,由于我本地创建 blazor 项目时只能选择.NET8.0,所以 blazor 相关包仅提供了 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 对应一个 SignalR 连接。针对 Blazor 的这种特殊的 Scope 场景,除了初始化操作,还需要一些额外操作。


我们知道,Blazor 项目在创建时可以选择交互渲染模式,除了 Server 模式外,其他的模式都会创建两个项目,多出来的这个项目的名称以.Client结尾。这里我称.Client项目为 Client 端项目,另一个项目为 Server 端项目(Server 模式下唯一的那个项目也称为 Server 端项目)。


Server 端项目


1、安装 NuGet


启动项目引用DependencyInjection.StaticAccessor.Blazor


dotnet add package DependencyInjection.StaticAccessor.Blazor


非启动项目引用DependencyInjection.StaticAccessor


dotnet add package DependencyInjection.StaticAccessor


2、初始化


var builder = WebApplication.CreateBuilder();
builder.Host.UsePinnedScopeServiceProvider(); // 唯一初始化步骤
var app = builder.Build();
app.Run();
复制代码


3、页面继承PinnedScopeComponentBase


推荐直接在_Imports.razor中声明。


// _Imports.razor
@inherits DependencyInjection.StaticAccessor.Blazor.PinnedScopeComponentBase
复制代码


Client 端项目


与 Server 端步骤基本一致,只是引用的 NuGet 有所区别:


1、安装 NuGet


启动项目引用DependencyInjection.StaticAccessor.Blazor.WebAssembly


dotnet add package DependencyInjection.StaticAccessor.Blazor.WebAssembly


非启动项目引用DependencyInjection.StaticAccessor


dotnet add package DependencyInjection.StaticAccessor


2、初始化


var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.UsePinnedScopeServiceProvider();
await builder.Build().RunAsync();
复制代码


3、页面继承PinnedScopeComponentBase


推荐直接在_Imports.razor中声明。


// _Imports.razor
@inherits DependencyInjection.StaticAccessor.Blazor.PinnedScopeComponentBase
复制代码


已有自定义 ComponentBase 基类的解决方案


你可能会使用其他包定义的ComponentBase基类,由于 C#不支持多继承,所以这里提供了不继承PinnedScopeComponentBase的解决方案。


// 假设你现在使用的ComponentBase基类是ThirdPartyComponentBase
// 定义新的基类继承ThirdPartyComponentBasepublic 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,还提供了PinnedScopeOwningComponentBasePinnedScopeLayoutComponentBase,后续会根据需要可能会加入更多类型。如有需求,也欢迎反馈和提交 PR.


注意事项


避免通过 PinnedScope 直接操作 IServiceScope


虽然你可以通过PinnedScope.Scope获取当前的 DI Scope,但最好不要通过该属性直接操作IServiceScope对象,比如调用 Dispose 方法,你应该通过你创建 Scope 时创建的变量进行操作。


不支持非通常 Scope


一般日常开发时不需要关注这个问题的,通常的 AspNetCore 项目也不会出现这样的场景,而 Blazor 就是官方项目类型中一个非通常 DI Scope 的案例。


在解释什么是非通常 Scope 前,我先聊聊通常的 Scope 模式。我们知道 DI Scope 是可以嵌套的,在通常情况下,嵌套的 Scope 呈现的是一种栈的结构,后创建的 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 了,只要是不按照这种井然有序的栈结构的,那就是非通常 Scope。比较常见的就是 Blazor 的这种情况:


我们知道,Blazor SSR 通过 SignalR 实现 SPA,一个 SignalR 连接对应一个 DI Scope,界面上的各种事件(点击、获取焦点等)通过 SignalR 通知服务端回调事件函数,而这个回调便是从外部横插一脚与 SignalR 进行交互的,在不进行特殊处理的情况下,回调事件所属的 Scope 是当前回调事件新创建的 Scope,但我们在回调事件中与之交互的Component是 SignalR 所属 Scope 创建的,这就出现了 Scope 交叉交互的情况。PinnedScopeComponentBase所做的便是在执行回调函数之前,将PinnedScope.Scope重设回 SignalR 对应 Scope。


肉夹馍相关应用


正如前面所说,DependencyInjection.StaticAccessor的核心逻辑是从肉夹馍的 DI 扩展中抽离出来的,抽离后肉夹馍 DI 扩展将依赖于DependencyInjection.StaticAccessor。现在你可以直接引用DependencyInjection.StaticAccessor,然后直接通过PinnedScope.Scope与 DI 进行交互,但还是推荐通过肉夹馍 DI 扩展进行交互,DI 扩展提供了一些额外的功能,稍后将一一介绍。


DI 扩展包变化


Autofac 相关包未发生重大变化,后续介绍的扩展包都是官方 DependencyInjection 的相关扩展包


本次不仅仅是一个简单的代码抽离,代码的核心实现上也有更新,更新后移出了扩展方法CreateResolvableScope,直接支持官方的CreateScopeCreateAsyncScope方法。同时扩展包Rougamo.Extensions.DependencyInjection.AspNetCoreRougamo.Extensions.DependencyInjection.GenericHost合并为Rougamo.Extensions.DependencyInjection.Microsoft


Rougamo.Extensions.DependencyInjection.Microsoft


仅定义切面类型的项目需要引用Rougamo.Extensions.DependencyInjection.Microsoft,启动项目根据项目类型引用DependencyInjection.StaticAccessor相关包即可,初始化也是仅需要完成DependencyInjection.StaticAccessor初始化即可。


更易用的扩展


Rougamo.Extensions.DependencyInjection.Microsoft针对MethodContext提供了丰富的 DI 扩展方法,简化代码编写。


public class TestAttribute : AsyncMoAttribute{    public override ValueTask OnEntryAsync(MethodContext context)    {        context.GetService<ITestService>();        context.GetRequiredService(typeof(ITestService));        context.GetServices<ITestService>();    }}
复制代码


从当前宿主类型实例中获取 IServiceProvider


DependencyInjection.StaticAccessor提供的是一种常用场景下获取当前 Scope 的IServiceProvider解决方案,但在千奇百怪的开发需求中,总会出现一些不寻常的 DI Scope 场景,比如前面介绍的非通常Scope,再比如 Blazor。针对这种场景,肉夹馍 DI 扩展虽然不能帮你获取到正确的IServiceProvider对象,但如果你自己能够提供获取方式,肉夹馍 DI 扩展可以方便的集成该获取方式。


下面以 Blazor 为例,虽然已经针对 Blazor 特殊的 DI Scope 提供了通用解决方案,但 Blazor 还存在着自己的特殊场景。我们知道 Blazor SSR 服务生存期是整个 SignalR 的生存期,这个生存期可能非常长,一个生存期期间可能会创建多个页面(ComponentBase),这多个页面也将共享注册为 Scoped 的对象,这在某些场景下可能会存在问题(比如共享 EF DBContext),所以微软提供了OwningComponentBase,它提供了更短的服务生存期,集成该类可以通过ScopedServices属性访问IServiceProvider对象。


// 1. 定义前锋类型,针对OwningComponentBase返回ScopedServices属性public class OwningComponentScopeForward : SpecificPropertyFoolScopeProvider, IMethodBaseScopeForward{    public override string PropertyName => "ScopedServices";}
// 2. 初始化var builder = WebApplication.CreateBuilder();
// 初始化DependencyInjection.StaticAccessorbuilder.Host.UsePinnedScopeServiceProvider();
// 注册前锋类型builder.Services.AddMethodBaseScopeForward<OwningComponentScopeForward>();
var app = builder.Build();
app.Run();
// 3. 使用public class TestAttribute : AsyncMoAttribute{ public override ValueTask OnEntryAsync(MethodContext context) { // 当TestAttribute应用到OwningComponentBase子类方法上时,ITestService将从OwningComponentBase.ScopedServices中获取 context.GetService<ITestService>(); }}
复制代码


除了上面示例中提供的OwningComponentScopeForward,还有根据字段名称获取的SpecificFieldFoolScopeProvider,根据宿主类型通过 lambda 表达式获取的TypedFoolScopeProvider<>,这里就不一一举例了,如果你的获取逻辑更加复杂,可以直接实现先锋类型接口IMethodBaseScopeForward


除了前锋类型接口IMethodBaseScopeForward,还提供了守门员类型接口IMethodBaseScopeGoalie,在调用GetService系列扩展方法时,内部实现按 [先锋类型 -> PinnedScope.Scope.ServiceProvider -> 守门员类型 -> PinnedScope.RootServices] 的顺序尝试获取IServiceProvider对象。


文章转载自:nigture

原文链接:https://www.cnblogs.com/nigture/p/18412772

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
.NET全局静态可访问IServiceProvider(支持Blazor)_.net_快乐非自愿限量之名_InfoQ写作社区