ASP.NET Core中的依赖注入(2):依赖注入(DI)

参考页面:

http://www.yuanjiaocheng.net/ASPNET-CORE/project-layout.html

http://www.yuanjiaocheng.net/ASPNET-CORE/projectjson.html

http://www.yuanjiaocheng.net/ASPNET-CORE/core-configuration.html

http://www.yuanjiaocheng.net/ASPNET-CORE/core-middleware.html

http://www.yuanjiaocheng.net/ASPNET-CORE/core-exception.html

IoC主要体现了这样一种设计思想:通过将一组通用流程的控制从应用转移到框架之中以实现对流程的复用,同时采用“好莱坞原则”是应用程序以被动的方式实现对流程的定制。我们可以采用若干设计模式以不同的方式实现IoC,比如我们在上面介绍的模板方法、工厂方法和抽象工厂,接下来我们介绍一种更为有价值的IoC模式,即依赖注入(DI:Dependency Injection,以下简称DI)。

目录
一、由外部容器提供服务对象
二、三种依赖注入方式
    构造器注入
    属性注入
    方法注入
三、实例演示:创建一个简易版的DI框架

一、由外部容器提供服务对象

和上面介绍的工厂方法和抽象工厂模式一样,DI旨在实现针对服务对象的动态提供。具体来说,服务的消费者利用一个独立的容器(Container)来获取所需的服务对象,容器自身在提供服务对象的过程中会自动完成依赖的解析与注入。话句话说,由DI容器提供的这个服务对象是一个” 开箱即用”的对象,这个对象自身直接或者间接依赖的对象已经在初始化的工程中被自动注入其中了。

举个简单的例子,我们创建一个名为Cat的DI容器类,那么我们可以通过调用具有如下定义的扩展方法GetService<T>从某个Cat对象获取指定类型的服务对象。我之所以将其命名为Cat,源于我们大家都非常熟悉的一个卡通形象“机器猫(哆啦A梦)”。它的那个四次元口袋就是一个理想的DI容器,大熊只需要告诉哆啦A梦相应的需求,它就能从这个口袋中得到相应的法宝。DI容器亦是如此,服务消费者只需要告诉容器所需服务的类型(一般是一个服务接口或者抽象服务类),就能得到与之匹配的服务对象。

   1: public static class CatExtensions
   2: {  
   3:     public static T GetService<T>(this Cat cat);
   4: }

对于我们在上一篇演示的MVC框架,我们在前面分别采用不同的设计模式对框架的核心类型MvcEngine进行了改造,现在我们采用DI的方式并利用上述的这个Cat容器按照如下的方式对其进行重新实现,我们会发现MvcEngine变得异常简洁而清晰。

   1: public class MvcEngine
   2: {
   3:     public Cat Cat { get; private set; }
   4:  
   5:     public MvcEngine(Cat cat)
   6:     {
   7:         this.Cat = cat;
   8:     }
   9:  
  10:     public void Start(Uri address)
  11:     {
  12:         while (true)
  13:         {
  14:             Request request = this.Cat.GetService<Listener>().Listen(address);
  15:             Task.Run(() =>
  16:             {
  17:                 Controller controller = this.Cat.GetService<ControllerActivator>().ActivateController(request);
  18:                 View view = this.Cat.GetService<ControllerExecutor>().ExecuteController(controller);
  19:                 this.Cat.GetService<ViewRenderer>().RenderView(view);
  20:             });
  21:         }
  22:     }
  23: } 

DI体现了一种最为直接的服务消费方式,消费者只需要告诉生产者(DI容器)关于所需服务的抽象描述,后者根据预先注册的规则提供一个匹配的服务对象。这里所谓的服务描述主要体现为服务接口或者抽象服务类的类型,当然也可以是包含实现代码的具体类型。至于应用程序对由框架控制的流程的定制,则可以通过对DI容器的定制来完成。如果具体的应用程序需要采用上面定义的SingletonControllerActivator以单例的模式来激活目标Controller,那么它可以在启动MvcEngine之前按照如下的形式将SingletonControllerActivator注册到后者使用的DI容器上。

   1: public class App
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         Cat cat = new Cat().Register<ControllerActivator, SingletonControllerActivator>();
   6:         MvcEngine engine     = new MvcEngine(cat);
   7:         Uri address          = new Uri("http://localhost/mvcapp");
   8:         Engine.Start(address);
   9:     }
  10: }

