本系列文章旨在剖析.NET Core的依赖注入框架的实现原理,到目前为止我们通过三篇文章(《控制反转》、《基于IoC的设计模式》和《 依赖注入模式》)从纯理论的角度对依赖注入进行了深入论述,为了让读者朋友能够更好地理解.NET Core的依赖注入框架的设计思想和实现原理,我们创建了一个简易版本的DI框架,也就是我们在前面文章中多次提及的Cat。我们会上下两篇来介绍这个被称为为Cat的DI框架,上篇介绍编程模型,下篇关注设计实现。[源代码从这里下载]

目录
一、DI容器的层次结构与服务实例生命周期

二、服务的注册于提取

三、提供泛型服务

四、多服务实例的提取

五、服务实例的释放回收

一、DI容器的层次结构与服务实例生命周期

虽然我们对这个名为Cat的DI框架进行了最大限度的简化,但是与.NET Core的真实DI框架相比,Cat不仅采用了一致的设计,而且几乎具备了后者所有的功能特性。作为DI容器的Cat对象不仅仅是作为服务实例的提供者,它同时还需要维护提供服务实例的生命周期。Cat提供了三种生命周期模式,如果要了解它们之间的差异,就必需对多个Cat之间的层次关系有充分的认识。一个代表DI容器的Cat用以来创建多个新的Cat对象,后者视前者为“父容器”,所以多个Cat对象通过其“父子关系”维系一个树形层次化结构。不过着仅仅是一个逻辑结构而已,实际上每个Cat对象只会按照图1所示的方式引用整棵树的


图1 Cat之间的关系

在了解了代表DI容器的多个Cat对象之间的关系之后,对于三种预定义的生命周期模式就很好理解了。如下所示的Lifetime枚举代表着三种生命周期模式,其中Transient代表容器针对每次服务请求都会创建一个新的服务实例,它代表一种“即用即取,用完即弃”的消费方式;而Self则是将提供服务实例保存在当前容器中,它代表针对某个容器的单例模式; Root则是将每个容器提供的服务实例统一存放到根容器中,所以该模式能够在多个“同根”容器范围内确保提供的服务是单例的。

public enum Lifetime
{
Root,
Self,
Transient
}

代表DI容器的Cat对象为我们提供所需服务实例的前提是相应的服务注册已经在此之前已经添加到容器之中。服务总是针对服务类型(接口、抽象类或者具体类型)来注册的,Cat通过定义的扩展方法提供了如下三种注册方式。除了以指定服务实例的形式外(默认采用Root模式),我们在注册服务的时候必须指定一个具体的生命周期模式。

  • 指定注册服务的实现类型;
  • 指定一个现有的服务实例;
  • 指定一个创建服务实例的工厂。

二、服务的注册于提取

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

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;
}
}

在如下所示的代码片段中我们创建了一个Cat对象并采用上面提到的方式针对接口IFoo、IBar和IBaz注册了对应的服务,它们采用的生命周期模式分别为Transient、Self和Root。接下来我们利用Cat对象创建了它的两个子容器,并利用调用后者的GetService<T>方法来提供相应的服务实例。

class Program
{
static void Main()
{
var root = new Cat()
.Register<IFoo, Foo>(Lifetime.Transient)
.Register<IBar>(_=> new Bar(), Lifetime.Self)
.Register<IBaz, Baz>( Lifetime.Root);
var cat1 = root.CreateChild();
var cat2 = root.CreateChild(); void GetServices<TService>(Cat cat)
{
cat.GetService<TService>();
cat.GetService<TService>();
} GetServices<IFoo>(cat1);
GetServices<IBar>(cat1);
GetServices<IBaz>(cat1);
Console.WriteLine();
GetServices<IFoo>(cat2);
GetServices<IBar>(cat2);
GetServices<IBaz>(cat2);
}
}

上面的程序运行之后会在控制台上输出如图2所示的结果,输出的内容不仅表明Cat能够根据添加的服务注册提供对应类型的服务实例,还体现了它对生命周期的控制。由于IFoo被注册为Transient服务,所以Cat针对该接口类型的四次请求都会创建一个全新的Foo对象。IBar服务的生命周期模式为Self,如果我们利用同一个Cat对象来提供对应的服务实例,该Cat对象只会创建一个Bar对象,所以整个程序执行过程中会创建两个Bar对象。IBaz服务采用Root生命周期,所以具有同根的两个Cat对象提供的总是同一个Baz对象,后者只会被创建一次。


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

三、提供泛型服务

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

