ASP.NET Core中的依赖注入(2):依赖注入(DI)
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实现揭秘【补充漏掉的细节】
ASP.NET Core中的依赖注入(2):依赖注入(DI)的更多相关文章
- ASP.NET Core Web 应用程序系列(三)- 在ASP.NET Core中使用Autofac替换自带DI进行构造函数和属性的批量依赖注入(MVC当中应用)
在上一章中主要和大家分享了在ASP.NET Core中如何使用Autofac替换自带DI进行构造函数的批量依赖注入,本章将和大家继续分享如何使之能够同时支持属性的批量依赖注入. 约定: 1.仓储层接口 ...
- ASP.NET Core Web 应用程序系列(二)- 在ASP.NET Core中使用Autofac替换自带DI进行批量依赖注入(MVC当中应用)
在上一章中主要和大家分享在MVC当中如何使用ASP.NET Core内置的DI进行批量依赖注入,本章将继续和大家分享在ASP.NET Core中如何使用Autofac替换自带DI进行批量依赖注入. P ...
- 【懒人有道】在asp.net core中实现程序集注入
前言 在asp.net core中,我巨硬引入了DI容器,我们可以在不使用第三方插件的情况下轻松实现依赖注入.如下代码: // This method gets called by the runti ...
- C# 嵌入dll 动软代码生成器基础使用 系统缓存全解析 .NET开发中的事务处理大比拼 C#之数据类型学习 【基于EF Core的Code First模式的DotNetCore快速开发框架】完成对DB First代码生成的支持 基于EF Core的Code First模式的DotNetCore快速开发框架 【懒人有道】在asp.net core中实现程序集注入
C# 嵌入dll 在很多时候我们在生成C#exe文件时,如果在工程里调用了dll文件时,那么如果不加以处理的话在生成的exe文件运行时需要连同这个dll一起转移,相比于一个单独干净的exe,这种形 ...
- ASP.NET Core中使用IOC三部曲(一.使用ASP.NET Core自带的IOC容器)
前言 本文主要是详解一下在ASP.NET Core中,自带的IOC容器相关的使用方式和注入类型的生命周期. 这里就不详细的赘述IOC是什么 以及DI是什么了.. emm..不懂的可以自行百度. 目录 ...
- ASP.NET Core中如影随形的”依赖注入”[上]: 从两个不同的ServiceProvider说起
我们一致在说 ASP.NET Core广泛地使用到了依赖注入,通过前面两个系列的介绍,相信读者朋友已经体会到了这一点.由于前面两章已经涵盖了依赖注入在管道构建过程中以及管道在处理请求过程的应用,但是内 ...
- ASP.NET Core中的依赖注入(1):控制反转(IoC)
ASP.NET Core在启动以及后续针对每个请求的处理过程中的各个环节都需要相应的组件提供相应的服务,为了方便对这些组件进行定制,ASP.NET通过定义接口的方式对它们进行了"标准化&qu ...
- ASP.NET Core中的依赖注入(3): 服务的注册与提供
在采用了依赖注入的应用中,我们总是直接利用DI容器直接获取所需的服务实例,换句话说,DI容器起到了一个服务提供者的角色,它能够根据我们提供的服务描述信息提供一个可用的服务对象.ASP.NET Core ...
- ASP.NET Core中的依赖注入(4): 构造函数的选择与服务生命周期管理
ServiceProvider最终提供的服务实例都是根据对应的ServiceDescriptor创建的,对于一个具体的ServiceDescriptor对象来说,如果它的ImplementationI ...
随机推荐
- 【.net 深呼吸】细说CodeDom(3):命名空间
在上一篇文章中,老周介绍了表达式和语句,尽管老周没有把所有的内容都讲一遍,但相信大伙至少已经掌握基本用法.在本文中,咱们继续探讨 CodeDom 方面的奥秘,这一次咱们聊聊命名空间. 在开始之前,老周 ...
- 微信小程序开发心得
微信小程序也已出来有一段时间了,最近写了几款微信小程序项目,今天来说说感受. 首先开发一款微信小程序,最主要的就是针对于公司来运营的,因为,在申请appid(微信小程序ID号)时候,需要填写相关的公司 ...
- Objective-C三种定时器CADisplayLink / NSTimer / GCD的使用
OC中的三种定时器:CADisplayLink.NSTimer.GCD 我们先来看看CADiskplayLink, 点进头文件里面看看, 用注释来说明下 @interface CADisplayLin ...
- Mysql事务探索及其在Django中的实践(二)
继上一篇<Mysql事务探索及其在Django中的实践(一)>交代完问题的背景和Mysql事务基础后,这一篇主要想介绍一下事务在Django中的使用以及实际应用给我们带来的效率提升. 首先 ...
- iOS开发之再探多线程编程:Grand Central Dispatch详解
Swift3.0相关代码已在github上更新.之前关于iOS开发多线程的内容发布过一篇博客,其中介绍了NSThread.操作队列以及GCD,介绍的不够深入.今天就以GCD为主题来全面的总结一下GCD ...
- H5坦克大战之【玩家控制坦克移动】
自从威少砍下45+11+11的大号三双之后,网上出现了各种各样的神级段子,有一条是这样的: 威少:Hey,哥们,最近过得咋样! 浓眉:对方开启了好友验证,请先添加对方为好友 威少:...... JRS ...
- OpenCV模板匹配算法详解
1 理论介绍 模板匹配是在一幅图像中寻找一个特定目标的方法之一,这种方法的原理非常简单,遍历图像中的每一个可能的位置,比较各处与模板是否“相似”,当相似度足够高时,就认为找到了我们的目标.OpenCV ...
- Velocity初探小结--velocity使用语法详解
做java开发的朋友一般对JSP是比较熟悉的,大部分人第一次学习开发View层都是使用JSP来进行页面渲染的,我们都知道JSP是可以嵌入java代码的,在远古时代,java程序员甚至在一个jsp页面上 ...
- 简单分析JavaScript中的面向对象
初学JavaScript的时候有人会认为JavaScript不是一门面向对象的语言,因为JS是没有类的概念的,但是这并不代表JavaScript没有对象的存在,而且JavaScript也提供了其它的方 ...
- 安卓客户端a标签长按弹框提示解决办法
昨天工作时候发现一个bug,是关于a标签的,在安卓客户端中,如果是a标签的话,长按会出现一个弹框,如图所示 是因为安卓客户端的长按触发机制,以后进行wap端开发的时候,如果用到跳转页面尽量不要用a标签 ...