二、三种依赖注入方式

一项确定的任务往往需要多个对象相互协作共同完成,或者某个对象在完成某项任务的时候需要直接或者间接地依赖其他的对象来完成某些必要的步骤,所以运行时对象之间的依赖关系是由目标任务来决定的,是“恒定不变的”,自然也无所谓“解耦”的说法。但是运行时的对象通过设计时的类来定义,类与类之间耦合则可以通过依赖进行抽象的方式来解除。

从服务使用的角度来讲,我们借助于一个服务接口对消费的服务进行抽象,那么服务消费程序针对具体服务类型的依赖可以转移到对服务接口的依赖上。但是在运行时提供给消费者总是一个针对某个具体服务类型的对象。不仅如此,要完成定义在服务接口的操作,这个对象可能需要其他相关对象的参与,换句话说提供的这个服务对象可能具有针对其他对象的依赖。作为服务对象提供者的DI容器,在它向消费者提供服务对象之前会自动将这些依赖的对象注入到该对象之中,这就是DI命名的由来。

如右图所示,服务消费程序调用GetService<IFoo>()方法向DI容器索取一个实现了IFoo接口的某个类型的对象,DI容器会根据预先注册的类型匹配关系创建一个类型为Foo的对象。此外,Foo对象依赖Bar和Baz对象的参与才能实现定义在服务接口IFoo之中的操作,所以Foo具有了针对Bar和Baz的直接依赖。至于Baz,它又依赖Qux,那么后者成为了Foo的间接依赖。对于DI容器最终提供的Foo对象,它所直接或者间接依赖的对象Bar、Baz和Qux都会预先被初始化并自动注入到该对象之中。

从编程的角度来讲,类型中的字段或者属性是依赖的一种主要体现形式,如果类型A中具有一个B类型的字段或者属性,那么A就对B产生了依赖。所谓依赖注入,我们可以简单地理解为一种针对依赖字段或者属性的自动化初始化方式。具体来说,我们可以通过三种主要的方式达到这个目的,这就是接下来着重介绍的三种依赖注入方式。

构造器注入

构造器注入就在在构造函数中借助参数将依赖的对象注入到创建的对象之中。如下面的代码片段所示,Foo针对Bar的依赖体现在只读属性Bar上,针对该属性的初始化实现在构造函数中,具体的属性值由构造函数的传入的参数提供。当DI容器通过调用构造函数创建一个Foo对象之前,需要根据当前注册的类型匹配关系以及其他相关的注入信息创建并初始化参数对象。

   1: public class Foo
   2: {
   3:     public IBar Bar{get; private set;}
   4:     public Foo(IBar bar)
   5:     {
   6:         this.Bar = bar;
   7:     }
   8: }

除此之外,构造器注入还体现在对构造函数的选择上面。如下面的代码片段所示,Foo类上面定义了两个构造函数,DI容器在创建Foo对象之前首选需要选择一个适合的构造函数。至于目标构造函数如何选择,不同的DI容器可能有不同的策略,比如可以选择参数做多或者最少的,或者可以按照如下所示的方式在目标构造函数上标注一个相关的特性(我们在第一个构造函数上标注了一个InjectionAttribute特性)。

   1: public class Foo
   2: {
   3:     public IBar Bar{get; private set;}
   4:     public IBaz Baz {get; private set;}
   5:  
   6:     [Injection]
   7:     public Foo(IBar bar)
   8:     {
   9:         this.Bar = bar;
  10:     }
  11:  
  12:     public Foo(IBar bar, IBaz):this(bar)
  13:     {
  14:         this.Baz = baz;
  15:     }
  16: }

属性注入

