编辑
2023-08-10
我当开发
00

目录

1. 环境准备 还原错误
2. 第一次尝试
3. 第二次尝试
4. 第三次尝试

开发时遇到了这个情况,我想在两个业务类中传输数据,即类1定义了日志的实现,类2调用,所以我想将日志实现作为一个依赖注入,使用 Scoped 在本次提交时生效,然而我的业务代码是 Singleton 注入 services.AddSingleton ,当业务类直接通过构造引用区域依赖 Scoped 时会报错,稍微测试了一下,这里记录总结;

1. 环境准备 还原错误

1.1 新建 一个消息类 ScopeInfo , 两个业务类 Class1Class2

namespace ScopeTest { public class ScopeInfo { public int Count { get; set; } = 0; } }
namespace ScopeTest { public class Class1 { private ScopeInfo info; public Class1(ScopeInfo info) { this.info = info; } public Class1 SetValue() { this.info.Count++; Console.WriteLine($"{nameof(Class1)}:{info?.Count}"); return this; } } }
namespace ScopeTest { public class Class2 { private ScopeInfo info; public Class2(ScopeInfo info) { this.info = info; } public Class2 GetValue() { Console.WriteLine($"{nameof(Class2)}:{info?.Count}"); return this; } } }

1.2 注册注入

builder.Services.AddSingleton<Class1>(); builder.Services.AddSingleton<Class2>(); builder.Services.AddScoped<ScopeInfo>();

1.3 在方法中使用

public WeatherForecastController(Class1 class1, Class2 class2) { this.class1 = class1; this.class2 = class2; } [HttpGet(Name = "GetWeatherForecast")] public IEnumerable<WeatherForecast> Get() { class1.SetValue(); class2.GetValue(); ...略 }

1.4 运行报错 运行后不能启动,报错 ❗️

System.AggregateException:“Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: ScopeTest.Class1 Lifetime: Singleton ImplementationType: ScopeTest.Class1': Cannot consume scoped service 'ScopeTest.ScopeInfo' from singleton 'ScopeTest.Class1'.) (Error while validating the service descriptor 'ServiceType: ScopeTest.Class2 Lifetime: Singleton ImplementationType: ScopeTest.Class2': Cannot consume scoped service 'ScopeTest.ScopeInfo' from singleton 'ScopeTest.Class2'.)”

翻译过来就一句话 无法从单例 "ScopeTest.Class1 "消费作用域服务 "ScopeTest.ScopeInfo"

因为 Class1Class2 都是全局单例的,所以一开始就会实例,而 ScopeInfo 提交时才实例,所以拿不到;

2. 第一次尝试

聪明的小伙伴😑已经想到了,可以从 IServiceProvider 中获取注入,而不是构造,将 Class1Class2 改造

