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

毫不夸张地说,整个ASP.NET Core框架是建立在依赖注入框架之上的。ASP.NET Core应用在启动时构建管道以及利用该管道处理每个请求过程中使用到的服务对象均来源于依赖注入容器。该依赖注入容器不仅为ASP.NET Core框架自身提供必要的服务,同时也是应用程序的服务提供者,依赖注入已经成为了ASP.NET Core应用的基本编程模式。

一、服务的注册与消费

为了让读者朋友们能够更加容易地认识.NET Core提供的依赖注入框架,我在“《一个迷你版DI框架》”中特意创建了一个名为Cat的迷你版依赖注入框架。不论是编程模式还是实现原理,Cat与我们即将介绍的依赖注入框架都非常相似。这个依赖注入框架主要涉及两个NuGet包,我们在编程过程中频繁使用的一些接口和基础数据类型都定义在NuGet包“Microsoft.Extensions.DependencyInjection.Abstractions”中,而依赖注入的具体实现则由“Microsoft.Extensions.DependencyInjection”这个NuGet包来承载。

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

作为依赖注入容器的IServiceProvider对象不仅具有类似于Cat的层次结构,两者对提供的服务实例也采用一致的生命周期管理方式。依赖注入框架利用如下这个枚举ServiceLifetime来表示Singleton、Scoped和Transient三种生命周期模式,我在Cat中则将其命名为Root、Self和Transient,前者命名关注于现象,后者则关注于内部实现。

public enum ServiceLifetime
{
Singleton,
Scoped,
Transient
}

应用程序初始化过程中添加的服务注册是依赖注入容器用来提供所需服务实例的依据。由于IServiceProvider对象总是利用指定的服务类型来提供对应服务实例,所以服务总是基于类型进行注册。我们倾向于利用接口来对服务进行抽象,所以这里的服务类型一般为接口,但是依赖注入框架对服务注册的类型并没有任何限制。具体的服务注册主要体现为如下三种形式,除了直接提供一个服务实例的注册形式之外(这种形式默认采用Singleton模式),我们在注册服务的时候必须指定一个具体的生命周期模式。

  • 指定具体的服务实现类型。
  • 提供一个现成的服务实例。
  • 指定一个创建服务实例的工厂。

我们的演示实例时一个普通的控制台应用。由于“Microsoft.Extensions.DependencyInjection”这个NuGet包承载了整个依赖注入框架的实现,所以我们应该添加该NuGet包的依赖。由于是ASP.NET Core框架的基础NuGet包之一,所以我们可以通过修改项目文件并按照如下的方式添加针对“Microsoft.AspNetCore.App”的框架引用(FrameworkReference)来引入该NuGet包。对于后续部分中采用 “Microsoft.NET.Sdk”作为SDK的演示实例,如果未作说明,在默认采用这种方式添加所需NuGet包的依赖。

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>

在添加了针对“Microsoft.Extensions.DependencyInjection”这个NuGet包的依赖之后,我们定义了如下这些接口和实现类型来表示相应的服务。如下面的代码片段所示,Foo、Bar和Baz分别实现了对应的接口IFoo、IBar和IBaz。为了反映DI框架对服务实例生命周期的控制,我们让它们派生于同一个基类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创建出代表依赖注入容器的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这样普通的服务实例之外,IServiceProvider对象同样也能提供泛型服务实例。如下面的代码片段所示,在为创建的ServiceCollection对象添加了针对IFoo和IBar接口的服务注册之后,我们调用AddTransient方法注册了针对泛型定义IFoobar<,>的服务注册(实现的类型为Foobar<,>)。当我们利用ServiceCollection创建出代表依赖注入容器的IServiceProvider对象并由它提供一个类型为IFoobar<IFoo, IBar>的服务实例的时候,它会创建并返回一个Foobar<Foo, Bar>对象。

