开发时遇到了这个情况,我想在两个业务类中传输数据,即类1定义了日志的实现,类2调用,所以我想将日志实现作为一个依赖注入,使用 Scoped 在本次提交时生效,然而我的业务代码是 Singleton 注入 services.AddSingleton
,当业务类直接通过构造引用区域依赖 Scoped 时会报错,稍微测试了一下,这里记录总结;
1. 环境准备 还原错误
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
提交时才实例,所以拿不到;
2. 第一次尝试
聪明的小伙伴😑已经想到了,可以从 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
是两个实例
3. 第二次尝试
这时我又想使用第三个类来 CreateScope
并且赋值到一个变量,这样区域就一个了,但是这个中间类 Middle
其实和 ScopeInfo
类的生命周期是一样的,如果使用 AddSingleton
会全局实例化一次,当第二次提交时不实例化,ScopeInfo
会保留上次的值,这样不行;如果使用 AddScoped
注入,和 ScopeInfo
一样 Class1
和 Class2
还是无法获取到区域实例;
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();
});
将 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
,这部分我倒没有测试