漫谈.net core和Autofac中的Scoped生命周期
我们知道,.net core内置了IOC容器,通常,一个服务的生命周期有三种:Transient、Scoped、Singleton
Transient:临时性的服务,当进行服务注入时,每次都是重新创建一个新的对象实例
Scoped:范围性的服务,当在一个范围内进行服务注入时,保证使用同一个实例对象(可以理解为一个IServiceProvider中只有一个实例对象,不同的IServiceProvider中是不同的实例对象)
Singleton:单例的服务,就是在整个生命周期内,只有一个实例对象(可以理解为所有的IServiceProvider中共享一个实例对象)
今天我们聊聊Scoped作用域这个生命周期。
首先,如何理解Scoped作用域?就是一个范围,还记的using的用法么?你应该在using的范围内使用相应的变量吧,超出了可能就会抛出一样,Scoped就是表示这么一个范围,所以它是一个IDisposable接口对象。举几个例子,比如我们一个请求的处理过程就是在一个Scoped作用域里面,比如我们常使用的EFCore的DbContext往往就是放在一个Scoped作用域里面。Scoped作用域就是保证在一个范围内始终保持唯一的实例,如果我们需要一个新的实例,那么我们就需要创建一个新的Scoped作用域。
.net core中的实现
既然这样,那么我们怎么区分两个作用域呢?或者换句话说,这个作用域多大呢?这就是需要一个标识,在.net core中,它是Microsoft.Extensions.DependencyInjection.IServiceScope
接口,它的实现类是Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope
,它是一个内部类,我们先看它大概得样子是这样的(砍掉了一些):
internal sealed class ServiceProviderEngineScope : IServiceScope, IServiceProvider, IAsyncDisposable, IServiceScopeFactory
{
private bool _disposed;
public ServiceProviderEngineScope(ServiceProvider provider, bool isRootScope)
{
ResolvedServices = new Dictionary<ServiceCacheKey, object>();
RootProvider = provider;
IsRootScope = isRootScope;
}
internal Dictionary<ServiceCacheKey, object> ResolvedServices { get; }
public bool IsRootScope { get; }
internal ServiceProvider RootProvider { get; }
public object GetService(Type serviceType)
{
if (_disposed)
{
ThrowHelper.ThrowObjectDisposedException();
}
return RootProvider.GetService(serviceType, this);
}
public IServiceProvider ServiceProvider => this;
public IServiceScope CreateScope() => RootProvider.CreateScope();
}
可以看到,ServiceProviderEngineScope
实现了IServiceScope, IServiceProvider, IAsyncDisposable, IServiceScopeFactory
几个接口(一个类四用,这个作者偷懒了,呵呵):
IServiceScope,表示他是一个Scoped范围
IServiceProvider,表示它是一个Scoped范围的容器接口
IAsyncDisposable,表示他是一个异步释放的对象
IServiceScopeFactory,表示他是一个Scoped范围工厂
我们现在梳理一下Scoped作用域的使用流程,首先我们从容器中得到IServiceScopeFactory
的实现(其实就是ServiceProviderEngineScope
,而且是Singleton
),然后我们通过IServiceScopeFactory
的CreateScope
方法创建一个IServiceScope
作用域对象(其实也是ServiceProviderEngineScope
),接着调用IServiceScope
作用域的ServiceProvider
属性得到一个新容器(其实就是IServiceScope
作用域自身,也就是ServiceProviderEngineScope
),最后当我们调用ServiceProvider
属性的GetService
方法时,其实就是调用IServiceScope
的GetService
方法(也就是ServiceProviderEngineScope
的GetService
方法),最终由RootProvider
根容器调用它自己的GetService
方法来创建实例,注意,这个时候它不仅传了类型,还传入了当前IServiceScope
对象作为区分不同Scoped作用域的一个标识,ServiceProviderEngineScope
构造函数中传进来的的ServiceProvider
,其实就是根容器,也就是说,实际上所有的服务最终都是由根容器创建的。
在实际开发中,我们可以通过几种方式来创建一个IServiceScope
的作用域,但最终都是通过IServiceScopeFactory
接口来创建作用域的,比如通过IServiceProvider
的CreateScope
拓展方法:
public static IServiceScope CreateScope(this IServiceProvider provider)
{
return provider.GetRequiredService<IServiceScopeFactory>().CreateScope();
}
public static AsyncServiceScope CreateAsyncScope(this IServiceProvider provider)
{
return new AsyncServiceScope(provider.CreateScope());
}
CreateAsyncScope
创建了一个异步的作用域,允许我们在代码中使用异步的方式来释放,但是最终,无论是CreateScope
还是CreateAsyncScope
,最终都是通过IServiceScopeFactory
接口来创建作用域的。
注意:这个时候,你可能会想,通过CreateScope
拓展方法创建作用域于IServiceScopeFactory
接口来创建作用域是等价的,但是其实不然,这个等价是有前提的,因为IServiceScopeFactory
是一个单例,因此它每次都能创建一个新的作用域,但是使用CreateScope
拓展方法创建的作用域会优先获取IServiceScopeFactory
接口实现,再来使用IServiceScopeFactory
创建新的作用域,这就有问题了,在ServiceProviderEngineScope
源码中可以看到,如果当前的ServiceProviderEngineScope
对象已经释放了,它的GetService
方法将会抛出ObjectDisposedException
异常,这也是很多同学在使用作用域是碰到最多的情况。怎么避免这种问题呢?前面已经提示了,因为IServiceScopeFactory
是单例的,所以我们只需要在作用域有效期内创建即可,后续可以随便使用来创建新的作用域,不管创建这个IServiceScopeFactory
的容器是否已经被释放(即作用域是否已释放)。
还有这里记一点,.net core原生的作用域是没有作用域树的概念的,也就是说,它的每个作用域都是一样的,他们均由根容器创建,一个父作用域创建了一个子作用域,如果父作用域释放了,并不影响子作用域的使用!
Autofac在.net core中的集成
其实Autofac的生命周期很多,连Scoped作用域还分好几种,以后有机会可以再写写,这里我以官方的Autofac.Extensions.DependencyInjection
库集成到.net core中使用的方式,来叙述它与原生.net core中Scoped作用域的区别。
首先,可以使用Nuget按照Autofac.Extensions.DependencyInjection
库,但是不同版本.net core的集成方式可能不一样,我这里使用.net 6,只需要这么用就可以了:
var builder = WebApplication.CreateBuilder(args);
//替换原生容器
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
上述代码使用后,原生容器的一些服务就会被替换掉了:
IServiceProvider:使用AutofacServiceProvider对象来实现
IServiceScopeFactory:使用AutofacServiceScopeFactory对象来实现,注意,这个时候,它就不是单例了,而是临时的
IServiceScope:使用AutofacServiceScope对象来实现
public static void Populate(this ContainerBuilder builder,IEnumerable<ServiceDescriptor> descriptors,object lifetimeScopeTagForSingletons)
{
if (descriptors == null)
{
throw new ArgumentNullException(nameof(descriptors));
}
var serviceProviderRegistration = builder.RegisterType<AutofacServiceProvider>()
.As<IServiceProvider>()
.ExternallyOwned();
#if NET6_0_OR_GREATER
// Add the additional registration if needed.
serviceProviderRegistration.As<IServiceProviderIsService>();
#endif
builder.RegisterType<AutofacServiceScopeFactory>().As<IServiceScopeFactory>();
Register(builder, descriptors, lifetimeScopeTagForSingletons);
}
我们在注入IServiceProvider
时,就会使用AutofacServiceProvider
对象,它会传入一个Autofac的ILifetimeScope
接口,调用AutofacServiceProvider
的GetService
方法本质上就是调用ILifetimeScope
的对应的方法,这样,就与原生的方案对齐了。
不过这里要说的是Scoped作用域的集成使用,首先,使用流程上与原生的方式是一样的,但是Autofac的容器是一个作用域树的结构,它的根容器是IContainer
接口(其实也是一个ILifetimeScope
接口,实现类是Autofac.Core.Container
),根容器传进的其它ILifetimeScope
都是子容器(实现类是Autofac.Core.Lifetime.LifetimeScope
),它与父容器密切相关,当你从一个ILifetimeScope
容器中获取服务时,它会检查当前容器以及所有的父、祖父等容器是否有效,也就是有没有释放,如果已释放,那么子容器就不可以使用了,这点与.net core原生的设计是不一样的。
public class LifetimeScope : Disposable, ISharingLifetimeScope, IServiceProvider
{
...
public ILifetimeScope BeginLifetimeScope(object tag)
{
CheckNotDisposed();
CheckTagIsUnique(tag);
var scope = new LifetimeScope(ComponentRegistry, this, tag);
RaiseBeginning(scope);
return scope;
}
private void CheckNotDisposed()
{
if (IsTreeDisposed())
{
throw new ObjectDisposedException(LifetimeScopeResources.ScopeIsDisposed, innerException: null);
}
}
private bool IsTreeDisposed()
{
return IsDisposed || (_parentScope?.IsTreeDisposed() ?? false);
}
}
这样,可能就有问题了,比如:
public void Test(IServiceProvider serviceProvider)
{
var parentScope = serviceProvider.CreateScope();
var childScope = parentScope.ServiceProvider.CreateScope();
var serviceScopeFactory = childScope.ServiceProvider.GetRequiredService<IServiceScopeFactory>();
parentScope.Dispose();
//如果是Autofac,这里会报错,如果.net core原生容器则不会
var service = childScope.ServiceProvider.GetService<MyService>();
//哪怕是提前获取了服务,如果是Autofac,这里也会报错
var newScope = serviceScopeFactory.CreateScope();
}
那么怎么解决这个问题?要么在开发的时候注意一下,不要随意释放容器,要么我们就从根容器来创建Scoped作用域,为此,我们只需要获取到根容器即可。
获取根容器的方法有好几个,最简单一个就是使用全局变量保存起来,.net core在启动过程中,在添加管道中间件时(也就是各种app.UserXXX),内部有个容器,它就是就是根容器,比如(我这里是.net6):
var builder = WebApplication.CreateBuilder(args);
//替换原生容器
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
//获取ApplicationBuilder
var app = builder.Build();
//保存到静态变量中去,以后就可以使用Providers.Root得到根容器了,这个办法是通用的
Providers.SetRoot(app.Services);
class Providers
{
static IServiceProvider root;
public static IServiceProvider Root => root;
public static void SetRoot(IServiceProvider serviceProvider)
{
root = serviceProvider;
}
}
但是个人建议,在实际开发过程中,我们应该尽量避免使用根容器,如果确实需要,我们应该使用根容器来创建作用域,再来实现我们的业务,使用完记得释放作用域。
除此之外,我们还有一个办法,不就是要从根节点获取一个新的作用域的,干脆通过拓展方法来保证我们的作用域是从根容器创建的就行了,比如:
public static class ServiceProviderExtensions
{
public static IServiceScope CreateRootScope(this IServiceProvider serviceProvider)
{
IServiceScopeFactory serviceScopeFactory;
//如果是Autofac
if (serviceProvider is AutofacServiceProvider autofacServiceProvider)
{
if (autofacServiceProvider.LifetimeScope is Autofac.Core.Lifetime.LifetimeScope lifetimeScope)
{
serviceScopeFactory = lifetimeScope.RootLifetimeScope.Resolve<IServiceScopeFactory>();
}
else
{
//从根节点获取就可以了
serviceScopeFactory = autofacServiceProvider.GetRequiredService<IServiceScopeFactory>();
}
}
else
{
serviceScopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
}
//否则直接创建
return serviceScopeFactory.CreateScope();
}
}
所以,开发过程中,我们需要注意这些细节。
结语
本文源于一次bug的解决,过程中总是出现服务被释放的问题,就是在写一些定时任务的时候,没控制好作用域,导致父作用域被释放了,找了很久,最终定位到就是作用域的用法错误,算是积累了一次经验吧,这里我把它写出来,记录原生容器与Autofac的差异,后续肯定还有同学碰到的。
.net core的容器不是很好用,所以一般我比较喜欢集成Autofac,但是因为兼容性的问题,过程中确实碰到了很多问题,这里说的就是其中一个,再比如之前提到的命名服务的问题,希望.net core的生态越来越好吧。至于Autofac,确实是非常优秀的.net库,后面看下有时间在写它的介绍,用法确实非常丰富,