毫不夸张地说,整个ASP.NET Core框架是建立在一个依赖注入框架之上的,它在应用启动时构建请求处理管道过程中,以及利用该管道处理每个请求过程中使用到的服务对象均来源于DI容器。该DI容器不仅为ASP.NET Core框架提供必要的服务,同时作为了应用的服务提供者,依赖注入已经成为了ASP.NET Core应用基本的编程模式。在前面一系列的文章中,我们主要从理论层面讲述了依赖注入这种设计模式,补充必要的理论基础是为了能够理解与ASP.NET Core框架无缝集成的依赖注入框架的设计原理。我们总是采用“先简单体验,后者深入剖析”来讲述每一个知识点,所以我们利用一些简单的实例从编程层面来体验一下服务注册的添加和服务实例的提取。

一、服务的注册与消费

为了让读者朋友们能够更加容易地认识依赖注入框架的实现原理和编程模式,我在《依赖注入[4]: 创建一个简易版的DI框架[上篇]》和《依赖注入[5]: 创建一个简易版的DI框架[下篇]》自行创建了一个名为Cat的依赖注入框架。不论是编程模式和实现原理,Cat与我们现在即将介绍的依赖注入框架都非常相似,对于后者提供的每一个特性,我们几乎都能在Cat中找到对应物。

我在设计Cat的时候即将它作为提供服务实例的DI容器,也作为了存放服务注册的容器,但是与ASP.NET Core框架集成的这个依赖注入框架则将这两者分离开来。我们添加的服务注册被保存到通过IServiceCollection接口表示的集合之中,基于这个集合创建的DI容器体现为一个IServiceProvider

由于作为DI框架的IServiceProvider具有类似于Cat的层次结构,所以两者对提供的服务实例采用一致的生命周期管理方式。DI框架利用如下这个枚举ServiceLifetime提供了SingletonScopedTransient三种生命周期模式是,我在Cat中则将其命名为RootSelfTransient,前者命名关注于现象,而我则关注于内部实现。

public enum ServiceLifetime
{
Singleton,
Scoped,
Transient
}

应用初始化过程中添加的服务注册是DI容器用于提供所需服务实例的依据。由于IServiceProvider总是利用指定的服务类型来提供对应服务实例,所以服务是基于类型进行注册的,我们倾向于利用接口来对服务进行抽象,所以这里的服务类型一般为接口。除了以指定服务实例的形式外(默认采用Singleton模式),我们在注册服务的时候必须指定一个具体的生命周期模式。

  • 指定注册非服务类型和实现类型;
  • 指定一个现有的服务实例;
  • 指定一个创建服务实例的委托对象。

我们定义了如下的接口和对应的实现类型来演示针对DI框架的服务注册和提取。其中Foo、Bar和Baz分别实现了对应的接口IFoo、IBar和IBaz,为了反映Cat对服务实例生命周期的控制,我们让它们派生于同一个基类Base。Base实现了IDisposable接口,我们在其构造函数和实现的Dispose方法中打印出相应的文字以确定对应的实例何时被创建和释放。我们还定义了一个泛型的接口IFoobar<T1, T2>和对应的实现类Foobar<T1, T2>来演示针对泛型服务实例的提供。

public interface IFoo {}
public interface IBar {}
public interface IBaz {}
public interface IFoobar<T1, T2> {}
public class Base : IDisposable
{
public Base() => Console.WriteLine($"An instance of {GetType().Name} is created.");
public void Dispose() => Console.WriteLine($"The instance of {GetType().Name} is disposed.");
} public class Foo : Base, IFoo, IDisposable { }
public class Bar : Base, IBar, IDisposable { }
public class Baz : Base, IBaz, IDisposable { }
public class Foobar<T1, T2>: IFoobar<T1,T2>
{
public IFoo Foo { get; }
public IBar Bar { get; }
public Foobar(IFoo foo, IBar bar)
{
Foo = foo;
Bar = bar;
}
}