var cat = new Cat()
.Register<IFoo, Foo>(Lifetime.Transient)
.Register<IBar, Bar>(Lifetime.Transient)
.Register(typeof(IFoobar<,>), typeof(Foobar<,>), Lifetime.Transient); var foobar = (Foobar<IFoo, IBar>)cat.GetService<IFoobar<IFoo, IBar>>();
Debug.Assert(foobar.Foo is Foo);
Debug.Assert(foobar.Bar is Bar);

四、多服务实例的提取

当我们在进行服务注册的时候,可以为同一个类型添加多个服务注册。不过由于扩展方法GetService<T>总是返回一个唯一的服务实例,我们对该方法采用了“后来居上”的策略,即总是采用最近添加的服务注册来创建服务实例。如果我们调用另一个扩展方法GetServices<T>,它将利用返回所有服务注册提供的服务实例。

如下面的代码片段所示,我们为创建的Cat对象添加了三个针对Base类型的服务注册,对应的实现类型分别为Foo、Bar和Baz。我们最后将Base作为泛型参数调用了GetServices<Base>方法,该方法会返回包含三个Base对象的集合,集合元素的类型分别为Foo、Bar和Baz。

var services = new Cat()
.Register<Base, Foo>(Lifetime.Transient)
.Register<Base, Bar>(Lifetime.Transient)
.Register<Base, Baz>(Lifetime.Transient)
.GetServices<Base>();
Debug.Assert(services.OfType<Foo>().Any());
Debug.Assert(services.OfType<Bar>().Any());
Debug.Assert(services.OfType<Baz>().Any());

五、服务实例的释放回收

如果提供的服务实例实现了IDisposable接口,我们应该适当的时候调用其Dispose方法释放该服务实例。由于服务实例的生命周期完全由作为DI容器的Cat对象来管理,通过调用Dispose方法来释放服务实例自然也应该由它来负责。Cat针对提供服务实例的释放策略取决于对应的服务注册采用的生命周期模式,具体的策略如下:

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

  • Root:由于服务实例保存在作为根容器的Cat对象上,所以后者的Dispose方法的调用会触发针对服务实例的释放。

上述的释放策略可以通过如下的演示实例来印证。我们在如下的代码片段中创建了一个Cat对象,并添加了针对IFoo、IBar和IBaz的服务注册。接下来我们调用了CreateChild方法创建代码子容器的Cat对象,并用后者提供了三个注册服务对应的实例。

class Program
{
static void Main()
{
using (var root = new Cat()
.Register<IFoo, Foo>(Lifetime.Transient)
.Register<IBar, Bar>(Lifetime.Self)
.Register<IBaz, Baz>(Lifetime.Root))
{
using (var cat = root.CreateChild())
{
cat.GetService<IFoo>();
cat.GetService<IBar>();
cat.GetService<IBaz>();
Console.WriteLine("Child cat is disposed.");
}
Console.WriteLine("Root cat is disposed.");
}
}
}

由于两个Cat对象的创建都是在using块中进行的,所有针对它们的Dispose方法都会在using块结束的地方被调用,为了确定方法被调用的时机,我们特意在控制台上打印了相应的文字。该程序运行之后会在控制台上输出如图3所示的结果,我们可以看到当作为子容器的Cat对象的Dispose方法被调用的时候,由它提供的两个生命周期模式分别为Transient和Self的两个服务实例(Foo和Bar)被正常释放了。至于生命周期模式为Root的服务实例Baz,它的Dispose方法会延迟到作为根容器Cat对象被释放的时候。


图3 Root服务实例的释放

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

