标题反映的是上周五一个同事咨询我的问题,我觉得这是一个很好的问题。这个问题有助于我们深入理解依赖注入框架在ASP.NET Core中的应用,以及服务实例的生命周期。

一、问题重现

我们通过一个简单的实例来模拟该同事遇到的问题。我们采用极简的方式创建了如下这个ASP.NET Core MVC应用。如下面的代码片段所示,除了注册与ASP.NET Core MVC框架相关的服务与中间件之外,我们还调用了IHostBuilder的UseDefaultServiceProvider方法将配置选项ServiceProviderOptions的ValidateScopes属性设置为True,以开启针对服务范围的验证。我们还采用Scoped生命周期模式注册了服务IFoobar,具体的实现类型Foobar还实现了IDisposable接口。

public class Program
{
public static void Main()
{
Host
.CreateDefaultBuilder()
.UseDefaultServiceProvider(options => options.ValidateScopes = true)
.ConfigureWebHostDefaults(builder => builder
.ConfigureLogging(logging => logging.ClearProviders())
.ConfigureServices(services => services
.AddScoped<IFoobar, Foobar>()
.AddRouting()
.AddControllers())
.Configure(app => app
.UseRouting()
.UseEndpoints(endpoints => endpoints.MapControllers())))
.Build()
.Run();
}
} public interface IFoobar { }
public class Foobar : IFoobar, IDisposable
{
public void Dispose() => Console.WriteLine("Foobar.Dispose();");
}

我们创建了如下这个HomeController,它的构造函数中注入了一个IServiceProvider对象。在Action方法Index中,我们调用Task的静态方法Run异步执行了一些操作。具体来说,在异步执行的操作中,我们利用调用上面注入的这个IServiceProvider对象的GetRequiredService<T>方法试图获取一个IFoobar服务实例。由于这段操作时在一个Try/Catch中执行的,抛出的异常消息的堆栈信息会直接输出到控制台上。

public class HomeController: Controller
{
private readonly IServiceProvider _requestServices;
public HomeController(IServiceProvider requestServices)
{
_requestServices = requestServices;
}
[HttpGet("/")]
public IActionResult Index()
{
Task.Run(async() => {
try
{
await Task.Delay(100);
var foobar = _requestServices.GetRequiredService<IFoobar>();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}
});
return Ok();
}
}

在运行该应用程序后,我们利用浏览器采用根路径(“/”)对Action方法Index发起访问后,服务端控制台上会出现如下所示的错误信息。

二、ApplicationServices与RequestServices

从上图所示的错误消息可以看出,问题出在我们试图利用一个被Dispose的IServiceProvider来获取我们所需的服务实例。我们知道,ASP.NET Core应用在启动和请求处理过程中所需的服务几乎都是由代表DI容器的IServiceProvider提供的。具体来说,这里存在着两种类型的IServiceProvider对象,一种与当前应用的生命周期保持一致,我们一般将其称为ApplicationServices,另一种则是具体针对每个请求的IServiceProvider对象,我们将其称为RequestServices。

一般来说,ApplicationServices用于提供管道构建过程中所需的服务实例,具体请求处理过程中所需的服务实例一般由RequestServices提供。具体来说,对于接收的每一个请求,ASP.NET Core框架都会利用ApplicationServices创建一个代表服务范围的IServiceScope对象,后者就是对RequestServices的封装。在完成了针对请求的处理之后,服务范围被终结,RequestServices被Dispose。

对于我们演示的实例来说,注入到HomeController构造函数中的IServiceProvider是RequestServices,由于针对RequestServices的使用是在另一个后台线程中执行的,并且在使用的时候针对当前请求的处理已经结束(因为我们人为等待了100毫秒),自然就会出现上图所示的异常。

三、如何获取ApplicationServices

既然与请求绑定的RequestServices不能用,我们只能使用与应用绑定的ApplicationServices,那么后者如何得到呢?ASP.NET Core 3采用了基于IHost/IHostBuilder的承载方式,表示宿主的IHost接口具有如下所示的Services属性,它返回的正式我们所需的ApplicationServices。

public interface IHost : IDisposable
{
Task StartAsync(CancellationToken cancellationToken = new CancellationToken());
Task StopAsync(CancellationToken cancellationToken = new CancellationToken()); IServiceProvider Services { get; }
}