如果依赖直接体现为类的某个属性,并且该属性不是只读的,我们可以让DI容器在对象创建之后自动对其进行赋值进而达到依赖自动注入的目的。一般来说,我们在定义这种类型的时候,需要显式将这样的属性标识为需要自动注入的依赖属性,以区别于该类型的其他普通的属性。如下面的代码片段所示,Foo类中定义了两个可读写的公共属性Bar和Baz,我们通过标注InjectionAttribute特性的方式将属性Baz设置为自动注入的依赖属性。对于由DI容器提供的Foo对象,它的Baz属性将会自动被初始化。

   1: public class Foo
   2: {
   3:     public IBar Bar{get; set;}
   4:  
   5:     [Injection]
   6:     public IBaz Baz {get; set;}
   7: }

方法注入

体现依赖关系的字段或者属性可以通过方法的形式初始化。如下面的代码片段所示,Foo针对Bar的依赖体现在只读属性上,针对该属性的初始化实现在Initialize方法中,具体的属性值由构造函数的传入的参数提供。我们同样通过标注特性(InjectionAttribute)的方式将该方法标识为注入方法。DI容器在调用构造函数创建一个Foo对象之后,它会自动调用这个Initialize方法对只读属性Bar进行赋值。在调用该方法之前,DI容器会根据预先注册的类型映射和其他相关的注入信息初始化该方法的参数。

   1: public class Foo
   2: {
   3:     public IBar Bar{get; private set;}
   4:  
   5:     [Injection]
   6:     public Initialize(IBar bar)
   7:     {
   8:         this.Bar = bar;
   9:     }
  10: }

三、实例演示:创建一个简易版的DI框架

上面我们对DI容器以及三种典型的依赖注入方式进行了详细介绍,为了让读者朋友们对此具有更加深入的理解,介绍我们通过简短的代码创建一个迷你型的DI容器,即我们上面提到过的Cat。在正式对Cat的设计展开介绍之前,我们先来看看Cat在具体应用程序中的用法。

   1: public interface IFoo {}
   2: public interface IBar {}
   3: public interface IBaz {}
   4: public interface IQux {}
   5:  
   6: public class Foo : IFoo
   7: {
   8:     public IBar Bar { get; private set; }
   9:  
  10:     [Injection]
  11:     public IBaz Baz { get; set; }
  12:  
  13:     public Foo() {}
  14:  
  15:     [Injection]
  16:     public Foo(IBar bar)
  17:     {
  18:         this.Bar = bar;
  19:     }
  20: }
  21:  
  22: public class Bar : IBar {}
  23:  
  24: public class Baz : IBaz
  25: {
  26:     public IQux Qux { get; private set; }
  27:  
  28:     [Injection]
  29:     public void Initialize(IQux qux)
  30:     {
  31:         this.Qux = qux;
  32:     }
  33: }
  34:  
  35: public class Qux : IQux {}

我们在一个控制台应用中按照如上的形式定义了四个服务类型(Foo、Bar、Baz和Qux),它们分别实现了各自的服务接口(IFoo、IBar、IBaz和IQux)。定义在Foo中的属性Bar和Baz,以及定义在Baz中的属性Qux是三个需要自动注入的依赖属性,我们采用的注入方式分别是构造器注入、属性注入和方法注入。

我们在作为应用入口的Main方法中编写了如下一段程序。如下面的代码片段所示,在创建了作为DI容器的Cat对象之后,我们调用它的Register<TFrom, TTo>()方法注册了服务类型和对应接口之间的匹配关系。然后我们调用Cat对象的GetService<T>()方法通过指定的服务接口类型IFoo得到对应的服务对象,为了确保相应的依赖属性均按照我们希望的方式被成功注入,我们将它们显式在控制台上。

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         Cat cat = new Cat();
   6:         cat.Register<IFoo, Foo>();
   7:         cat.Register<IBar, Bar>();
   8:         cat.Register<IBaz, Baz>();
   9:         cat.Register<IQux, Qux>();
  10:  
  11:         IFoo service = cat.GetService<IFoo>();
  12:         Foo foo = (Foo)service;
  13:         Baz baz = (Baz)foo.Baz;
  14:  
  15:         Console.WriteLine("cat.GetService<IFoo>(): {0}", service);
  16:         Console.WriteLine("cat.GetService<IFoo>().Bar: {0}", foo.Bar);
  17:         Console.WriteLine("cat.GetService<IFoo>().Baz: {0}", foo.Baz);
  18:         Console.WriteLine("cat.GetService<IFoo>().Baz.Qux: {0}", baz.Qux);
  19:     }
  20: }

