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

[301]普通服务的注册和提取(源代码

[302]针对泛型服务类型的支持(源代码

[303]为同一类型提供多个服务注册(源代码

[304]服务实例的生命周期(源代码

[305]服务实例的释放回收(源代码

[306]服务范围的验证(源代码

[307]服务注册有效性的验证(源代码

[301]普通服务的注册和提取

我们提供的演示实例是一个控制台程序。在添加了“Microsoft.Extensions.DependencyInjection”NuGet包引用之后,我们定义了如下接口和实现类型来表示相应的服务。如代码片段所示,Foo、Bar和Baz分别实现了对应的接口IFoo、IBar与IBaz。它们派生的基类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 T1 Foo { get; }
public T2 Bar { get; }
public Foobar(T1 foo, T2 bar)
{
Foo = foo;
Bar = bar;
}
}

在如下所示的演示程序中,我们创建了一个ServiceCollection对象(ServiceCollection实现了IServiceCollection接口),并现有调用AddTransient、AddScoped和AddSingleton扩展方法针对IFoo、IBar和IBaz接口注册了对应的服务,从方法命名可以看出注册的服务采用的生命周期模式分别为Transient、Scoped和Singleton。我们接下来调用IServiceCollection对象的BuildServiceProvider扩展方法创建出代表依赖注入容器的IServiceProvider对象,并调用它的GetService<T>扩展方法来提供所需的服务实例。

using App;
using Microsoft.Extensions.DependencyInjection;
using System.Diagnostics; 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);

[302]针对泛型服务类型的支持

表示依赖注入容器的IServiceProvider对象还能提供泛型服务实例。如下面的代码片段所示,在为创建的ServiceCollection对象添加了针对IFoo和IBar接口的服务注册之后,我们调用AddTransient方法注册了针对泛型定义IFoobar<,>的服务(实现的类型为Foobar<,>)。在构建出代表依赖注入容器的IServiceProvider对象之后,我们利用它提供一个类型为IFoobar<IFoo, IBar>的服务实例(S302)。

using App;
using Microsoft.Extensions.DependencyInjection;
using System.Diagnostics; 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);

[303]为同一类型提供多个服务注册

我们可以为同一个类型添加多个服务注册,虽然所有服务注册均是有效的,但是GetService<T>扩展方法只能返回一个服务实例。框架采用了“后来居上”的策略,总是采用最近添加的服务注册来创建服务实例。GetServices<TService>扩展方法将利用指定服务类型的所有服务注册来提供一组服务实例。需要的演示程序添加了三个针对Base类型的服务注册,对应的实现类型分别为Foo、Bar和Baz。我们将Base作为泛型参数调用了GetServices<Base>方法,返回的集合将包含这三个类型的对象。

using App;
using Microsoft.Extensions.DependencyInjection;
using System.Diagnostics;
using System.Linq; 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());

[304]服务实例的生命周期

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

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

using App;
using Microsoft.Extensions.DependencyInjection; 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>();
}

演示程序启动后会在控制台上输出如图1所示的结果。由于IFoo服务被注册为Transient服务,所以四次服务获取请求都会创建一个新的Foo对象。IBar服务的生命周期模式为Scoped,同一个IServiceProvider对象只会创建一个Bar对象,所以整个过程中会创建两个Bar对象。IBaz服务采用Singleton生命周期,具有同根的两个IServiceProvider对象提供的是同一个Baz对象。


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

[305]服务实例的释放回收

作为依赖注入容器的IServiceProvider对象不仅用来构建并提供服务实例,还负责管理这服务实例的生命周期。如果某个服务实例的类型实现了IDisposable接口,就意味着当生命周期完结的时候需要调用Dispose方法执行一些资源释放操作,针对服务实例的释放同样由IServiceProvider对象来负责。框架针对提供服务实例的释放策略取决于采用的生命周期模式,具体的策略如下。

  • Transient和Scoped:所有实现了IDisposable接口的服务实例会被当前IServiceProvider对象保存起来,当IServiceProvider对象的Dispose方法被调用的时候,这些服务实例的Dispose方法会随之被调用。
  • Singleton:服务实例保存在作为根容器的IServiceProvider对象上,只有当后者的Dispose方法被调用的时候,这些服务实例的Dispose方法才会随之被调用。

ASP.NET Core应用具有一个代表根容器的IServiceProvider对象,由于它与应用具有一致的生命周期而被称为ApplicationServices。对于处理的每一次请求,应用都会利用这个根容器来创建基于当前请求的服务范围,该服务范围所在的IServiceProvider对象被称为RequestServices,处理请求所需的服务实例均由它来提供。请求处理完成之后,创建的服务范围被终结,RequestServices也随之被释放,此时在当前请求范围内创建的Scoped服务实例和实现了IDisposable接口的Transient服务实例得以及时释放。

上述释放策略可以通过如下演示实例进行印证。如代码片段所示,我们并采用不同的生命周期模式添加了针对IFoo、IBar和IBaz的服务注册。在作为根容器的IServiceProvider对象被构建出来后,可以调用其CreateScope方法创建出对应的服务范围。我们利用服务范围所在的IServiceProvider对象提供了三个对应的实例。

using App;
using Microsoft.Extensions.DependencyInjection; 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 服务实例的释放

[306]服务范围的验证

Singleton和Scoped这两种不同的生命周期是通过将提供的服务实例分别存放到作为根容器的IServiceProvider对象和当前IServiceProvider对象来实现的,这意味着作为根容器的IServiceProvider对象提供的Scoped服务实例也是单例的。如果某个Singleton服务依赖另一个Scoped服务,那么Scoped服务实例将被一个Singleton服务实例所引用,也就意味着Scoped服务实例也成了一个Singleton服务实例。在ASP.NET Core应用中,我们一般只会将于请求具有一致生命周期的服务注册为Scope模式。一旦出现上述这种情况,就意味着Scoped服务实例将变成一个Singleton服务实例,这基本上不是我们希望看到的结果,这极有可能造成严重的内存泄露问题。为了避免这种情况的发生,框架提供了相应的验证机制。

如果希望IServiceProvider对象在提供服务时针对服务范围作有效性检验,我们只需要在调用IServiceCollection接口的BuildServiceProvider扩展方法时提供一个值为True作为参数即可。下面的演示程序定义了两个服务接口(IFoo和IBar)和对应的实现类型(Foo和Bar),其中,Foo需要依赖IBar。如果将IFoo和IBar分别注册为Singleton服务与Scoped服务,当调用BuildServiceProvider方法创建代表依赖注入容器的IServiceProvider对象的时候将validateScopes参数设置为True即可。下面这个实例演示了这种验证方式。

using App;
using Microsoft.Extensions.DependencyInjection; var root = new ServiceCollection()
.AddSingleton<IFoo, Foo>()
.AddScoped<IBar, Bar>()
.BuildServiceProvider(true);
var child = root.CreateScope().ServiceProvider; ResolveService<IFoo>(root);
ResolveService<IBar>(root);
ResolveService<IFoo>(child);
ResolveService<IBar>(child); 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}");
}
}
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针对服务范围的检验

