在后台服务中使用 Scoped 服务,为什么总是报错?1.前言
在使用framwork时构造对象都是直接new本身框架没有提供DI的能力当然也可以集成Autofac 因为Autofac 提供了完善的适配器可以无缝集成到各类项目中可以看出微软选择不是“修补旧船”而是“建造新航母”随着 .netcore 的发展依赖注入也是被作为.netcore 平台框架的基础核心功能之一。带来这个有好处最大的亮点就是依赖倒置容器帮你管理对象的生命周期在搭建框架时配置下你完全不需要操心只需要在构造函数写入就行了。internal class SingletonService : ISingletonService { public ITransientService Transient { get; } public Guid SingletonId { get; } Guid.NewGuid(); public SingletonService(ITransientService transient) { Transient transient; } }相信有大部分小伙伴也是实际应用过的只要按照项目内人家写的代码风格一样往构造函数里放就完事了其他的完全不用操心框架也是尽可能这么做的让你省心但是您是否仔细思考一下您真的对他了解吗因为新的技术意味着带来新的挑战要学习新的东西不然的话1.假设哪天启动时突然构造函数不能注入了搞半天不知道怎么回事2.人家问你单例服务注入瞬时会不会报错如果你没了解过或者实际操作过的话只能靠懵我就遇到过不过我不是靠懵我是斩钉截铁的说会!因为慢一秒都代表心虚底气来源就是他的枪里没有子弹,赌他可能也不知道接下来就聊聊一些需要注意的点如果您知道当看个热闹如果不知道就当做扩展知识了2. 三种服务生命周期的定义与行为Transient瞬时每次从容器请求时都创建新实例就跟每次直接new一样。Scoped作用域在同一个 IServiceScope 内是单例不同作用域之间实例不同一个Scope对应一个逻辑上下文如一次 HTTP 请求、一次后台任务同一作用域内Scoped:服务是单例。Transient:服务每次解析都是新实例但可共享同一个 Scoped 实例就是你有2个不同的Transient引用了一个scope的类在同一作用域2次解析的scope类都会是同一对象 。Singleton整个应用程序生命周期内只有一个实例就例如你定义的静态类。var services new ServiceCollection(); // 注入瞬时 services.AddTransientITransientService, TransientService(); //注入单例 services.AddSingletonISingletonService, SingletonService(); //注入作用域 services.AddSingletonIScopedService, ScopedService(); //构造服务对象 var serviceProvider services.BuildServiceProvider();3.生命周期依赖的规则这里说的就是你不同生命周期的类如果要相互引用需要参照并且按照以下的规则依赖方的生命周期不能长于其依赖的服务就是说你使用的类如果依赖其他类那么一定不能存在当前类还存在依赖的服务已经死了的情况。短生命周期 → 依赖 → 长生命周期Transient 依赖 Singleton 就可以长生命周期 → 依赖 → 短生命周期Singleton 依赖 Scoped 或 Transient有状态时单例是不能依赖作用域的依赖瞬时不会报错但是瞬时推荐要无状态的类消费者类型 \ 依赖类型TransientScopedSingletonTransient允许必须要在作用域内允许Scoped允许允许允许Singleton需要注意依赖不能长于自身不允许依赖 Scoped允许我们注意看这个表格中的关系后面依赖这个展开当然具体就是围绕3个特殊的来分析因为容易出错1.瞬时周期类依赖作用域周期类2.单例周期类依赖瞬时类3.单例周期类依赖作用域周期类4.瞬时周期类依赖作用域周期类控制台或者后台服务应用1.根容器IServiceProvider不能解析 Scoped 服务provider根容器来获取注册为作用域周期的类就直接报错了2.不能直接注入注册为作用域周期的类例如我在瞬时的对象中注入一个scoped周期的类internal class TransientService2 : ITransientService { public Guid Id { get; } Guid.NewGuid(); public IScopedService Scoped { get; } public TransientService2(IScopedService scoped) { Scoped scoped; Console.WriteLine($[Transient] 构造ID: {Id}持有 Scoped: {scoped.Id}); } }他就会出现如下错误WebApi 应用如果你善于观察你会发现一个有趣的现象这样的代码如果放在api的控制器中注入我这里分别注入依赖了作用域的瞬时直接注入作用域注入根容器通过根容器获取作用域服务他竟然全都不会报错。结论1.在控制台或者后台服务报错的原因是因为作用域服务不能在根容器解析这个如何理解呢因为作用域服务的生命周期是绑定在逻辑作用域如一个 HTTP 请求上的每个逻辑操作单元一个实例实例应该在该作用域内创建、共享、并在作用域结束时被释放。而根容器Root ServiceProvider是应用程序生命周期级别的它没有作用域的逻辑概念如果直接从根容器解析作用域服务的话就会1.作用域服务变成“伪单例”在整个应用程序生命周期内复用而不是每次请求一个新实例或作用域内共享如果是DbContext那么同一个DbContext实例被多个 HTTP 请求、多个线程共享虽然数据库连接本身可能被复用但DbContext持有连接状态长时间不释放 → 连接无法归还池 → 新请求无法获取连接 → 超时2.因为没有真正的作用域上下文破坏作用域的生命周期管理不能正确触发作用域服务的释放和 Dispose()可能造成内存泄漏或数据混乱。那如何解决这个问题呢答案就是我们需要通过作用域解析 Scoped 服务var services new ServiceCollection(); services.AddScopedIScopedService, ScopedService(); services.AddTransientITransientService, TransientService2(); var provider services.BuildServiceProvider(validateScopes: true); // 通过作用域解析 Scoped 服务和依赖了 Scoped 的 Transient 服务 using (var scopeed provider.CreateScope()) { var scopedProvider scopeed.ServiceProvider; var scopedService scopedProvider.GetRequiredServiceITransientService(); }可以发现scopedProvider和provider都是IServiceProvider但它们代表的是两个不同层级的 DI 容器实例生命周期和解析行为完全不同。根IServiceProvider是整个应用程序级别的。而scopedProvider是当前using块范围内区分他们很简单IServiceProvider有一个IsScope属性如果细心调试下会发现通过scopeed 创建的IServiceProvider的IsScope为true,根容器IServiceProvider的IsScope为false所以得出结论如果在后台服务线程或者控制台中要解析作用域周期对象或者解析依赖了作用域周期的对象必须使用scopeed 来创建一个根容器来获取。如果是在web中就不用管因为微软已经在web框架中的RequestServicesFeature帮我们实现了将IServiceProvider设为请求级别的 Scoped 根容器在注入时可以发现它的IsScope为true