在如下所示的代码片段中我们创建了一个ServiceCollection(它是对IServiceCollection接口的默认实现)对象并调用相应的方法(AddTransient、AddScoped和AddSingleton)针对接口IFoo、IBar和IBaz注册了对应的服务,从方法命名可以看出注册的服务采用的生命周期模式分别为Transient、Scoped和Singleton。在完成服务注册之后,我们调用IServiceCollection接口的扩展方法BuildServiceProvider创建出代表DI容器的IServiceProvider对象,并利用它调用后者的GetService<T>方法来提供相应的服务实例。调试断言表明IServiceProvider提供的服务实例与预先添加的服务注册是一致的。

class Program
{
static void Main()
{
var provider = new ServiceCollection()
.AddTransient<IFoo, Foo>()
.AddScoped<IBar>(_ => new Bar())
.AddSingleton<IBaz, Baz>()
.BuildServiceProvider();
Debug.Assert(provider.GetService<IFoo>() is Foo);
Debug.Assert(provider.GetService<IBar>() is Bar);
Debug.Assert(provider.GetService<IBaz>() is Baz);
}
}

除了提供类似于IFoo、IBar和IBaz这样非泛型服务实例之外,如果具有对应的泛型定义(Generic Definition)的服务注册,IServiceProvider同样也能提供泛型服务实例。如下面的代码片段所示,在为创建的ServiceCollection对象添加了针对IFoo和IBar接口的服务注册之后,我们调用AddTransient方法注册了针对泛型定义IFoobar<,>的服务注册,实现的类型为Foobar<,>。当我们利用ServiceCollection创建出代表DI容器的IServiceProvider对象并利用后者提供一个类型为IFoobar<IFoo, IBar>的服务实例的时候,它会创建并返回一个Foobar<Foo, Bar>对象。

var provider = new ServiceCollection()
.AddTransient<IFoo, Foo>()
.AddTransient<IBar, Bar>()
.AddTransient(typeof(IFoobar<,>), typeof(Foobar<,>))
.BuildServiceProvider(); var foobar = (Foobar<IFoo, IBar>)provider.GetService<IFoobar<IFoo, IBar>>();
Debug.Assert(foobar.Foo is Foo);
Debug.Assert(foobar.Bar is Bar);

当我们在进行服务注册的时候,可以为同一个类型添加多个服务注册,实际上添加的所有服务注册均是有效的。不过由于扩展方法GetService<T>总是返回一个唯一的服务实例,我们对该方法采用了“后来居上”的策略,即总是采用最近添加的服务注册来创建服务实例。如果我们调用另一个扩展方法GetServices<T>,它将利用返回所有服务注册提供的服务实例。如下面的代码片段所示,我们为创建的ServiceCollection对象添加了三个针对Base类型的服务注册,对应的实现类型分别为Foo、Bar和Baz。我们最后将Base作为泛型参数调用了GetServices<Base>方法,该方法会返回包含三个Base对象的集合,集合元素的类型分别为Foo、Bar和Baz。

var services = new ServiceCollection()
.AddTransient<Base, Foo>()
.AddTransient<Base, Bar>()
.AddTransient<Base, Baz>()
.BuildServiceProvider()
.GetServices<Base>();
Debug.Assert(services.OfType<Foo>().Any());
Debug.Assert(services.OfType<Bar>().Any());
Debug.Assert(services.OfType<Baz>().Any());

对于IServiceProvider针对服务实例的提供还具有这么一个细节:如果我们在调用GetService或者GetService<T>方法是将服务类型设置为IServiceProvider接口类型,提供的服务实例实际上就是当前的IServiceProvider对象。这一特性意味着我们可以将代表DI容器的IServiceProvider作为服务进行注入,但是在《依赖注入[3]: 依赖注入模式》已经提到过,一旦我们在应用中利用注入的IServiceProvider来获取其他依赖的服务实例,意味着我们在使用“Service Locator”模式。这是一种“反模式(Anti-Pattern)”,如果迫不得已最好不要这么做。IServiceProvider的这一特性体现在如下所示的调试断言中。