[307]服务注册有效性的验证

针对服务范围的检验体现在ServiceProviderOptions配置选项的ValidateScopes属性上。如下面的代码片段所示,ServiceProviderOptions还具有另一个名为ValidateOnBuild的属性。如果将该属性设置为True,就意味着IServiceProvider对象被构建的时候会对每个ServiceDescriptor对象实施有效性验证。

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属性来源于内嵌方法的同名参数。

using App;
using Microsoft.Extensions.DependencyInjection; 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具有唯一的私有构造函数,而提供的服务注册并不能将服务实例创建出来,所以这个服务注册是无效的。由于在默认情况下构建IServiceProvider对象的时候并不会对服务注册做有效性检验,所以此时无效的服务注册并不会及时被探测到。一旦将ValidateOnBuild选项设置为True,IServiceProvider对象在被构建的时候就会抛出异常,图4所示的输出结果就体现了这一点。


图4 构建IServiceProvider对象针对服务注册有效性的检验

ASP.NET Core 6框架揭秘实例演示[05]:依赖注入基本编程模式的更多相关文章

  1. ASP.NET Core 6框架揭秘实例演示[10]:Options基本编程模式

    依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式注入消费该功能的组件或者服务中.除了可以采用依赖注入的形式消费承载某种功能的服务,还可以采用相同的方式消费承载配置数据的Options对 ...

  2. ASP.NET Core 6框架揭秘实例演示[07]:文件系统

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

  3. ASP.NET Core 6框架揭秘实例演示[08]:配置的基本编程模式

    .NET的配置支持多样化的数据源,我们可以采用内存的变量.环境变量.命令行参数.以及各种格式的配置文件作为配置的数据来源.在对配置系统进行系统介绍之前,我们通过几个简单的实例演示一下如何将具有不同来源 ...

  4. ASP.NET Core 6框架揭秘实例演示[09]:配置绑定

    我们倾向于将IConfiguration对象转换成一个具体的对象,以面向对象的方式来使用配置,我们将这个转换过程称为配置绑定.除了将配置树叶子节点配置节的绑定为某种标量对象外,我们还可以直接将一个配置 ...

  5. ASP.NET Core 6框架揭秘实例演示[11]:诊断跟踪的几种基本编程方式

    在整个软件开发维护生命周期内,最难的不是如何将软件系统开发出来,而是在系统上线之后及时解决遇到的问题.一个好的程序员能够在系统出现问题之后马上定位错误的根源并找到正确的解决方案,一个更好的程序员能够根 ...

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

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

  7. ASP.NET Core 6框架揭秘实例演示[13]:日志的基本编程模式[上篇]

    <诊断跟踪的几种基本编程方式>介绍了四种常用的诊断日志框架.其实除了微软提供的这些日志框架,还有很多第三方日志框架可供我们选择,比如Log4Net.NLog和Serilog 等.虽然这些框 ...

  8. ASP.NET Core 6框架揭秘实例演示[14]:日志的进阶用法

    为了对各种日志框架进行整合,微软创建了一个用来提供统一的日志编程模式的日志框架.<日志的基本编程模式>以实例演示的方式介绍了日志的基本编程模式,现在我们来补充几种"进阶" ...

  9. ASP.NET Core 6框架揭秘实例演示[15]:针对控制台的日志输出

    针对控制台的ILogger实现类型为ConsoleLogger,对应的ILoggerProvider实现类型为ConsoleLoggerProvider,这两个类型都定义在 NuGet包"M ...