对于我们演示的程序来说,我们可以采用如下的方式在HomeController的构造中注入IHost服务的方式间接地获得这个ApplicationServices对象。

public class HomeController: Controller
{
private readonly IServiceProvider _applicationServices;
public HomeController(IHost host)
{
_applicationServices = host.Services;
}
[HttpGet("/")]
public IActionResult Index()
{
Task.Run(async() => {
try
{
await Task.Delay(100);
var foobar = _applicationServices.GetRequiredService<IFoobar>();
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.StackTrace);
}
});
return Ok();
}
}

当我们采用如上的方式将RequestServices替换成ApplicationServices之后,我们的问题是否就解决了呢?在采用上面相同的方式进行测试之后,我们会发现服务端控制台上出现了如下所示的错误消息。

四、服务实例的生命周期

上面的问题是由我们试图利用一个代表“根容器”的IServiceProvider对象去解析一个生命周期模式为Scoped服务实例导致,具体的原因在《依赖注入[8]:服务实例的生命周期》已经讲得很清楚了。为了解决这个问题,我们应该根据ApplicationServices创建一个“服务范围”,并在该服务范围内提取我们所需的服务实例。为了确保服务实例能够被正常回收,我们还应该将代表服务范围的IServiceScope对象及时终结掉。如下所示的是正确的编程方式。

public class HomeController: Controller
{
private readonly IServiceProvider _applicationServices;
public HomeController(IHost host)
{
_applicationServices = host.Services;
}
[HttpGet("/")]
public IActionResult Index()
{
Task.Run(async() => {
await Task.Delay(100);
using (var scope = _applicationServices.CreateScope())
{
var foobar = scope.ServiceProvider.GetRequiredService<IFoobar>();
}
});
return Ok();
}
}

五、统一的解决方案

之前我们将问题的解决方案落实在如何获取与当前应用具有相同生命周期的ApplicationServices上,所以我们采用注入IHost的方式得到这个ApplicationServices。如果采用传统的基于IWebHost/IWebHostBuilder的承载方式,IHost自然是获取不到了。但是我们是真的需要这个ApplicationServices对象吗?其实不是,我们真正需要的是利用它创建一个代表服务范围的IServiceScope对象,并在该范围内消费我们所需的服务实例。由于IServiceScope是通过IServiceScopeFactory创建的,所以我们只需要注入IServiceScopeFactory即可。

public class HomeController : Controller
{
private readonly IServiceScopeFactory _serviceScopeFactory; public HomeController(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
} [HttpGet("/")]
public IActionResult Index()
{
Task.Run(async () =>
{
await Task.Delay(100);
using (var scope = _serviceScopeFactory.CreateScope())
{
var foobar = scope.ServiceProvider.GetRequiredService<IFoobar>();
}
});
return Ok();
}
}