这段程序被成功执行之后会在控制台上产生如下所示的输出结果,这充分证明了作为DI容器的Cat对象不仅仅根据指定的服务接口IFoo创建了对应类型(Foo)的服务对象,而且直接依赖的两个属性(Bar和Baz)分别以构造器注入和属性注入的方式被成功初始化,间接依赖的属性(Baz的属性Qux)也以方法注入的形式被成功初始化。

   1: cat.GetService<IFoo>(): Foo
   2: cat.GetService<IFoo>().Bar: Bar
   3: cat.GetService<IFoo>().Baz: Baz
   4: cat.GetService<IFoo>().Baz.Qux: Qux

在对Cat容器的用法有了基本了解之后,我们来正式讨论它的总体设计和具体实现。我们首先来看看用来标识注入构造函数、注入属性和注入方法的InjectionAttribute特性的定义,如下面的代码片段所示,InjectionAttribute仅仅是一个单纯的标识特性,它的用途决定了应用该特性的目标元素的类型(构造函数、属性和方法)。

   1: [AttributeUsage( AttributeTargets.Constructor| 
   2:                  AttributeTargets.Property| 
   3:                  AttributeTargets.Method, 
   4:                  AllowMultiple = = false)]
   5: public class InjectionAttribute: Attribute {}

如下所示的是Cat类的完整定义。我们采用一个ConcurrentDictionary<Type, Type>类型的字段来存放服务接口和具体服务类型之间的映射关系,这样的映射关系通过调用Register方法实现。针对服务类型(服务接口类型或者具体服务类型均可)的服务对象提供机制实现在GetService方法中。

   1: public class Cat
   2: {
   3:     private ConcurrentDictionary<Type, Type> typeMapping = new ConcurrentDictionary<Type, Type>();
   4:  
   5:     public void Register(Type from, Type to)
   6:     {
   7:         typeMapping[from] = to;
   8:     }
   9:  
  10:     public object GetService(Type serviceType)
  11:     {
  12:         Type type;
  13:         if (!typeMapping.TryGetValue(serviceType, out type))
  14:         {
  15:             type = serviceType;
  16:         }
  17:         if (type.IsInterface || type.IsAbstract)
  18:         {
  19:             return null;
  20:         }
  21:  
  22:         ConstructorInfo constructor = this.GetConstructor(type);
  23:         if (null == constructor)
  24:         {
  25:             return null;
  26:         }
  27:  
  28:         object[] arguments = constructor.GetParameters().Select(p => this.GetService(p.ParameterType)).ToArray();
  29:         object service = constructor.Invoke(arguments);
  30:         this.InitializeInjectedProperties(service);
  31:         this.InvokeInjectedMethods(service);
  32:         return service;
  33:     }
  34:  
  35:     protected virtual ConstructorInfo GetConstructor(Type type)
  36:     {
  37:         ConstructorInfo[] constructors = type.GetConstructors();
  38:         return constructors.FirstOrDefault(c => c.GetCustomAttribute<InjectionAttribute>() != null)
  39:             ?? constructors.FirstOrDefault();
  40:     }
  41:  
  42:     protected virtual void InitializeInjectedProperties(object service)
  43:     {
  44:         PropertyInfo[] properties = service.GetType().GetProperties()
  45:             .Where(p => p.CanWrite && p.GetCustomAttribute<InjectionAttribute>() != null)
  46:             .ToArray();
  47:         Array.ForEach(properties, p =>p.SetValue(service, this.GetService(p.PropertyType)));
  48:     }
  49:  
  50:     protected virtual void InvokeInjectedMethods(object service)
  51:     {
  52:         MethodInfo[] methods = service.GetType().GetMethods()
  53:             .Where(m => m.GetCustomAttribute<InjectionAttribute>() != null)
  54:             .ToArray();
  55:         Array.ForEach(methods, m=> 
  56:         {
  57:             object[] arguments = m.GetParameters().Select(p => this.GetService(p.ParameterType)).ToArray();
  58:             m.Invoke(service, arguments);
  59:         });
  60:     }        
  61: }

