当前位置 : 主页 > 网络编程 > ASP >

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

来源:互联网 收集:自由互联 发布时间:2023-01-30
目录 前言 问题演示 解决问题 问题探究 请求中的IServiceProvider 请求中的IServiceProvider和IServiceScopeFactory 后续插曲 总结 前言 问题的起因是在帮同事解决遇到的一个问题,他的本意是在E
目录
  • 前言
  • 问题演示
  • 解决问题
  • 问题探究
    • 请求中的IServiceProvider
    • 请求中的IServiceProvider和IServiceScopeFactory
  • 后续插曲
    • 总结

      前言

      问题的起因是在帮同事解决遇到的一个问题,他的本意是在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<InformationController> _logger;
      
          public InformationController(LibraryContext libraryContext, 
              IServiceProvider serviceProvider,
              ILogger<InformationController> 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<LibraryContext>();
                      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<LibraryContext>();
                  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<InformationController> _logger;
      
          public InformationController(LibraryContext libraryContext, 
              IServiceScopeFactory scopeFactory,
              ILogger<InformationController> 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<LibraryContext>();
                      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

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

      上一篇:ASP.NETMVC在基控制器中处理Session
      下一篇:没有了
      网友评论