var provider = new ServiceCollection().BuildServiceProvider();
Debug.Assert(provider.GetService<IServiceProvider>() == provider);

二、生命周期管理

IServiceProvider之间的层次结构造就了三种不同的生命周期模式:由于Singleton服务实例保存在作为根容器的IServiceProvider对象上,所以它能够在多个同根IServiceProvider对象之间提供真正的单例保证。Scoped服务实例被保存在当前IServiceProvider上,所以它只能在当前IServiceProvider对象的“服务范围”保证的单例的。没有实现IDisposable接口的Transient服务则采用“即用即取,用后即弃”的策略。

接下来我们通过简单的实例来演示三种不同生命周期模式的差异。在如下所示的代码片段中我们创建了一个ServiceCollection对象并针对接口IFoo、IBar和IBaz注册了对应的服务,它们采用的生命周期模式分别为Transient、Scoped和Singleton。在利用ServiceCollection创建出代表DI容器的IServiceProvider对象之后,我们调用其CreateScope方法创建了两个所谓的“服务范围”,后者的ServiceProvider属性返回一个新的IServiceProvider对象,它实际上是当前IServiceProvider对象的子容器。我们最后利用作为子容器的IServiceProvider对象来提供相应的服务实例。

class Program
{
static void Main()
{
var root = new ServiceCollection()
.AddTransient<IFoo, Foo>()
.AddScoped<IBar>(_ => new Bar())
.AddSingleton<IBaz, Baz>()
.BuildServiceProvider();
var provider1 = root.CreateScope().ServiceProvider;
var provider2 = root.CreateScope().ServiceProvider; void GetServices<TService>(IServiceProvider provider)
{
provider.GetService<TService>();
provider.GetService<TService>();
} GetServices<IFoo>(provider1);
GetServices<IBar>(provider1);
GetServices<IBaz>(provider1);
Console.WriteLine();
GetServices<IFoo>(provider2);
GetServices<IBar>(provider2);
GetServices<IBaz>(provider2);
}
}

上面的程序运行之后会在控制台上输出如图1所示的结果。由于服务IFoo被注册为Transient服务,所以IServiceProvider针对该接口类型的四次请求都会创建一个全新的Foo对象。IBar服务的生命周期模式为Scoped,如果我们利用同一个IServiceProvider对象来提供对应的服务实例,它只会创建一个Bar对象,所以整个程序执行过程中会创建两个Bar对象。IBaz服务采用Singleton生命周期,所以具有同根的两个IServiceProvider对象提供的总是同一个Baz对象,后者只会被创建一次。


图1 IServiceProvider按照服务注册对应的生命周期模式提供服务实例

作为DI容器的IServiceProvider不仅仅为我们提供所需的服务实例,它还帮我们管理者这些服务实例的生命周期。如果某个服务实例实现了IDisposable接口,意味着当生命周期完结的时候需要通过调用Dispose方法执行一些资源释放操作,这些操作同样由提供服务实例的IServiceProvider对象来驱动执行。DI框架针对提供服务实例的释放策略取决于对应的服务注册采用的生命周期模式,具体的策略如下:

  • Transient和Scoped:所有实现了IDisposable接口的服务实例会被作为服务提供者的当前IServiceProvider对象保存起来,当IServiceProvider对象自身被释放的时候,这些服务实例的Dispose方法会随之被调用。

  • Singleton:由于服务实例保存在作为根容器的IServiceProvider对象上,所以后者被释放的时候调用会触发针对服务实例的释放。

对于一个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块结束的地方被调用,为了确定方法被调用的时机,我们特意在控制台上打印了相应的文字。该程序运行之后会在控制台上输出如图2所示的结果,我们可以看到当作为子容器的IServiceProvider对象被释放的时候,由它提供的两个生命周期模式分别为Transient和Scoped的两个服务实例(Foo和Bar)被正常释放了。至于生命周期模式为Singleton的服务实例Baz,它的Dispose方法会延迟到作为根容器IServiceProvider对象被释放的时候。