如上面的代码片段所示,GetService方法利用GetConstructor方法返回的构造函数创建服务对象。GetConstructor方法体现了我们采用的注入构造函数的选择策略:优先选择标注有InjectionAttribute特性的构造函数,如果不存在则选择第一个公有的构造函数。执行构造函数传入的参数是递归地调用GetService方法根据参数类型获得的。

服务对象被成功创建之后,我们分别调用InitializeInjectedProperties和InvokeInjectedMethods方法针对服务对象实施属性注入和方法注入。对于前者(属性注入),我们在以反射的方式得到所有标注了InjectionAttribute特性的依赖属性并对它们进行赋值,具体的属性值同样是以递归的形式调用GetService方法针对属性类型获得。至于后者(方法注入),我们同样以反射的方式得到所有标注有InjectionAttribute特性的注入方法后自动调用它们,传入的参数值依然是递归地调用GetService方法针对参数类型的返回值。

ASP.NET Core中的依赖注入(1):控制反转(IoC)
ASP.NET Core中的依赖注入(2):依赖注入(DI)
ASP.NET Core中的依赖注入(3):服务注册与提取
ASP.NET Core中的依赖注入(4):构造函数的选择与生命周期管理
ASP.NET Core中的依赖注入(5):ServicePrvider实现揭秘【总体设计】
ASP.NET Core中的依赖注入(5):ServicePrvider实现揭秘【解读ServiceCallSite】
ASP.NET Core中的依赖注入(5):ServicePrvider实现揭秘【补充漏掉的细节】

时间: 2024-10-05 19:58:18

ASP.NET Core中的依赖注入(2):依赖注入(DI)的相关文章

ASP.NET Core Web 应用程序系列(三)- 在ASP.NET Core中使用Autofac替换自带DI进行构造函数和属性的批量依赖注入(MVC当中应用)

在上一章中主要和大家分享了在ASP.NET Core中如何使用Autofac替换自带DI进行构造函数的批量依赖注入,本章将和大家继续分享如何使之能够同时支持属性的批量依赖注入. 约定: 1.仓储层接口都以“I”开头,以“Repository”结尾.仓储层实现都以“Repository”结尾. 2.服务层接口都以“I”开头,以“Service”结尾.服务层实现都以“Service”结尾. 接下来我们正式进入主题,在上一章的基础上我们再添加一个web项目TianYa.DotNetShare.Core

ASP.NET Core Web 应用程序系列(二)- 在ASP.NET Core中使用Autofac替换自带DI进行批量依赖注入(MVC当中应用)

原文:ASP.NET Core Web 应用程序系列(二)- 在ASP.NET Core中使用Autofac替换自带DI进行批量依赖注入(MVC当中应用) 在上一章中主要和大家分享在MVC当中如何使用ASP.NET Core内置的DI进行批量依赖注入,本章将继续和大家分享在ASP.NET Core中如何使用Autofac替换自带DI进行批量依赖注入. PS:本章将主要采用构造函数注入的方式,下一章将继续分享如何使之能够同时支持属性注入的方式. 约定: 1.仓储层接口都以“I”开头,以“Repos

ASP.NET Core中的依赖注入(5): ServiceProvider实现揭秘 【解读ServiceCallSite 】

通过上一篇的介绍我们应该对实现在ServiceProvider的总体设计有了一个大致的了解,但是我们刻意回避一个重要的话题,即服务实例最终究竟是采用何种方式提供出来的.ServiceProvider最终采用何种方式提供我们所需的服务实例取决于最终选择了怎样的ServiceCallSite,而服务注册是采用的ServiceDescriptor有决定了ServiceCallSite类型的选择.我们将众多不同类型的ServiceCallSite大体分成两组,一组用来创建最终的服务实例,另一类则与生命周

