• 解决ASP.NET Core在Task中使用IServiceProvider的问题


    前言#

        问题的起因是在帮同事解决遇到的一个问题,他的本意是在EF Core中为了解决避免多个线程使用同一个DbContext实例的问题。但是由于对Microsoft.Extensions.DependencyInjection体系的深度不是很了解,结果遇到了新的问题,当时整得我也有点蒙了,所以当时也没解决,而且当时快下班了,就想着第二天再解决。在地铁上,经过我一系列的思维跳跃,终于想到了问题的原因,第二天也顺利的解决了这个问题。虽然我前面说了EFCore,但是本质和EFCore没有关系,只是凑巧。解决了之后觉得这个问题是个易错题,觉得挺有意思的,便趁机记录一下。

    问题演示#

    接下来我们还原一下当时的场景,以下代码只是作为演示,无任何具体含义,只是为了让操作显得更清晰一下,接下来就贴一下当时的场景代码

    [Route("api/[controller]/[action]")]
    [ApiController]
    public class InformationController : ControllerBase
    {
        private readonly LibraryContext _libraryContext;
        private readonly IServiceProvider _serviceProvider;
        private readonly ILogger _logger;
    
        public InformationController(LibraryContext libraryContext, 
            IServiceProvider serviceProvider,
            ILogger logger)
        {
            _libraryContext = libraryContext;
            _serviceProvider = serviceProvider;
            _logger = logger;
        }
    
        [HttpGet]
        public string GetFirst()
        {
            var caseInfo = _libraryContext.Informations.Where(i => i.IsDelete == 0).FirstOrDefault();
            //这里直接使用了Task方式
            Task.Run(() => {
                try
                {
                    //Task里创建了新的IServiceScope
                    using var scope = _serviceProvider.CreateScope();
                    //通过IServiceScope创建具体实例
                    LibraryContext dbContext = scope.ServiceProvider.GetService();
                    var list = dbContext!.Informations.Where(i => i.IsDelete == 0).Take(100).ToList();
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex.Message, ex);
                }
            });
            return caseInfo.Title;
        }
    }
    

    再次强调一下,上述代码纯粹是为了让演示更清晰,无任何业务含义,不喜勿喷。咱们首先看一下这段代码表现出来的意思,就是在ASP.NET Core的项目里,在Task.Run里使用IServiceProvider去创建Scope的场景。如果对ASP.NET Core Controller生命周期和IServiceProvider不够了解的话,会很容易遇到这个问题,且不知道是什么原因。上述这段代码会偶现一个错误

    Cannot access a disposed object.
    Object name: 'IServiceProvider'.
    

    这里为什么说是偶现呢?因为会不会出现异常完全取决于Task.Run里的代码是在当前请求输出之前执行完成还是之后完成。说到这里相信有一部分同学已经猜到了代码报错的原因了。问题的本质很简单,是因为IServiceProvider被释放掉了。我们知道默认情况下ASP.NET Core为每次请求处理会创建单独的IServiceScope,这会关乎到声明周期为Scope对象的声明周期。所以如果Task.Run里的逻辑在请求输出之前执行完成,那么代码运行没任何问题。如果是在请求完成之后完成再执行CreateScope操作,那必然会报错。因为Task.Run里的逻辑何时被执行,这个是由系统CPU调度本身决定的,特别是CPU比较繁忙的时候,这种异常会变得更加频繁。

    这个问题不仅仅是在Task.Run这种场景里,类似的本质就是在一个IServiceScope里创建一个新的子Scope作用域的时候,这个时候需要注意的是父级的IServiceProvider释放问题,如果父级的IServiceProvider已经被释放了,那么基于这个Provider再去创建Scope则会出现异常。但是这个问题在结合Task或者多线程的时候,更容易出现问题。

    解决问题#

    既然我们知道了它为何会出现异常,那么解决起来也就顺理成章了。那就是保证当前请求执行完成之前,最好保证Task.Run里的逻辑也要执行完成,所以我们上述的代码会变成这样

    [HttpGet]
    public async Task<string> GetFirst()
    {
        var caseInfo = _libraryContext.Informations.Where(i => i.IsDelete == 0).FirstOrDefault();
        //这里使用了await Task方式
        await Task.Run(() => {
            try
            {
                //Task里创建了新的IServiceScope
                using var scope = _serviceProvider.CreateScope();
                //通过IServiceScope创建具体实例
                LibraryContext dbContext = scope.ServiceProvider.GetService();
                var list = dbContext!.Informations.Where(i => i.IsDelete == 0).Take(100).ToList();
            }
            catch (Exception ex)
            {
                _logger.LogError(ex.Message, ex);
            }
        });
        return caseInfo.Title;
    }
    

    试一下,发现确实能解决问题,因为等待Task完成能保证Task里的逻辑能在请求执行完成之前完成。但是,很多时候我们并不需要等待Task执行完成,因为我们就是希望它在后台线程去执行这些操作,而不需要阻塞执行。

    上面我们提到了本质是解决在IServiceScope创建子Scope时遇到的问题,因为这里注入进来的IServiceProvider本身是Scope的,只在当前请求内有效,所以基于IServiceProvider去创建IServiceScope要考虑到当前IServiceProvider是否释放。那么我们就得打破这个枷锁,我们要想办法在根容器中去创建新的IServiceScope。这一点我大微软自然是考虑到了,在Microsoft.Extensions.DependencyInjection体系中提供了IServiceScopeFactory这个根容器的作用域,基于根容器创建的IServiceScope可以得到平行与当前请求作用域的独立的作用域,而不受当前请求的影响。改造上面的代码用以下形式

    [Route("api/[controller]/[action]")]
    [ApiController]
    public class InformationController : ControllerBase
    {
        private readonly LibraryContext _libraryContext;
        private readonly IServiceScopeFactory _scopeFactory;
        private readonly ILogger _logger;
    
        public InformationController(LibraryContext libraryContext, 
            IServiceScopeFactory scopeFactory,
            ILogger logger)
        {
            _libraryContext = libraryContext;
            _scopeFactory = scopeFactory;
            _logger = logger;
        }
    
        [HttpGet]
        public string GetFirst()
        {
            var caseInfo = _libraryContext.Informations.Where(i => i.IsDelete == 0).FirstOrDefault();
            //这里直接使用了Task方式
            Task.Run(() => {
                try
                {
                    //Task里创建了新的IServiceScope
                    using var scope = _scopeFactory.CreateScope();
                    //通过IServiceScope创建具体实例
                    LibraryContext dbContext = scope.ServiceProvider.GetService();
                    var list = dbContext!.Informations.Where(i => i.IsDelete == 0).Take(100).ToList();
                }
                catch (Exception ex)
                {
                    _logger.LogError(ex.Message, ex);
                }
            });
            return caseInfo.Title;
        }
    }
    

    如果你是调试起来的话你可以看到IServiceScopeFactory的具体实例是Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope类型的,它里面包含了一个IsRootScope属性,通过这个属性我们可以知道当前容器作用域是否是根容器作用域。当使用IServiceProvider实例的时候IsRootScopefalse,当使用IServiceScopeFactory实例的时候IsRootScopetrue。使用CreateScope创建IServiceScope实例的时候,注意用完了需要释放,否则可能会导致TransientScope类型的实例得不到释放。在之前的文章咱们曾提到过TransientScope类型的实例都是在当前容器作用域释放的时候释放的,这个需要注意一下。

    问题探究#

    上面我们了解到了在每次请求的时候使用IServiceProvider和使用IServiceScopeFactory的时候他们作用域的实例来源是不一样的。IServiceScopeFactory来自根容器,IServiceProvider则是来自当前请求的Scope。顺着这个思路我们可以看一下他们两个究竟是如何的不相同。这个问题还得从构建Controller实例的时候,注入到Controller中的实例作用域的问题。

    请求中的IServiceProvider#

    在之前的文章我们知道,Controller是每次请求都会创建新的实例,我们再次拿出来这段核心的代码来看一下,在DefaultControllerActivator类的Create方法中[点击查看源码👈]

    internal class DefaultControllerActivator : IControllerActivator
    {
        private readonly ITypeActivatorCache _typeActivatorCache;
        public DefaultControllerActivator(ITypeActivatorCache typeActivatorCache)
        {
            _typeActivatorCache = typeActivatorCache;
        }
    
        public object Create(ControllerContext controllerContext)
        {
            //省略一系列判断代码
    
            var serviceProvider = controllerContext.HttpContext.RequestServices;
            //这里传递的IServiceProvider本质就是来自HttpContext.RequestServices
            return _typeActivatorCache.CreateInstance<object>(serviceProvider, controllerTypeInfo.AsType());
        }
    }
    

    通过这个方法我们可以看到创建Controller实例时,如果存在构造依赖,本质则是通过HttpContext.RequestServices实例创建出来的,而它本身就是IServiceProvider的实例,ITypeActivatorCache实例中则存在真正创建Controller实例的逻辑,具体可以查看TypeActivatorCache类的实现[点击查看源码👈]

    internal class TypeActivatorCache : ITypeActivatorCache
    {
        private readonly Func _createFactory =
            (type) => ActivatorUtilities.CreateFactory(type, Type.EmptyTypes);
        private readonly ConcurrentDictionary _typeActivatorCache =
                new ConcurrentDictionary();
    
        public TInstance CreateInstance<TInstance>(
            IServiceProvider serviceProvider,
            Type implementationType)
        {
            //省略一系列判断代码
    
            var createFactory = _typeActivatorCache.GetOrAdd(implementationType, _createFactory);
            //创建Controller的时候,需要的依赖实例都是来自IServiceProvider
            return (TInstance)createFactory(serviceProvider, arguments: null);
        }
    }
    

    其实在这里我们就可以得到一个结论,我们在当前请求默认通过构造注入的IServiceProvider的实例其实就是HttpContext.RequestServices,也就是针对当前请求的作用域有效,同样的是来自当前作用域的Scope周期的对象实例也是在当前请求结束就会释放。验证这个很简单可以写个demo来演示一下

    [Route("api/[controller]/[action]")]
    [ApiController]
    public class InformationController : ControllerBase
    {
        private readonly IServiceProvider _serviceProvider;
    
        public InformationController(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }
    
        [HttpGet]
        public bool[] JudgeScope([FromServices]IServiceProvider scopeProvider)
        {
            //比较构造注入的和在HttpContext获取的
            bool isEqualOne = _serviceProvider == HttpContext.RequestServices;
            //比较通过Action绑定的和在HttpContext获取的
            bool isEqualTwo = scopeProvider == HttpContext.RequestServices;
            return new[] { isEqualOne, isEqualTwo };
        }
    }
    

    毫无疑问,默认情况下isEqualOne和isEqualTwo的结构都是true,这也验证了我们上面的结论。因此在当前请求默认注入IServiceProvider实例的时候,都是来自HttpContext.RequestServices的实例。

    请求中的IServiceProvider和IServiceScopeFactory#

    上面我们看到了在当前请求中获取IServiceProvider实例本身就是Scope的,而且在当前请求中通过各种注入方式获取到的实例都是相同的。那么接下来我们就可以继续跟踪,本质的HttpContext.RequestServices的IServiceProvider到底来自什么地方呢?我们找到HttpContext默认的实现类DefaultHttpContext中关于RequestServices属性的定义[点击查看源码👈]

    //接受
    public IServiceScopeFactory ServiceScopeFactory { get; set; } = default!;
    //数据来自RequestServicesFeature
    private static readonly Func _newServiceProvidersFeature = context => new RequestServicesFeature(context, context.ServiceScopeFactory);
    //缓存来自_newServiceProvidersFeature
    private IServiceProvidersFeature ServiceProvidersFeature =>
                _features.Fetch(ref _features.Cache.ServiceProviders, this, _newServiceProvidersFeature)!;
    //数据来自ServiceProvidersFeature的RequestServices
    public override IServiceProvider RequestServices
    {
        get { return ServiceProvidersFeature.RequestServices; }
        set { ServiceProvidersFeature.RequestServices = value; }
    }
    

    通过上面的源码我们可以看到HttpContext.RequestServices的数据最终来自RequestServicesFeature类的RequestServices属性,我们可以直接找到RequestServicesFeature类的定义[点击查看源码👈]

    public class RequestServicesFeature : IServiceProvidersFeature, IDisposable, IAsyncDisposable
    {
        private readonly IServiceScopeFactory? _scopeFactory;
        private IServiceProvider? _requestServices;
        private IServiceScope? _scope;
        private bool _requestServicesSet;
        private readonly HttpContext _context;
    
        public RequestServicesFeature(HttpContext context, IServiceScopeFactory? scopeFactory)
        {
            _context = context;
            _scopeFactory = scopeFactory;
        }
    
        public IServiceProvider RequestServices
        {
            get
            {
                if (!_requestServicesSet && _scopeFactory != null)
                {
                    //释放掉之前没释放掉的RequestServicesFeature实例
                    _context.Response.RegisterForDisposeAsync(this);
                    //通过IServiceScopeFactory创建Scope
                    _scope = _scopeFactory.CreateScope();
                    //RequestServices来自IServiceScopeFactory的CreateScope实例
                    _requestServices = _scope.ServiceProvider;
                    //填充已经设置了RequestServices的标识
                    _requestServicesSet = true;
                }
                return _requestServices!;
            }
            set
            {
                _requestServices = value;
                _requestServicesSet = true;
            }
        }
    
        //释放的真实逻辑
        public ValueTask DisposeAsync()
        {
            switch (_scope)
            {
                case IAsyncDisposable asyncDisposable:
                    var vt = asyncDisposable.DisposeAsync();
                    if (!vt.IsCompletedSuccessfully)
                    {
                        return Awaited(this, vt);
                    }
                    
                    vt.GetAwaiter().GetResult();
                    break;
                case IDisposable disposable:
                    disposable.Dispose();
                    break;
            }
            //释放时重置相关属性
            _scope = null;
            _requestServices = null;
    
            return default;
    
            static async ValueTask Awaited(RequestServicesFeature servicesFeature, ValueTask vt)
            {
                await vt;
                servicesFeature._scope = null;
                servicesFeature._requestServices = null;
            }
        }
        //IDisposable的Dispose的方法,通过using可隐式调用
        public void Dispose()
        {
            DisposeAsync().AsTask().GetAwaiter().GetResult();
        }
    }
    

    通过上面的两段源码,我们得到了许多关于IServiceProvider和IServiceScopeFactory的相关信息。

    • DefaultHttpContext的RequestServices值来自于RequestServicesFeature实例的RequestServices属性
    • RequestServicesFeature的RequestServices属性的值通过IServiceScopeFactory通过CreateScope创建的
    • 构建RequestServicesFeature的IServiceScopeFactory值来自于DefaultHttpContext的ServiceScopeFactory属性

    那么接下来我们直接可以找到DefaultHttpContext的ServiceScopeFactory属性是谁给它赋的值,我们找到创建HttpContext的地方,在DefaultHttpContextFactory的Create方法里[点击查看源码👈]

    public class DefaultHttpContextFactory : IHttpContextFactory
    {
        private readonly IHttpContextAccessor? _httpContextAccessor;
        private readonly FormOptions _formOptions;
        private readonly IServiceScopeFactory _serviceScopeFactory;
    
        public DefaultHttpContextFactory(IServiceProvider serviceProvider)
        {
            _httpContextAccessor = serviceProvider.GetService();
            _formOptions = serviceProvider.GetRequiredService>().Value;
            //通过IServiceProvider的GetRequiredService直接获取IServiceScopeFactory
            _serviceScopeFactory = serviceProvider.GetRequiredService();
        }
    
        //创建HttpContext实例的方法
        public HttpContext Create(IFeatureCollection featureCollection)
        {
            if (featureCollection is null)
            {
                throw new ArgumentNullException(nameof(featureCollection));
            }
    
            var httpContext = new DefaultHttpContext(featureCollection);
            Initialize(httpContext);
            return httpContext;
        }
    
        private DefaultHttpContext Initialize(DefaultHttpContext httpContext)
        {
            //IHttpContextAccessor也是在这里赋的值
            if (_httpContextAccessor != null)
            {
                _httpContextAccessor.HttpContext = httpContext;
            }
    
            httpContext.FormOptions = _formOptions;
            //DefaultHttpContext的ServiceScopeFactory属性值来自注入的IServiceProvider
            httpContext.ServiceScopeFactory = _serviceScopeFactory;
    
            return httpContext;
        }
    }
    

    这里我们可以看到IServiceScopeFactory的实例来自于通过DefaultHttpContextFactory注入的IServiceProvider实例,这里获取IServiceScopeFactory的地方并没有CreateScope,所以这里的IServiceScopeFactoryIServiceProvider中的实例都是来自根容器。这个我们还可以通过注册DefaultHttpContextFactory地方看到
    [点击查看源码👈]

    services.TryAddSingleton();
    

    通过这里可以看到DefaultHttpContextFactory注册的是单例模式,注册它的地方则是在IHostBuilderConfigureServices方法里。关于每次请求的创建流程,不是本文的重点,但是为了让大家对本文讲解的IServiceScopeFactoryIServiceProvider来源更清楚,咱们可以大致的描述一下

    • GenericWebHostService类实现自IHostedService,在StartAsync方法中启动了IServer实例,默认则是启动的Kestrel。
    • IServer启动的方法StartAsync中会传递HostingApplication实例,构建HostingApplication实例的时候则会依赖IHttpContextFactory实例,而IHttpContextFactory实例则是在构建GenericWebHostService服务的时候注入进来的。
    • 当每次请求ASP.NET Core服务的时候会调用HostingApplicationCreateContext方法,该方法中则会创建HttpContext实例,每次请求结束后则调用该类的DisposeContext释放HttpContext实例。

    说了这么多其实就是为了方便让大家得到一个关系,即在每次请求中获取的IServiceProvider实例来自HttpContext.RequestServices实例,HttpContext.RequestServices实例来自IServiceScopeFactory来自CreateScope方法创建的实例,而IServiceScopeFactory实例则是来自根容器,且DefaultHttpContextFactory的生命周期则和当前ASP.NET Core保持一致。

    后续插曲#

    就在解决这个问题后不久,有一次不经意间翻阅微软的官方文档,发现官方文档有提到相关的问题,而且也是结合efcore来讲的。标题是《Do not capture services injected into the controllers on background threads》翻译成中文大概就是不要在后台线程上捕获注入控制器的服务,说的正是这个问题,微软给我们的建议是

    • 注入一个IServiceScopeFactory以便在后台工作项中创建一个范围。
    • IServiceScopeFactory是一个单例对象。
    • 在后台线程中创建一个新的依赖注入范围。
    • 不引用控制器中的任何东西。
    • 不从传入请求中捕获DbContext。

    得到的结论和我们在本文描述的基本上是差不多的,而且微软也很贴心的给我们提供了相关示例

    [HttpGet("/fire-and-forget-3")]
    public IActionResult FireAndForget3([FromServices]IServiceScopeFactory serviceScopeFactory)
    {
        _ = Task.Run(async () =>
        {
            await Task.Delay(1000);
    
            using (var scope = serviceScopeFactory.CreateScope())
            {
                var context = scope.ServiceProvider.GetRequiredService();
                context.Contoso.Add(new Contoso());
                await context.SaveChangesAsync();                                        
            }
        });
    
        return Accepted();
    }
    

    原来还是自己的坑自己最了解,也不得不说微软现在的文档确实挺详细的,同时也提醒我们有空还是得多翻一翻文档避免踩坑。

    总结#

        本文主要是通过帮助同事解决问题而得到的灵感,觉得挺有意思的,希望能帮助更多的人了解这个问题,且能避免这个问题。我们应该深刻理解ASP.NET Core处理每次请求则都会创建一个Scope,这会影响当前请求获取的IServiceProvider实例,和通过IServiceProvider创建的生命周期为Scope的实例。如果把握不住,则可以理解为当前请求直接注入的服务,和当前服务直接注入的IServiceProvider实例。如果想获取根容器的实例则可以通过获取IServiceScopeFactory实例获取,最后请注意IServiceScope的释放问题。
        曾几何时,特别喜欢去解决遇到的问题,特别喜欢那种解决问题沉浸其中的过程。解决了问题,了解到为什么会让自己感觉很通透,也更深刻,不经意间的也扩展了自己的认知边界。这个过程得到的经验是一种通识,是一种意识。而思维和意识则是我们适应这个不断在变化时代的底层逻辑。

    👇欢迎扫码关注我的公众号👇
  • 相关阅读:
    回归和拟合有什么不同-(非)参数检验-假设检验
    HTML+CSS简单漫画网页设计成品--(红猪(9页)带注释)
    使用navicat查看类型颜色
    DigestUtils
    Web攻防05_MySQL_二次注入&堆叠注入&带外注入
    这几个Python装逼神器一定要收藏好
    算法--数据结构
    Java从入门到精通-类和对象(一)
    科学计算语言Julia编程初步
    (三)行为模式:10、策略模式(Strategy Pattern)(C++示例)
  • 原文地址:https://www.cnblogs.com/wucy/p/16566495.html