DependencyInjection.StaticAccessor

前言

如何在静态方法中访问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

// 定义新的基类继承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,还提供了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.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)
{
// 当TestAttribute应用到OwningComponentBase子类方法上时,ITestService将从OwningComponentBase.ScopedServices中获取
context.GetService<ITestService>();
}
}

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

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

完整示例

完整示例请访问:https://github.com/inversionhourglass/Rougamo.DI/tree/master/samples

.NET全局静态可访问IServiceProvider(支持Blazor)的更多相关文章

  1. C++内存分配方式详解——堆、栈、自由存储区、全局/静态存储区和常量存储区

    栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区.里面的变量通常是局部变量.函数参数等.在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用.和堆一样 ...

  2. 【校招面试 之 C/C++】第14题 C++ 内存分配方式详解——堆、栈、自由存储区、全局/静态存储区和常量存储区(堆栈的区别)

    栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区.里面的变量通常是局部变量.函数参数等.在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用.和堆一样 ...

  3. (转)C++内存分配方式详解——堆、栈、自由存储区、全局/静态存储区和常量存储区

    程序在内存有五个存在区域: A:动态区域中的栈区  B:动态区域中的栈区 C:静态区域中:全局变量 和静态变量    (这个区域又可以进一步细分为:初始化的全局变量和静态变量    以及    未初始 ...

  4. spring mvc官网下最新jar搭建框架-静态资源访问处理-注解-自动扫描

    1.从官网下载spring相关jar http://spring.io/projects 点击SPRING FRAMEWORK

  5. C++中的局部变量、全局变量、局部静态变量、全局静态变量的区别

    局部变量(Local variables)与 全局变量: 在子程序或代码块中定义的变量称为局部变量,在程序的一开始定义的变量称为全局变量. 全局变量作用域是整个程序,局部变量作用域是定义该变量的子程序 ...

  6. Django1.7如何配置静态资源访问

    Django是非常轻量级的Web框架,今天散仙来看下如何在Django中配置静态的资源访问路径,一个中等规模的网站,可能就会有很多静态的资源需要访问,无论是html,txt,还是压缩包,有时候访问这些 ...

  7. SpringBoot静态资源访问+拦截器+Thymeleaf模板引擎实现简单登陆

    在此记录一下这十几天的学习情况,卡在模板引擎这里已经是四天了. 对Springboot的配置有一个比较深刻的认识,在此和大家分享一下初学者入门Spring Boot的注意事项,如果是初学SpringB ...

  8. c++ 堆、栈、自由存储区、全局/静态存储区和常量存储区

    在C++中,内存分成5个区,他们分别是堆.栈.自由存储区.全局/静态存储区和常量存储区. 栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清楚的变量的存储区.里面的变量通常是局部变量.函数参数 ...

  9. linux面试之--堆、栈、自由存储区、全局/静态存储区和常量存储区

    栈,就是那些由编译器在须要的时候分配,在不须要的时候自己主动清除的变量的存储区.里面的变量一般是局部变量.函数參数等.在一个进程中.位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用.和堆 ...

  10. wpf绑定全局静态变量(mvvm)

    原文 wpf绑定全局静态变量(mvvm) 在实际的开发中,有一些集合或者属性可能是全局的,比如当你做一个oa的时候,可能需要展示所有的人员,这时这个所有的人员列表显然可以作为全局参数,比如这里有一个全 ...

随机推荐

  1. 可视化—gojs 超多超实用经验分享(二)

    想了想序号还是接上一篇分享的的序号接着写,如果在本文中没有获取需要的答案,可以移步去看看上一篇的分享.gojs 超多超实用经验分享(一) 目录 22. 指定线段连接到节点的某一个特定的接口上 23. ...

  2. Day 9 - 线段树

    线段树 引入 线段树是算法竞赛中常用的用来维护 区间信息 的数据结构. 线段树可以在 \(O(\log N)\) 的时间复杂度内实现单点修改.区间修改.区间查询(区间求和,求区间最大值,求区间最小值) ...

  3. 最强AI语音克隆和文本配音工具!与真人无异,CosyVoice下载介绍

    CosyVoice是一个大规模预训练语言模型,深度融合文本理解和语音生成的一项新型语音合成技术,能够精准解析并诠释各类文本内容,将其转化为宛如真人般的自然语音 CosyVoice采用了总共超15万小时 ...

  4. TIER 1: Appointment

    TIER 1: Appointment SQL Structured Query Language 是一种用于管理关系型数据库的编程语言.它是一种标准化的语言,用于定义.操作和管理数据库中的数据. 经 ...

  5. Vue 基于VSCode结合Vetur+ESlint+Prettier统一Vue代码风格

    基于VSCode结合Vetur+ESlint+Prettier统一Vue代码风格 插件安装 安装Vetur,ESlint, Prettier - Code formatter插件 安装方法(安装ESl ...

  6. gist.github.com 无法访问解决办法,亲测永远有效!

    1.打开https://www.ipaddress.com/,输入gist.github.com获取IP地址 2.ping 此ip地址,可以访问 3.将IP地址写入Hosts文件,140.82.113 ...

  7. android常用布局基础学习

    总结:可水平放置可垂直放置也可穿插使用,默认为水平 <!--我在第一次使用权重的时候忽视了本线性布局中的宽度与高度,如果要使用权重,请将线性布局的最初大小设置为match_parent,否则不会 ...

  8. FFmpeg开发笔记(四十二)使用ZLMediaKit开启SRT视频直播服务

    ​<FFmpeg开发实战:从零基础到短视频上线>一书在第10章介绍了轻量级流媒体服务器MediaMTX,通过该工具可以测试RTSP/RTMP等流媒体协议的推拉流.不过MediaMTX的功能 ...

  9. Qt+OpenCascade开发笔记(二):windows开发环境搭建(二):Qt引入occ库,搭建基础工程模板Demo和发布Demo

    前言   Open CASCADE是由Open Cascade SAS公司开发和支持的开源软件开发平台,旨在为特定领域快速开发程序而设计.它是一个面向对象的C++类库,提供了丰富的几何造型.数据交换和 ...

  10. 【POI】Excel数据导入

    Postman请求方式: Controller接口代码: /** * /partImport/part/importUpload * @param importFile * @return */ @P ...