ASP.NET Core中的依赖注入(5):ServicePrvider实现揭秘【补充漏掉的细节】

到目前为止,我们定义的ServiceProvider已经实现了基本的服务提供和回收功能,但是依然漏掉了一些必需的细节特性.这些特性包括如何针对IServiceProvider接口提供一个ServiceProvider对象,何创建ServiceScope,以及如何提供一个服务实例的集合. 一.提供一个ServiceProvider对象 我们知道当将服务类型指定为IServiceProvider接口并调用ServiceProvider的GetService方法是,ServiceProvider对象本

ASP.NET Core中的依赖注入(5): ServiceProvider实现揭秘 【总体设计 】

本系列前面的文章我们主要以编程的角度对ASP.NET Core的依赖注入系统进行了详细的介绍,如果读者朋友们对这些内容具有深刻的理解,我相信你们已经可以正确是使用这些与依赖注入相关的API了.如果你还对这个依赖注入系统底层的实现原理具有好奇心,可以继续阅读这一节的内容. 目录一.ServiceCallSite 二.Service 三.ServiceEntry 四.ServiceTable 五.ServiceProvider 作为DI容器的体现,ServiceProvider是ASP.NET Co

ASP.NET Core 中的依赖注入 [共7篇]

一.控制反转(IoC) ASP.NET Core在启动以及后续针对每个请求的处理过程中的各个环节都需要相应的组件提供相应的服务,为了方便对这些组件进行定制,ASP.NET通过定义接口的方式对它们进行了“标准化”,我们将这些标准化的组件称为服务,ASP.NET在内部专门维护了一个DI容器来提供所需的服务.要了解这个DI容器以及现实其中的服务提供机制,我们先得知道什么是DI(Dependence Injection),而一旦我们提到DI,又不得不说IoC(Inverse of Control)… [

ASP.NET Core中使用GraphQL - 第三章 依赖注入

ASP.NET Core中使用GraphQL ASP.NET Core中使用GraphQL - 第一章 Hello World ASP.NET Core中使用GraphQL - 第二章 中间件 SOLID原则中的D表示依赖倒置原则.这个原则的内容是: 上层模块不应该直接依赖底层模块,而应该依赖其抽象 抽象不应该依赖于细节, 细节应该依赖抽象 来源:WIKIPEDIA 在一个模块中创建一个其他模块的实例会导致这个模块与其他模块之间的紧耦合. 为了让不同的模块解耦,我们需要遵循依赖倒置原则.按照这种

ASP.NET Core中使用自定义MVC过滤器属性的依赖注入

原文:ASP.NET Core中使用自定义MVC过滤器属性的依赖注入 除了将自己的中间件添加到ASP.NET MVC Core应用程序管道之外,您还可以使用自定义MVC过滤器属性来控制响应,并有选择地将它们应用于整个控制器或控制器操作. ASP.NET Core中常用的MVC过滤器之一是  ExceptionFilterAttribute,用于处理Wep API应用程序中的错误响应.它很容易实现,开发人员和我在ASP.NET Core中使用MVC过滤器属性所面临的问题是访问Startup.cs类

ASP.NET Core中使用IOC三部曲(二.采用Autofac来替换IOC容器,并实现属性注入)

前言 本文主要是详解一下在ASP.NET Core中,自带的IOC容器相关的使用方式和注入类型的生命周期. 这里就不详细的赘述IOC是什么 以及DI是什么了.. emm..不懂的可以自行百度. 目录 ASP.NET Core中使用IOC三部曲(一.使用ASP.NET Core自带的IOC容器) ASP.NET Core中使用IOC三部曲(二.采用Autofac来替换IOC容器,并实现属性注入) ASP.NET Core中使用IOC三部曲(三.采用替换后的Autofac来实现AOP拦截) 正文 上