.NET 全局静态可访问 IServiceProvider(支持 Blazor)
前言
如何在静态方法中访问 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
如上示例,通过静态属性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
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、初始化
3、页面继承PinnedScopeComponentBase
推荐直接在_Imports.razor
中声明。
Client 端项目
与 Server 端步骤基本一致,只是引用的 NuGet 有所区别:
1、安装 NuGet
启动项目引用DependencyInjection.StaticAccessor.Blazor.WebAssembly
dotnet add package DependencyInjection.StaticAccessor.Blazor.WebAssembly
非启动项目引用DependencyInjection.StaticAccessor
dotnet add package DependencyInjection.StaticAccessor
2、初始化
3、页面继承PinnedScopeComponentBase
推荐直接在_Imports.razor
中声明。
已有自定义 ComponentBase 基类的解决方案
你可能会使用其他包定义的ComponentBase
基类,由于 C#不支持多继承,所以这里提供了不继承PinnedScopeComponentBase
的解决方案。
其他 ComponentBase 基类
除了PinnedScopeComponentBase
,还提供了PinnedScopeOwningComponentBase
和PinnedScopeLayoutComponentBase
,后续会根据需要可能会加入更多类型。如有需求,也欢迎反馈和提交 PR.
注意事项
避免通过 PinnedScope 直接操作 IServiceScope
虽然你可以通过PinnedScope.Scope
获取当前的 DI Scope,但最好不要通过该属性直接操作IServiceScope
对象,比如调用 Dispose 方法,你应该通过你创建 Scope 时创建的变量进行操作。
不支持非通常 Scope
一般日常开发时不需要关注这个问题的,通常的 AspNetCore 项目也不会出现这样的场景,而 Blazor 就是官方项目类型中一个非通常 DI Scope 的案例。
在解释什么是非通常 Scope 前,我先聊聊通常的 Scope 模式。我们知道 DI Scope 是可以嵌套的,在通常情况下,嵌套的 Scope 呈现的是一种栈的结构,后创建的 scope 先释放,井然有序。
了解了通常 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
,直接支持官方的CreateScope
和CreateAsyncScope
方法。同时扩展包Rougamo.Extensions.DependencyInjection.AspNetCore
和Rougamo.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 扩展方法,简化代码编写。
从当前宿主类型实例中获取 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
对象。
除了上面示例中提供的OwningComponentScopeForward
,还有根据字段名称获取的SpecificFieldFoolScopeProvider
,根据宿主类型通过 lambda 表达式获取的TypedFoolScopeProvider<>
,这里就不一一举例了,如果你的获取逻辑更加复杂,可以直接实现先锋类型接口IMethodBaseScopeForward
。
除了前锋类型接口IMethodBaseScopeForward
,还提供了守门员类型接口IMethodBaseScopeGoalie
,在调用GetService
系列扩展方法时,内部实现按 [先锋类型 -> PinnedScope.Scope.ServiceProvider -> 守门员类型 -> PinnedScope.RootServices] 的顺序尝试获取IServiceProvider
对象。
文章转载自:nigture
评论