[ASP.NET Core 3框架揭秘] 依赖注入[5]: 利用容器提供服务
不夸张地说,整个ASP.NET Core框架是建立在依赖注入框架之上的。ASP.NET Core应用在启动时构建管道以及利用该管道处理每个请求过程中使用到的服务对象均来源于依赖注入容器。该依赖注入容器不仅为ASP.NET Core框架自身提供必要的服务,同时也是应用程序的服务提供者,依赖注入已经成为了ASP.NET Core应用的基本编程模式。
一、服务的注册与消费
为了让读者朋友们能够更加容易地认识.NET Core提供的依赖注入框架,我在“《
作为依赖注入容器的IServiceProvider对象不仅为我们提供所需的服务实例,它还帮我们管理这些服务实例的生命周期。如果某个服务类型实现了IDisposable接口,意味着当生命周期完结的时候需要通过调用Dispose方法执行一些资源释放操作,这些操作同样由提供该服务实例的IServiceProvider对象来驱动执行。依赖注入框架针对提供服务实例的释放策略取决于对应的服务注册采用的生命周期模式,具体的策略如下:
- Transient和Scoped:所有实现了IDisposable接口的服务实例会被当前IServiceProvider对象保存起来,当IServiceProvider对象的Dispose方法被调用的时候,这些服务实例的Dispose方法会随之被调用。
- Singleton:由于服务实例保存在作为根容器的IServiceProvider对象上,只有当后者的Dispose方法被调用的时候,这些服务实例的Dispose方法才会随之被调用。
对于一个ASP.NET Core应用来说,它具有一个与当前应用绑定代表全局根容器的IServiceProvider对象。对于处理的每一次请求,ASP.NET Core框架都会利用这个根容器来创建基于当前请求的服务范围,并利用后者提供的IServiceProvider对象来提供请求处理所需的服务实例。请求处理完成之后,创建的服务范围被终结,对应的IServiceProvider对象也随之被释放,此时由它提供的Scoped服务实例以及实现了IDisposable接口的Transient服务实例得以及时释放。
上述的释放策略可以通过如下的演示实例来印证。我们在如下的代码片段中创建了一个ServiceCollection对象,并针对不同的生命周期模式添加了针对IFoo、IBar和IBaz的服务注册。在利用ServiceCollection创建出作为根容器的IServiceProvider之后,我们调用它的CreateScope方法创建出对应的服务范围。接下来我们利用创建的服务范围得到代表子容器的IServiceProvider对象,并用它提供了三个注册服务对应的实例。
class Program { static void Main() { using (var root = new ServiceCollection() .AddTransient<IFoo, Foo>() .AddScoped<IBar, Bar>() .AddSingleton<IBaz, Baz>() .BuildServiceProvider()) { using (var scope = root.CreateScope()) { var provider = scope.ServiceProvider; provider.GetService<IFoo>(); provider.GetService<IBar>(); provider.GetService<IBaz>(); Console.WriteLine("Child container is disposed."); } Console.WriteLine("Root container is disposed."); } } }
由于代表根容器的IServiceProvider对象和服务范围的创建都是在using块中进行的,所有针对它们的Dispose方法都会在using块结束的地方被调用。为了确定方法被调用的时机,我们特意在控制台上打印了相应的文字。该程序运行之后会在控制台上输出如下图所示的结果,我们可以看到当作为子容器的IServiceProvider对象被释放的时候,由它提供的两个生命周期模式分别为Transient和Scoped的两个服务实例(Foo和Bar)被正常释放了。至于生命周期模式为Singleton的服务实例Baz,它的Dispose方法会延迟到作为根容器的IServiceProvider对象被释放的时候。
三、针对服务注册的验证
Singleton和Scoped这两种不同的生命周期是通过将提供的服务实例分别存放到作为根容器的IServiceProvider对象和当前IServiceProvider对象来实现的,这意味着作为根容器的IServiceProvider对象提供的Scoped服务实例也是单例的。如果某个Singleton服务依赖另一个Scoped服务,那么Scoped服务实例将被一个Singleton服务实例所引用,意味着Scoped服务实例也成了一个Singleton服务实例。
在ASP.NET Core应用中,我们将某个服务注册的生命周期设置为Scoped的真正意图是希望依赖注入容器根据每个接收的请求来创建和释放服务实例,但是一旦出现上述这种情况,意味着Scoped服务实例将变成一个Singleton服务实例,这样的Scoped服务实例会直到应用关闭的那一刻才会被释放,这无疑不是我们希望得到的结果。如果某个Scoped服务实例引用的资源(比如数据库连接)需要被及时释放,这可能会对应用造成灭顶之灾。为了避免这种情况的出现,我们在利用IServiceProvider提供服务过程中可以开启针对服务范围的验证。
如果希望IServiceProvider在提供服务的过程中对服务范围作有效性检验,我们只需要在调用IServiceCollection的BuildServiceProvider扩展方法的时候将一个布尔类型的True值作为参数即可。在如下所示的演示程序中,我们定义了两个服务接口(IFoo和IBar)和对应的实现类型(Foo和Bar),其中Foo依赖IBar。我们将IFoo和IBar分别注册为Singleton和Scoped服务,当调用BuildServiceProvider方法创建代表依赖注入容器的IServiceProvider对象的时候,我们将参数设置为True以开启针对服务范围的检验。我们最后分别利用代表根容器和子容器的IServiceProvider来提供这两种类型的服务实例。
class Program { static void Main() { var root = new ServiceCollection() .AddSingleton<IFoo, Foo>() .AddScoped<IBar, Bar>() .BuildServiceProvider(true); var child = root.CreateScope().ServiceProvider; void ResolveService<T>(IServiceProvider provider) { var isRootContainer = root == provider ?