[ASP.NET Core 3框架揭秘] 异步线程无法使用IServiceProvider?的更多相关文章

  1. ASP.NET Core 6框架揭秘实例演示[06]:依赖注入框架设计细节

    由于依赖注入具有举足轻重的作用,所以<ASP.NET Core 6框架揭秘>的绝大部分章节都会涉及这一主题.本书第3章对.NET原生的依赖注入框架的设计和实现进行了系统的介绍,其中设计一些 ...

  2. ASP.NET Core 6框架揭秘实例演示[12]:诊断跟踪的进阶用法

    一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根据当前的运行状态预知未来可能发生的问题,并将问题扼杀在摇篮中.诊断跟踪能够帮助我们有效地纠错和排错&l ...

  3. [ASP.NET Core 3框架揭秘] 跨平台开发体验: Windows [上篇]

    微软在千禧年推出 .NET战略,并在两年后推出第一个版本的.NET Framework和IDE(Visual Studio.NET 2002,后来改名为Visual Studio),如果你是一个资深的 ...

  4. [ASP.NET Core 3框架揭秘] 跨平台开发体验: Docker

    对于一个 .NET Core开发人员,你可能没有使用过Docker,但是你不可能没有听说过Docker.Docker是Github上最受欢迎的开源项目之一,它号称要成为所有云应用的基石,并把互联网升级 ...

  5. [ASP.NET Core 3框架揭秘] 依赖注入:控制反转

    ASP.NET Core框架建立在一些核心的基础框架之上,这些基础框架包括依赖注入.文件系统.配置选项和诊断日志等.这些框架不仅仅是支撑ASP.NET Core框架的基础,我们在进行应用开发的时候同样 ...

  6. [ASP.NET Core 3框架揭秘] 依赖注入[5]: 利用容器提供服务

    毫不夸张地说,整个ASP.NET Core框架是建立在依赖注入框架之上的.ASP.NET Core应用在启动时构建管道以及利用该管道处理每个请求过程中使用到的服务对象均来源于依赖注入容器.该依赖注入容 ...

  7. [ASP.NET Core 3框架揭秘] 文件系统[1]:抽象的“文件系统”

    ASP.NET Core应用 具有很多读取文件的场景,比如配置文件.静态Web资源文件(比如CSS.JavaScript和图片文件等)以及MVC应用的View文件,甚至是直接编译到程序集中的内嵌资源文 ...

  8. [ASP.NET Core 3框架揭秘] 文件系统[2]:总体设计

    在<抽象的"文件系统">中,我们通过几个简单的实例演示从编程的角度对文件系统做了初步的体验,接下来我们继续从设计的角度来进一步认识它.这个抽象的文件系统以目录的形式来组 ...

  9. [ASP.NET Core 3框架揭秘] 配置[1]:读取配置数据[上篇]

    提到"配置"二字,我想绝大部分.NET开发人员脑海中会立即浮现出两个特殊文件的身影,那就是我们再熟悉不过的app.config和web.config,多年以来我们已经习惯了将结构化 ...

随机推荐

  1. django-模板之for标签(十)

  2. web.xml 配置文件 超详细说明!!!

    一.web.xml是什么? 首先 web.xml 是java web 项目的一个重要的配置文件,但是web.xml文件并不是Java web工程必须的. web.xml文件是用来配置:欢迎页.serv ...

  3. Java HashSet对txt文本内容去重(统计小说用过的字或字数)

    Java HashSet对txt文本内容去重(统计小说用过的字或字数) 基本思路: 1.字节流读需要去重的txt文本.(展示demo为当前workspace下名为utf-8.txt的文本) 2.对读取 ...

  4. 相关推导式-Python

    列表.’字典等推导式 #利用zip()函数同时给多个变量赋值 a = [1,2,3,4,5] b = [4,5,6,7,8] c = [9,2,3,4,0] l = [[1,2],[3,4]] for ...

  5. ElasticSearch(三):通分词器(Analyzer)进行分词(Analysis)

    ElasticSearch(三):通过分词器(Analyzer)进行分词(Analysis) 学习课程链接<Elasticsearch核心技术与实战> Analysis与Analyzer ...

  6. 持续集成Gitlab CICD Runner&Jenkins

    目录 使用Gitlab Runner实现 再要部署的服务器上安装 gitlab runner 下载可执行文件 设置可执行权限权限 创建用户 运行服务 注册 Runner 到gitlab上找到需要用的U ...

  7. m99 然而并没有想出来标题!

    这是放假回来的第一次考试,如同往常一样,我每逢放假回来第一次考试就会废掉,这次也不例外 这次不想粘成绩,因为实在是rp没了! 之前的几次都是别人在CE等等被lemon砍分,而我被lemon多测分. 但 ...

  8. C语言程序设计100例之(4):水仙花数

    例4    水仙花数 题目描述 一个三位整数(100-999),若各位数的立方和等于该数自身,则称其为“水仙花数”(如:153=13+53+33),找出所有的这种数. 输入格式 没有输入 输出格式 若 ...

  9. 生活问题 | 对华为畅玩手机5X进行升级

    步骤一:准备一张SD卡,建议使用Sandisk, Kingstone, 或Kingmax,大小建议在2G KIW-AL10C00B258 软件版本升级指导书 Secret  2016-11-25 Hu ...

  10. MQ应用之解耦

    简介 消息队列 MQ 既可为分布式应用系统提供异步解耦和削峰填谷的能力,同时也具备互联网应用所需的海量消息堆积.高吞吐.可靠重试等特性. 应用场景 削峰填谷:诸如秒杀.抢红包.企业开门红等大型活动时皆 ...