开发时遇到了这个情况,我想在两个业务类中传输数据,即类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,这部分我倒没有测试