漫谈.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),然后我们通过IServiceScopeFactoryCreateScope方法创建一个IServiceScope作用域对象(其实也是ServiceProviderEngineScope),接着调用IServiceScope作用域的ServiceProvider属性得到一个新容器(其实就是IServiceScope作用域自身,也就是ServiceProviderEngineScope),最后当我们调用ServiceProvider属性的GetService方法时,其实就是调用IServiceScopeGetService方法(也就是ServiceProviderEngineScopeGetService方法),最终由RootProvider根容器调用它自己的GetService方法来创建实例,注意,这个时候它不仅传了类型,还传入了当前IServiceScope对象作为区分不同Scoped作用域的一个标识,ServiceProviderEngineScope构造函数中传进来的的ServiceProvider ,其实就是根容器,也就是说,实际上所有的服务最终都是由根容器创建的。

  在实际开发中,我们可以通过几种方式来创建一个IServiceScope的作用域,但最终都是通过IServiceScopeFactory 接口来创建作用域的,比如通过IServiceProviderCreateScope拓展方法:

    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接口,调用AutofacServiceProviderGetService方法本质上就是调用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库,后面看下有时间在写它的介绍,用法确实非常丰富,

 

posted @ 2024-02-28 14:12  没有星星的夏季  阅读(391)  评论(0编辑  收藏  举报