图2 服务实例的释放

三、服务范围的检验

Singleton和Scoped这两种不同生命周期是通过将提供的服务实例分别存放到作为根容器的IServiceProvider对象和当前IServiceProvider对象来实现,这意味着作为根容器的IServiceProvider对象提供的Scoped服务实例也是不能被释放的。如果某个Singleton服务以来另一个Scoped服务,那么Scoped服务实例将被一个Singleton服务实例所引用,意味着Scoped服务实例也成了一个不会被释放的服务实例。

在ASP.NET Core应用中,当我们将某个服务注册的生命周期设置为Scoped的真正意图是希望DI容器根据请求上下文来创建和释放服务实例,但是一旦出现上述的情况下,意味着Scoped服务实例将变成一个Singleton服务实例,这样的Scoped服务实例直到应用关闭的哪一个才会得到释放。如果某个Scoped服务实例引用的资源(比如数据库连接)需要被及时释放,这可能会对应用造成灭顶之灾。为了避免这种情况下,我们在利用IServiceProvider提供服务过程开启针对服务范围的验证。

如果希望IServiceProvider在提供服务的过程中对服务范围作有效性检验,我们只需要在调用ServiceCollection的BuildServiceProvider方法的时候将一个布尔类型的True值作为参数即可。在如下所示的演示程序中,我们定义了两个服务接口(IFoo和IBar)和对应的实现类型(Foo和Bar),其中Foo依赖IBar。我们将IFoo和IBar分别注册为Singleton和Scoped服务,当我们在调用BuildServiceProvider方法创建代表DI容器的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 ? "Yes" : "No";
try
{
provider.GetService<T>();
Console.WriteLine( $"Status: Success; Service Type: {typeof(T).Name}; Root: {isRootContainer}");
}
catch (Exception ex)
{
Console.WriteLine($"Status: Fail; Service Type: {typeof(T).Name}; Root: {isRootContainer}");
Console.WriteLine($"Error: {ex.Message}");
}
} ResolveService<IFoo>(root);
ResolveService<IBar>(root);
ResolveService<IFoo>(child);
ResolveService<IBar>(child);
}
} public interface IFoo {}
public interface IBar {}
public class Foo : IFoo
{
public IBar Bar { get; }
public Foo(IBar bar) => Bar = bar;
}
public class Bar : IBar {}

上面这个演示实例启动之后将在控制台上输出如图3所示的输出结果。从输出结果可以看出针对四个服务解析,只有一次(使用代表子容器的IServiceProvider提供IBar服务实例)是成功的。这个实例充分说明了一旦开启了针对服务范围的验证,IServiceProvider对象不可能提供以单例形式存在的Scoped服务。


图3 IServiceProvider针对服务范围的检验

依赖注入[1]: 控制反转
依赖注入[2]: 基于IoC的设计模式
依赖注入[3]: 依赖注入模式
依赖注入[4]: 创建一个简易版的DI框架[上篇]
依赖注入[5]: 创建一个简易版的DI框架[下篇]
依赖注入[6]: .NET Core DI框架[编程体验]
依赖注入[7]: .NET Core DI框架[服务注册]
依赖注入[8]: .NET Core DI框架[服务消费]