依赖注入[4]: 创建一个简易版的DI框架[上篇]的更多相关文章

  1. .NET CORE学习笔记系列(2)——依赖注入[4]: 创建一个简易版的DI框架[上篇]

    原文https://www.cnblogs.com/artech/p/net-core-di-04.html 本系列文章旨在剖析.NET Core的依赖注入框架的实现原理,到目前为止我们通过三篇文章从 ...

  2. 依赖注入[5]: 创建一个简易版的DI框架[下篇]

    为了让读者朋友们能够对.NET Core DI框架的实现原理具有一个深刻而认识,我们采用与之类似的设计构架了一个名为Cat的DI框架.在<依赖注入[4]: 创建一个简易版的DI框架[上篇]> ...

  3. .NET CORE学习笔记系列(2)——依赖注入[5]: 创建一个简易版的DI框架[下篇]

    为了让读者朋友们能够对.NET Core DI框架的实现原理具有一个深刻而认识,我们采用与之类似的设计构架了一个名为Cat的DI框架.在上篇中我们介绍了Cat的基本编程模式,接下来我们就来聊聊Cat的 ...

  4. [ASP.NET Core 3框架揭秘] 依赖注入[4]:一个Mini版的依赖注入框架

    在前面的章节中,我们从纯理论的角度对依赖注入进行了深入论述,我们接下来会对.NET Core依赖注入框架进行单独介绍.为了让读者朋友能够更好地理解.NET Core依赖注入框架的设计与实现,我们按照类 ...

  5. 使用 js 实现一个简易版的 vue 框架

    使用 js 实现一个简易版的 vue 框架 具有挑战性的前端面试题 refs https://www.infoq.cn/article/0NUjpxGrqRX6Ss01BLLE xgqfrms 201 ...

  6. 实现一个简易版的SpringMvc框架

    先来看我们的程序入口DispatcherServlet 主要核心处理流程如下: 1 扫描基础包下的带有controller 以及 service注解的class,并保存到list中 2 对第一步扫描到 ...

  7. 如何实现一个简易版的 Spring - 如何实现 Setter 注入

    前言 之前在 上篇 提到过会实现一个简易版的 IoC 和 AOP,今天它终于来了...相信对于使用 Java 开发语言的朋友们都使用过或者听说过 Spring 这个开发框架,绝大部分的企业级开发中都离 ...

  8. 如何实现一个简易版的 Spring - 如何实现 Constructor 注入

    前言 本文是「如何实现一个简易版的 Spring」系列的第二篇,在 第一篇 介绍了如何实现一个基于 XML 的简单 Setter 注入,这篇来看看要如何去实现一个简单的 Constructor 注入功 ...

  9. .NET Core的文件系统[5]:扩展文件系统构建一个简易版“云盘”

    FileProvider构建了一个抽象文件系统,作为它的两个具体实现,PhysicalFileProvider和EmbeddedFileProvider则分别为我们构建了一个物理文件系统和程序集内嵌文 ...

随机推荐

  1. Python——类与对象,异常处理

    类 class C1: def setdata(self,value): self.data = value def display(self): print(self.data) class C2( ...

  2. Asp.Net Core 项目搭建基础

    很多新接触ASP.NET Core 技术的同学可能会对项目运行机制不了解,会碰到服务在哪添加?路由在哪配置?中间件怎么使用?依赖注入在哪写?诸如此类的问题.同样作为初学者,以下是本人在学习.Net技术 ...

  3. MACE环境搭建

    主要参考https://blog.csdn.net/u012505617/article/details/85763065 1.安装docker 2.安装NDK https://www.linuxid ...

  4. 部署Mvc Core SSL网站到Centos并用Nginx作为反向代理

    1. 先在本地比如~/Downloads下建立MVC项目 2. 生成mvc使用的ssl证书
 2.1. 生成.key文件
 openssl genrsa -des3 -out server.key 2 ...

  5. RESTful-3架构详解

    1. 什么是REST REST全称是Representational State Transfer,中文意思是表述(编者注:通常译为表征)性状态转移. 它首次出现在2000年Roy Fielding的 ...

  6. C语言第01次作业--顺序、分支结构

    1.本章学习总结 1.1 思维导图 1.2本章学习体会及代码量学习体会 1.2.1学习体会 本周我学到了很多C语言中基础的结构和语法(见思维导图),能对一些生活中的简单问题对应编写程序解决一些这些简单 ...

  7. Aspnet Mvc 前后端分离项目手记(二)关于token认证

    在前后端分离的项目中,首先我们要解决的问题就是身份认证 以往的时候,我们使用cookie+session,或者只用cookie来保持会话. 一,先来复习一下cookie和session 首先我们来复习 ...

  8. #工具 Intellij IDEA中自定义的Maven Archetype管理

    背景,手贱在输入自定义的 archetype时后面多输入了一个空格 解决:自定义的Archetype 会保存在Windows下面的文件中 C:\Users\<user>\.IntelliJ ...

  9. kubernates使用kubeadm安装

    kubeadm是Kubernetes官方提供的用于快速安装Kubernetes集群的工具,伴随Kubernetes每个版本的发布都会同步更新,kubeadm会对集群配置方面的一些实践做调整,通过实验k ...

  10. box-shadow 画叮当猫

    值 描述 h-shadow 必需.水平阴影的位置.允许负值 v-shadow 必需.垂直阴影的位置.允许负值 blur 可选.模糊距离 spread 可选.阴影的尺寸 color 可选.阴影的颜色.请 ...