开发时遇到了这个情况,我想在两个业务类中传输数据,即类1定义了日志的实现,类2调用,所以我想将日志实现作为一个依赖注入,使用 Scoped 在本次提交时生效,然而我的业务代码是 Singleton 注入 services.AddSingleton
,当业务类直接通过构造引用区域依赖 Scoped 时会报错,稍微测试了一下,这里记录总结;
1.1 新建 一个消息类 ScopeInfo
, 两个业务类 Class1
和 Class2
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"
因为 Class1
和 Class2
都是全局单例的,所以一开始就会实例,而 ScopeInfo
提交时才实例,所以拿不到;
聪明的小伙伴😑已经想到了,可以从 IServiceProvider
中获取注入,而不是构造,将 Class1
和 Class2
改造
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()
都是新的区域,所以 Class1
和 Class2
是两个实例
这时我又想使用第三个类来 CreateScope
并且赋值到一个变量,这样区域就一个了,但是这个中间类 Middle
其实和 ScopeInfo
类的生命周期是一样的,如果使用 AddSingleton
会全局实例化一次,当第二次提交时不实例化,ScopeInfo
会保留上次的值,这样不行;如果使用 AddScoped
注入,和 ScopeInfo
一样 Class1
和 Class2
还是无法获取到区域实例;
再来捋一下需求,我希望 类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(); });
将 Class1
和 Class2
改造
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
;创建一个类 SharedScope
和 HttpContext
的扩展方法
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
,这部分我倒没有测试
本文作者:没想好
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!