public class Program
{
public static void Main()
{
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<TService>,它将利用返回根据所有服务注册提供的服务实例。如下面的代码片段所示,我们为创建的ServiceCollection对象添加了三个针对Base类型的服务注册,对应的实现类型分别为Foo、Bar和Baz。我们最后将Base作为泛型参数调用了GetServices<Base>方法,该方法会返回包含三个Base对象的集合,集合元素的类型分别为Foo、Bar和Baz。

public class Program
{
public static void Main()
{
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对象。这一特性意味着我们可以将代表依赖注入容器的IServiceProvider作为服务进行注入,这一特性体现在如下所示的调试断言中。但是在上一章已经提到过,一旦我们在应用中利用注入的IServiceProvider来获取其他依赖的服务实例,意味着我们在使用“Service Locator”模式。这是一种“反模式(Anti-Pattern)”,当我们的应用程序出现了这样的代码时,最好多想想是否真的需要这么做。

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

二、生命周期

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

接下来我们通过对前面演示的实例略作修改来演示三种不同生命周期模式的差异。在如下所示的代码片段中我们创建了一个ServiceCollection对象并针对接口IFoo、IBar和IBaz注册了对应的服务,它们采用的生命周期模式分别为Transient、Scoped和Singleton。在利用ServiceCollection创建出代表依赖注入容器的IServiceProvider对象之后,我们调用其CreateScope方法创建了两个代表“服务范围”的IServiceScope对象,该对象的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; GetServices<IFoo>(provider1);
GetServices<IBar>(provider1);
GetServices<IBaz>(provider1);
Console.WriteLine();
GetServices<IFoo>(provider2);
GetServices<IBar>(provider2);
GetServices<IBaz>(provider2); static void GetServices<T>(IServiceProvider provider)
{
provider.GetService<T>();
provider.GetService<T>();
}
}
}

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

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

作为依赖注入容器的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对象被释放的时候。

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

三、针对服务注册的验证

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 ? "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 {}

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

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

针对服务范围的检验体现在配置选项类型ServiceProviderOptions的ValidateScopes属性上。如下面的代码片段所示,ServiceProviderOptions还具有另一个名为ValidateOnBuild的属性,如果该属性设置为True,意味着IServiceProvider对象被构建的时候会检验提供的每个ServiceDescriptor的有效性,即确保它们最终都具有提供对应服务实例的能力。默认情况下ValidateOnBuild的属性值为False,意味着只有利用IServiceProvider对象来提供我们所需的服务实例的时候,相应的异常采用才会抛出来。

public class ServiceProviderOptions
{
public bool ValidateScopes { get; set; }
public bool ValidateOnBuild { get; set; }
}

我们照例来作一个在构建IServiceProvider对象时检验服务注册有效性的实例。我们定义了如下一个接口IFoobar和对应的实现类型Foobar,由于我们希望采用单例的形式来使用Foobar对象,所以我们为它定义了唯一的私有构造函数。

public interface IFoobar {}
public class Foobar : IFoobar
{
private Foobar() {}
public static readonly Foobar Instance = new Foobar();
}

在如下的演示实例中,我们定义了一个内嵌的BuildServiceProvider方法来完成针对IFoobar/Foobar的服务注册和最终对IServiceProvider对象的创建。当我们在调用扩展方法BuildServiceProvider创建对应IServiceProvider对象时指定了一个ServiceProviderOptions对象,而它的ValidateOnBuild属性来源于内嵌方法的同名参数。

class Program
{
static void Main()
{
BuildServiceProvider(false);
BuildServiceProvider(true); static void BuildServiceProvider(bool validateOnBuild)
{
try
{
var options = new ServiceProviderOptions
{
ValidateOnBuild = validateOnBuild
};
new ServiceCollection()
.AddSingleton<IFoobar, Foobar>()
.BuildServiceProvider(options);
Console.WriteLine(
$"Status: Success; ValidateOnBuild: {validateOnBuild}");
}
catch (Exception ex)
{
Console.WriteLine( $"Status: Fail; ValidateOnBuild: {validateOnBuild}");
Console.WriteLine($"Error: {ex.Message}");
}
}
}
}

由于Foobar只具有一个唯一的私有构造函数,所以内嵌方法BuildServiceProvider提供的服务注册并不能提供我们所需的服务实例,所以这个服务注册是无效的。由于默认情况下构建IServiceProvider对象的时候并不会对服务注册作有效性检验,所以此时无效的服务注册并不会及时被探测到。一旦我们将ValidateOnBuild选项设置为True,IServiceProvider对象在被构建的时候就会抛出异常,如下图所示的输出结果体现了这一点。

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

[ASP.NET Core 3框架揭秘] 依赖注入[1]:控制反转
[ASP.NET Core 3框架揭秘] 依赖注入[2]:IoC模式
[ASP.NET Core 3框架揭秘] 依赖注入[3]:依赖注入模式
[ASP.NET Core 3框架揭秘] 依赖注入[4]:一个迷你版DI框架
[ASP.NET Core 3框架揭秘] 依赖注入[5]:利用容器提供服务
[ASP.NET Core 3框架揭秘] 依赖注入[6]:服务注册
[ASP.NET Core 3框架揭秘] 依赖注入[7]:服务消费
[ASP.NET Core 3框架揭秘] 依赖注入[8]:服务实例的生命周期
[ASP.NET Core 3框架揭秘] 依赖注入[9]:实现概述
[ASP.NET Core 3框架揭秘] 依赖注入[10]:与第三方依赖注入框架的适配

上一篇:一起学 Java(三) 集合框架、数据结构、泛型


下一篇:netconf简介