依赖注入[6]: .NET Core DI框架[编程体验]的更多相关文章

  1. .NET CORE学习笔记系列(2)——依赖注入[6]: .NET Core DI框架[编程体验]

    原文https://www.cnblogs.com/artech/p/net-core-di-06.html 毫不夸张地说,整个ASP.NET Core框架是建立在一个依赖注入框架之上的,它在应用启动 ...

  2. 依赖注入[8]: .NET Core DI框架[服务消费]

    包含服务注册信息的IServiceCollection对象最终被用来创建作为DI容器的IServiceProvider对象.当需要消费某个服务实例的时候,我们只需要指定服务类型调用IServicePr ...

  3. 依赖注入[7]: .NET Core DI框架[服务注册]

    包含服务注册信息的IServiceCollection对象最终被用来创建作为DI容器的IServiceProvider对象.服务注册就是创建出现相应的ServiceDescriptor对象并将其添加到 ...

  4. .NET CORE学习笔记系列(2)——依赖注入[7]: .NET Core DI框架[服务注册]

    原文https://www.cnblogs.com/artech/p/net-core-di-07.html 包含服务注册信息的IServiceCollection对象最终被用来创建作为DI容器的IS ...

  5. .NET CORE学习笔记系列(2)——依赖注入[8]: .NET Core DI框架[服务消费]

    原文:https://www.cnblogs.com/artech/p/net-core-di-08.html 包含服务注册信息的IServiceCollection对象最终被用来创建作为DI容器的I ...

  6. Spring的三大核心思想:IOC(控制反转),DI(依赖注入),AOP(面向切面编程)

    Spring核心思想,IoC与DI详解(如果还不明白,放弃java吧) 1.IoC是什么?    IoC(Inversion of Control)控制反转,IoC是一种新的Java编程模式,目前很多 ...

  7. 浅谈(IOC)依赖注入与控制反转(DI)

    前言:参考了百度文献和https://www.cnblogs.com/liuqifeng/p/11077592.html以及http://www.cnblogs.com/leoo2sk/archive ...

  8. 依赖注入在 dotnet core 中实现与使用:1 基本概念

    关于 Microsoft Extension: DependencyInjection 的介绍已经很多,但是多数偏重于实现原理和一些特定的实现场景.作为 dotnet core 的核心基石,这里准备全 ...

  9. 依赖注入在 dotnet core 中实现与使用:2 使用 Extensions DependencyInjection

    既然是依赖注入容器,必然会涉及到服务的注册,获取服务实例,管理作用域,服务注入这四个方面. 服务注册涉及如何将我们的定义的服务注册到容器中.这通常是实际开发中使用容器的第一步,而容器本身通常是由框架来 ...

随机推荐

  1. No grammar constraints (DTD or XML Schema) referenced in the document.

    问题描述 web.xml 使用 Servlet4.0 版本,No grammar constraints (DTD or XML Schema) referenced in the document. ...

  2. centOS设置开机自启

    原文:https://blog.csdn.net/txz317/article/details/49683439 1.利用 chkconfig 来配置启动级别 在CentOS或者RedHat其他系统下 ...

  3. SoftEther

    sudo apt-get update   sudo wget http://www.softether-download.com/files/softether/v4.25-9656-rtm-201 ...

  4. Docker使用Mysql镜像命令

    本次使用的环境是win10下的hyper-v安装的CentOS7系统 控制台输入命令: docker run -p 3307:3306 --name mysql01 -v $PWD/conf:/etc ...

  5. 快速从一个空虚拟机,空idea打通提交spark

    https://www.cnblogs.com/xxbbtt/p/8143593.html #!/bin/bash # Install Spark on CentOS 7 yum install ja ...

  6. Anaconda的安装与使用

    1. 安装Anaconda(Command Line) 1.1 下载 首先去Anaconda官网查看下载链接,然后通过命令行下载: $ wget https://repo.anaconda.com/a ...

  7. sqlserver 删除表 视图 函数 存储过程

    use tax_ceshiselect 'DROP TABLE '+name from sysobjects where type = 'U'union select 'DROP VIEW '+nam ...

  8. java TripleDES加密

    package com.zhx.base.util; import org.apache.commons.codec.binary.Base64; import javax.crypto.Cipher ...

  9. CheckedTextView文字不居中的问题

    问题:CheckedTextView设置了android:gravity="center",但是不居中 解决方法:添加属性android:textAlignment="c ...

  10. 英语知识积累-D01-body+animal

    My body What's your name? Here are lots of children playing. Are they happy or sad? Who's waving at ...