namespace ScopeTest { public class Class1 { private ScopeInfo info; private readonly IServiceProvider applicationServices; public Class1(IServiceProvider applicationServices) { //第一次尝试 this.applicationServices = applicationServices; //从IServiceProvider 中拿 info = applicationServices.GetService<ScopeInfo>(); } public Class1 SetValue() { this.info.Count++; Console.WriteLine($"{nameof(Class1)}:{info?.Count}"); return this; } } }
namespace ScopeTest { public class Class2 { private ScopeInfo info; private readonly IServiceProvider applicationServices; public Class2(IServiceProvider applicationServices) { //第一次尝试 this.applicationServices = applicationServices; //从IServiceProvider 中拿 info = applicationServices.GetService<ScopeInfo>(); } public Class2 GetValue() { Console.WriteLine($"{nameof(Class2)}:{info?.Count}"); return this; } } }

这次可以运行启动了,我们来触发一次访问, 报错了 ❗️

System.InvalidOperationException:“Cannot resolve scoped service 'ScopeTest.ScopeInfo' from root provider.”

翻译过来 无法从根提供程序解析作用域服务 "ScopeTest.ScopeInfo"

2.1 这里需要创建区域 使用 CreateScope() 可以解决这个问题,写成这样

info = applicationServices.CreateScope().ServiceProvider.GetService<ScopeInfo>();

到这提交终于不报错了 ,输出如下

Class1:1 Class2:0

很明显 Class1 的赋值 没有传入到 Class2 中🤔,因为在构造时区域是 Create 出来的,每次 CreateScope() 都是新的区域,所以 Class1Class2 是两个实例

3. 第二次尝试

这时我又想使用第三个类来 CreateScope 并且赋值到一个变量,这样区域就一个了,但是这个中间类 Middle 其实和 ScopeInfo 类的生命周期是一样的,如果使用 AddSingleton 会全局实例化一次,当第二次提交时不实例化,ScopeInfo 会保留上次的值,这样不行;如果使用 AddScoped 注入,和 ScopeInfo 一样 Class1Class2 还是无法获取到区域实例;

4. 第三次尝试

再来捋一下需求,我希望 类ScopeInfo 在请求开始实例化一次,之后的所有类中都是这个实例,请求结束销毁,这个类是个什么呢;其实就是当前的上下文 HttpContext ,而且 HttpContext 本来也专门干这个事 HttpContext.Items😲

我已经使用 AddScoped<ScopeInfo>(); 了,所以我肯定不会再手动 HttpContext.Items["ScopeInfo"] =new ScopeInfo(); 放一次,也不会出现 new ScopeInfo() 的写法,所以这里放进去的不应该是 ScopeInfo ,而是当前区域,即 提交时,我将 CreateScope 放到 HttpContext.Items 中,这样,所有的类中的区域就一样了,而且提交结束后会随 HttpContext 释放;

问题来了,怎么在提交时 将 一个类 或者 参数 放到 HttpContext.Items 中呢,这样写

//这里需要注入 IHttpContextAccessor builder.Services.AddHttpContextAccessor(); app.Use(async (context, next) => { context.Items["SharedScope"] = app.Services.CreateScope(); await next(); });

Class1Class2 改造

namespace ScopeTest { public class Class1 { //不要在构造柱中实例了,直接拿 private ScopeInfo info => (httpContext.HttpContext.Items["SharedScope"] as IServiceScope).ServiceProvider.GetService<ScopeInfo>(); //private readonly IServiceProvider applicationServices; //当前上线文 private readonly IHttpContextAccessor httpContext; public Class1(IServiceProvider applicationServices, IHttpContextAccessor httpContext) { //第一次尝试 //this.applicationServices = applicationServices; //info = applicationServices.GetService<ScopeInfo>(); this.httpContext = httpContext; } public Class1 SetValue() { this.info.Count++; Console.WriteLine($"{nameof(Class1)}:{info?.Count}"); return this; } } }
namespace ScopeTest { public class Class2 { //不要在构造柱中实例了,直接拿 private ScopeInfo info => (httpContext.HttpContext.Items["SharedScope"] as IServiceScope).ServiceProvider.GetService<ScopeInfo>(); //private readonly IServiceProvider applicationServices; //当前上线文 private readonly IHttpContextAccessor httpContext; public Class2(IServiceProvider applicationServices, IHttpContextAccessor httpContext) { //第一次尝试 //this.applicationServices = applicationServices; //info = applicationServices.GetService<ScopeInfo>(); this.httpContext = httpContext; } public Class2 GetValue() { Console.WriteLine($"{nameof(Class2)}:{info?.Count}"); Console.WriteLine($"================================"); return this; } } }

这时 再次提交测试 输出如下

Class1:1 Class2:1 ================================ Class1:1 Class2:1 ================================

可以看到值被共享了,而且两次提交 值被重置,😬

4.1 封装 HttpContext.Items 很难看,如果是强类型,可以使用 HttpContext.Features.Set ;创建一个类 SharedScopeHttpContext 的扩展方法

using Microsoft.AspNetCore.Mvc.RazorPages; namespace ScopeTestWebAPP.ScopeTest { public class SharedScope { public IServiceScope? sharedScope; public SharedScope(IApplicationBuilder app) { sharedScope = app?.ApplicationServices?.CreateScope(); } } public static class HttpContextExtensions { /// <summary> /// 得到服务从上下文共享的Scope中, /// </summary> /// <typeparam name="T"></typeparam> /// <returns></returns> public static T GetServerScope<T>(this HttpContext context) { var share = context.Features.Get<SharedScope>(); return share.sharedScope.ServiceProvider.GetService<T>(); } } }

在注册时可以这样写

app.Use(async (context, next) => { context.Features.Set(new SharedScope(app)); await next(); });

使用时可以这样

using ScopeTestWebAPP.ScopeTest; namespace ScopeTest { public class Class1 { private ScopeInfo info => httpContext.HttpContext.GetServerScope<ScopeInfo>(); //当前上线文 private readonly IHttpContextAccessor httpContext; public Class1(IHttpContextAccessor httpContext) { this.httpContext = httpContext; } } }

IHttpContextAccessor 也不想写,可以写到静态类里,其实项目中用是这样的

namespace ScopeTest { public class Class1 { private ScopeInfo info => FineUICore.PageContext.Current.GetServerScope<ScopeInfo>(); //又是FineUICore 😐 public Class1() { } } }

另外再异步应将 sharedScope 套上 AsyncLocal,这部分我倒没有测试

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:没想好

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!