首页 > 其他分享 >漫谈.net core和Autofac中的Scoped生命周期

漫谈.net core和Autofac中的Scoped生命周期

时间:2024-02-28 14:26:39浏览次数:22  
标签:core Autofac 容器 作用域 Scoped IServiceProvider net

  我们知道,.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库,后面看下有时间在写它的介绍,用法确实非常丰富,

 

标签:core,Autofac,容器,作用域,Scoped,IServiceProvider,net
From: https://www.cnblogs.com/shanfeng1000/p/17792953.html

相关文章

  • ASP.NET Core MVC应用模型的构建[3]: Controller的收集
    从编程的角度来看,一个MVC应用是由一系列Controller类型构建而成的,所以对于一个代表应用模型的ApplicationModel对象来说,它的核心就是Controllers属性返回的一组ControllerModel对象,每个ControllerModel对象是应用模型针对Controller类型的描述。一、ControllerModel二、实例演示......
  • .Net core & C#
    1.VisualStudio安装时.net桌面开发和通用Windows平台开发的区别?在VisualStudio的安装选项中,“.NET桌面开发”与“通用Windows平台开发”指的是两种不同的应用程序开发框架。具体分析如下:.NET桌面开发:这个选项包括了用于开发传统的Win32桌面应用程序的组件,例如WindowsForms(......
  • 样本轮廓系数(原理、sklearn.metrics.silhouette_score、silhouette_samples参数介绍)
    https://blog.csdn.net/maple05/article/details/110454075?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522170902662116800226570765%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=170902662116800226570765&biz_id=0&am......
  • 使用cmd命令行(.NET Core CLI)来启动ASP.NET Core 应用程序的多个实例
    本章主要和大家分享下如何使用cmd命令行(.NETCoreCLI)来启动ASP.NETCore应用程序的多个实例,以此来模拟集群。.NETCore命令行接口(CLI)工具是用于开发、生成、运行和发布.NETCore应用程序的跨平台工具链。CLI命令结构包含驱动程序(“dotnet”)和命令,还可能包含命令参数......
  • ASP.NET Core 过滤器返回自定义响应数据
    自定义返回类publicclassApiResponse{publicintCode{get;set;}publicstringMessage{get;set;}publicobjectData{get;set;}publicApiResponse(intcode,stringmessage,objectdata=null){Code=code;......
  • 13.分布式事件总线DotNetCore.CAP的简单使用
    DotNetCore.CAP框架提供了一个简单易用的API和多种消息传输协议支持(包括Redis、RabbitMQ等),可以让用户轻松地实现消息队列、事件发布/订阅、分布式事务等功能。它还具备自动重试、异常处理、数据序列化等高级特性,可以保证消息的可靠性和一致性。使用DotNetCore.CAP框架,你可以:1.......
  • 开发框架DevExpress XAF - Entity Framework Core 8支持.NET 8性能基准
    DevExpressXAF是一款强大的现代应用程序框架,允许同时开发ASP.NET和WinForms。XAF采用模块化设计,开发人员可以选择内建模块,也可以自行创建,从而以更快的速度和比开发人员当前更强有力的方式创建应用程序。对于使用EntityFrameworkCore(EFCore)(实体核心框架)的用户来说,这是一个......
  • ASP.NET MVC中使用Autofac依赖注入
      ASP.NETMVC中使用Autofac依赖注入官网文档:https://docs.autofac.org/en/latest/integration/mvc.html2024年02月26日在.net4.8framework建立的MVC项目中测试通过引入NUGET包:Autofac和Autofac.Mvc5Global中加入以下代码: //autofac注入ContainerBuilderbuil......
  • [超实用插件]在Visual Studio中查看EF Core查询计划
    前言EFCore是我们.NET开发中比较常用的一款ORM框架,今天我们分享一款可以直接在VisualStudio中查看EFCore查询计划调试器可视化工具(帮助开发者分析和优化数据库查询性能):EFCore.Visualizer。值得推荐的.NETORM框架对于还不知道怎么选择.NETORM框架的同学可以看下面这两篇文......
  • Net8 Autofac实现依赖注入(构造函数注入、属性注入)
    项目以net8建立为例子(net6也通用),使用Autofac实现构造函数注入、属性注入两种。引用以下packageAutofacAutofac.Extensions.DependencyInjectionMicrosoft.Extensions.DependencyModel在program下添加autofacbuilder.Host.UseServiceProviderFactory(newAutofacServicePr......