随机推荐

  1. 第10组 Beta冲刺 (1/5)(组长)

    1.1基本情况 ·队名:今晚不睡觉 ·组长博客:https://www.cnblogs.com/cpandbb/p/14012521.html ·作业博客:https://edu.cnblogs.co ...

  2. 基于 Keras 实现图像风格转移

     Style Transfer 这个方向火起来是从2015年Gatys发表的Paper A Neural Algorithm of Artistic Style(神经风格迁移) , 这里就简单提一下论 ...

  3. Android官方文档翻译 十七 4.1Starting an Activity

    Starting an Activity 开启一个Activity This lesson teaches you to 这节课教给你 Understand the Lifecycle Callbac ...

  4. MATLAB中拟合算法刚入门

    %%%1.拟合问题:(做预测,主要使用的范围是样本比较小,拟合效果会好,样本比较多,拟合的效果就不是很好) 1.应用预测的场景:已经知道10年的样本,预测第11年以内的数据 2.用拟合的到关系式:样本 ...

  5. 打印十字码 DataMatrix

    nuget 安装 DataMatrix.net //示例 DmtxImageEncoder Die = new DmtxImageEncoder(); DataMatrix.net.DmtxImage ...

  6. 【记录一个问题】opencv中使用UMat类,在析构的时候出现refcount == 0的断言错误

    发生coredump时的错误信息如下: terminate called after throwing an instance of 'cv::Exception' what(): OpenCV(4. ...

  7. DNS主从同步部署

    DNS 主从同步原理 主从同步:主每次修改配置文件需要修改一下序列号,主从同步主要 根据序列号的变化. 从DNS:从可以单独修改,主从不会报错.但从修改后,主端同步给从后 从端修改数据会丢失 主从原理 ...

  8. 使用Termux并与ubuntu建立ssh连接

    什么是Termux? Termux是一个Android终端仿真器和Linux环境应用程序,直接工作,无需根目录或设置.一个最小的基本系统被自动安装-额外的软件包可以使用APT软件包管理器来使用.不需要 ...

  9. es的settings设置详解

    //静态设置:只能在索引创建时或者在状态为 closed index(闭合的索引)上设置   index.number_of_shards //主分片数,默认为5.只能在创建索引时设置,不能修改   ...

  10. [源码解析] 分布式训练Megatron (1) --- 论文 & 基础

    [源码解析] 分布式训练Megatron (1) --- 论文 & 基础 目录 [源码解析] 分布式训练Megatron (1) --- 论文 & 基础 0x00 摘要 0x01 In ...