.NET Core依赖注入

一、概念解释

使用 .NET,通过 new 运算符(即,new MyService 或任何想要实例化的对象类型)调用构造函数即可轻松实现对象实例化。这样就形成了强耦合,所以提供了一个间接层,不用直接使用 new 运算符实例化服务,而是请求一个接口,并提供程序实现该接口。我们将解耦返回到客户端的实际实例的模式称为控制反转。

1、控制反转

IOC—Inversion of Control,即“控制反转”,不是什么技术,而是一种设计思想。Ioc意味着将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。

谁控制谁,控制什么:传统程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对象的创建;谁控制谁?当然是IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象也包括比如文件等)。

为何是反转,哪些方面反转了:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象(通过new进行创建对象),也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。
容器:容器负责两件事情:

  • 绑定服务与实例之间的关系
  • 获取实例,并对实例进行管理(创建与销毁)

2、依赖注入

DI—Dependency Injection,即“依赖注入”:是组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。 理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下:

谁依赖于谁:当然是应用程序依赖于IoC容器;

为什么需要依赖:应用程序需要IoC容器来提供对象需要的外部资源;

谁注入谁:很明显是IoC容器注入应用程序某个对象,应用程序依赖的对象;

注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。

3、两者关系

控制反转是目的,依赖注入是方式

二、实现DI

我们在控制台应用程序中,先自己模拟实现简单的DI,主要步骤如下

1)编写测试对象及对应的接口代码

2)编写容器,实现对象注册和提取


1、首先,我们编写我们自己的测试对象如下

    public class Company : ICompany
    {
    }

编写对应的接口代码如下:

    public interface ICompany
    {
    }

2、编写容器

2.1 创建线程安全的键/值对集合用来存储注册的服务

        //线程安全的键/值对集合用来存储注册的服务
        private ConcurrentDictionary<Type, Type> typeMapping = new ConcurrentDictionary<Type, Type>();

2.2 注册

        public void Register<T1, T2>()
        {
            Register(typeof(T1), typeof(T2));
        }

        public void Register(Type from, Type to)
        {
            //添加注册
            typeMapping[from] = to;
        }

2.3 根据程序需要的类型名称选择相应的实体类型,并返回类型实例

        public T GetService<T>() where T : class
        {
            return this.GetService(typeof(T)) as T;
        }

        //根据程序需要的类型名称选择相应的实体类型,并返回类型实例
        public object GetService(Type serviceType)
        {
            Type type;
            //
            if(!typeMapping.TryGetValue(serviceType, out type))
            {
                type = serviceType;
            }

            if(type.IsInterface || type.IsAbstract)
            {
                return null;
            }

            //根据构造函数参数获取实例对象
            ConstructorInfo constructor = type.GetConstructors().FirstOrDefault();
            if(null == constructor)
            {
                return null;
            }
            object service = constructor.Invoke(null);
            
            return service;
        }

3、使用

namespace IocDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            Cat cat = new Cat();
            //注册
            cat.Register<ICompany, Company>();

            //获取
            ICompany service = cat.GetService<ICompany>();

            Console.WriteLine("cat.GetService<ICompany>(): {0}", service);
            Console.ReadKey();
            
        }
    }
}

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

    public class Company : ICompany
    {
        public IOrganize Organize { get; }

        public Company(IOrganize organize)
        {
            this.Organize = organize;
        }
    }
    //获取参数
    object[] argments = constructor.GetParameters().Select(p => this.GetService(p.ParameterType)).ToArray();
    object service = constructor.Invoke(argments);

4.2 多个构造器
为了解决构造函数的选择问题,我们引入如下这个InjectionAttribute特性。我们将所有公共实例构造函数作为候选的构造函数,并会优先选择标注了该特性的构造函数。加入GetConstructor()方法获取构造函数,当构造函数被选择出来后,我们需要通过GetService()分析其参数类型来提供具体的参数值,这实际上是一个递归的过程。

(1)加入InjectionAttribute.cs

namespace IoCDemo.FlowControl
{
    [AttributeUsage(AttributeTargets.Constructor | AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = false)]
    public class InjectionAttribute : Attribute { }
}

(2)GetService()修改如下

        //根据程序需要的类型名称选择相应的实体类型,并返回类型实例
        public object GetService(Type serviceType)
        {
            Type type;
            //
            if(!typeMapping.TryGetValue(serviceType, out type))
            {
                type = serviceType;
            }

            if(type.IsInterface || type.IsAbstract)
            {
                return null;
            }

            //根据构造函数参数获取实例对象
            ConstructorInfo constructor = this.GetConstructor(type);
            if (null == constructor)
            {
                return null;
            }
            //获取参数
            object[] argments = constructor.GetParameters().Select(p => this.GetService(p.ParameterType)).ToArray();
            object service = constructor.Invoke(argments);

            return service;
        }

        //根据标注InjectionAttribute特性,获取构造函数,如果没有标记则返回默认的
        protected virtual ConstructorInfo GetConstructor(Type type)
        {
            ConstructorInfo[] constructors = type.GetConstructors();
            return constructors.FirstOrDefault(c => c.GetCustomAttribute<InjectionAttribute>() != null)
                ?? constructors.FirstOrDefault();
        }

(3)对象及接口修改如下

    public class Organize : IOrganize
    {
    }

    public interface IOrganize
    {
    }
    public class Company : ICompany
    {
        public IOrganize Organize { get; private set; }

        public Company() { }

        [Injection]
        public Company(IOrganize organize)
        {
            this.Organize = organize;
        }
    }

(4)使用

namespace IocDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            Cat cat = new Cat();
            cat.Register<ICompany, Company>();
            cat.Register<IOrganize, Organize>();

            ICompany service = cat.GetService<ICompany>();

            Console.WriteLine("cat.GetService<ICompany>(): {0}", service);
            Console.ReadKey();
            
        }
    }
}

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

    public class Company : ICompany
    {
        [Injection]
        public IOrganize Organize { get; set; }
    }

在GetService()中加入InitializeInjectedProperties()来实现属性注入

        public object GetService(Type serviceType)
        {
           /**
            省略
           **/
            object service = constructor.Invoke(argments);
            this.InitializeInjectedProperties(service);

            return service;
        }

        protected virtual void InitializeInjectedProperties(object service)
        {
            PropertyInfo[] properties = service.GetType().GetProperties()
                .Where(p => p.CanWrite && p.GetCustomAttribute<InjectionAttribute>() != null)
                .ToArray();

            Array.ForEach(properties, p => p.SetValue(service, this.GetService(p.PropertyType)));
        }

5、方法注入
体现依赖关系的字段或者属性可以通过方法的形式初始化。如下面的代码片段所示,Company针对Organize的依赖体现在属性上,针对该属性的初始化实现在Method方法中,具体的属性值由构造函数的传入的参数提供。我们同样通过标注特性(InjectionAttribute)的方式将该方法标识为注入方法。DI容器在调用构造函数创建一个Foo对象之后,它会自动调用这个Method方法对只读属性Organize进行赋值。

    public class Company : ICompany
    {
        public IOrganize Organize { get; set; }

        [Injection]
        public void Method(IOrganize organize)
        {
            this.Organize = organize;
        }
    }

在GetService()中加入InvokeInjectedMethods()来实现方法注入

        public object GetService(Type serviceType)
        {
           /**
            省略
           **/
            object service = constructor.Invoke(argments);
            this.InitializeInjectedProperties(service);
            this.InvokeInjectedMethods(service);
            return service;
        }

       protected virtual void InvokeInjectedMethods(object service)
        {
            MethodInfo[] methods = service.GetType().GetMethods()
                .Where(m => m.GetCustomAttribute<InjectionAttribute>() != null)
                .ToArray();

            Array.ForEach(methods, m =>
                {
                    object[] arguments = m.GetParameters().Select(p => this.GetService(p.ParameterType)).ToArray();
                    m.Invoke(service, arguments);
                });
        }

三、.NET Core 自带DI框架

毫不夸张地说,整个ASP.NET Core框架是建立在一个依赖注入框架之上的,它在应用启动时构建请求处理管道过程中,以及利用该管道处理每个请求过程中使用到的服务对象均来源于DI容器。该DI容器不仅为ASP.NET Core框架提供必要的服务,同时作为了应用的服务提供者,依赖注入已经成为了ASP.NET Core应用基本的编程模式。使用.NET Core自带的DI框架,我们要先通过Nuget安装Microsoft.Extensions.DependencyInjection。其核心就是IServiceCollection与IServiceProvider两个类,我们添加的服务注册被保存到通过IServiceCollection接口表示的集合之中,包含服务注册信息的IServiceCollection对象最终被用来创建作为DI容器的IServiceProvider对象。当需要消费某个服务实例的时候,我们只需要指定服务类型调用IServiceProvider的GetService方法,IServiceProvider就会根据对应的服务注册提供所需的服务实例。


1、服务的注册与消费

using Microsoft.Extensions.DependencyInjection;
using System;

namespace 自带DI
{
    interface ITransient { }
    class Transient : ITransient { }
    interface ISingleton { }
    class Singleton : ISingleton { }
    interface IScoped { }
    class Scoped : IScoped { }
    class Program
    {
        static void Main(string[] args)
        {
            IServiceCollection services = new ServiceCollection();
            services = services.AddTransient<ITransient, Transient>();
            services = services.AddScoped<IScoped, Scoped>();
            services = services.AddSingleton<ISingleton, Singleton>();
            IServiceProvider serviceProvider = services.BuildServiceProvider();
            Console.WriteLine(serviceProvider.GetService<ITransient>() is Transient);
            Console.WriteLine(serviceProvider.GetService<IScoped>() is Scoped);
            Console.WriteLine(serviceProvider.GetService<ISingleton>() is Singleton);
            Console.ReadKey();
        }
    }
}
运行结果

应用初始化过程中添加的服务注册是DI容器用于提供所需服务实例的依据。由于IServiceProvider总是利用指定的服务类型来提供对应服务实例,所以服务是基于类型进行注册的,我们倾向于利用接口来对服务进行抽象,所以这里的服务类型一般为接口。除了以指定服务实例的形式外,我们在注册服务的时候必须指定一个具体的生命周期模式。
我们创建了一个ServiceCollection(它是对IServiceCollection接口的默认实现)对象并调用相应的方法(AddTransient、AddScoped和AddSingleton)针对接口ITransient、ISingleton和IScoped注册了对应的服务,从方法命名可以看出注册的服务采用的生命周期模式分别为Transient、Scoped和Singleton。在完成服务注册之后,我们调用IServiceCollection接口的扩展方法BuildServiceProvider创建出代表DI容器的IServiceProvider对象,并利用它调用后者的GetService<T>方法来提供相应的服务实例。运行结果表明IServiceProvider提供的服务实例与预先添加的服务注册是一致的。
(1)IServiceCollection

public interface IServiceCollection : IList<ServiceDescriptor>
{
}

IServiceCollection对象是一个存放服务注册信息的集合
(2)ServiceDescriptor

public class ServiceDescriptor
{
    public ServiceLifetime Lifetime { get; }
    public Type ServiceType { get; }

    public Type ImplementationType { get; }
    public object ImplementationInstance { get; }
    public Func<IServiceProvider, object> ImplementationFactory { get; }
    public ServiceDescriptor(Type serviceType, object instance);
    public ServiceDescriptor(Type serviceType, Func<IServiceProvider, object> factory, ServiceLifetime lifetime);
    public ServiceDescriptor(Type serviceType, Type implementationType, ServiceLifetime lifetime);
}

DI框架将服务注册存储在一个通过IServiceCollection接口表示的集合之中。如上面的代码片段所示,一个IServiceCollection对象本质上就是一个元素类型为ServiceDescriptor的列表。属性包含生命周期、接口类型、实现类型、实现实例、实现工厂,后三个属性体现了服务实例的三种提供方式,并对应着三个构造函数。
(3)注册服务
我们通过ServiceCollectionServiceExtensions提供的方法注册类型
在第一个代码块中,都是使用“服务类型实例类型”(提供一个服务类型,一个实例类型)的注册方式。此外,微软还提供了“服务实例”(提供一个服务类型,一个实例对象)以及“服务实例工厂”(提供一个服务类型,一个实例对象工厂)的注册方式,前者只供单例服务使用,使用起来也很简单

//服务实例(提供一个服务类型,一个实例对象)
services.AddSingleton<ISingleton>(new Singleton());

//服务实例工厂(提供一个服务类型,一个实例对象工厂)
services.AddSingleton<ISingleton>(_ => new Singleton());

(4)生命周期
使用 AddSingleton、AddScoped、AddTransient 三种方式注册的服务在 ServiceDescriptor 中的 LifeTime 属性分别对应下面这个枚举类型

public enum ServiceLifetime
{
    Singleton,
    Scoped,
    Transient
}

1、Transient:每次从容器 (IServiceProvider)中获取的时候都是一个新的实例
2、Singleton:每次从同根容器中(同根 IServiceProvider)获取的时候都是同一个实例
3、Scoped:每次从同一个容器中获取的实例是相同的

interface ITransient { }
class Transient : ITransient { }
interface ISingleton { }
class Singleton : ISingleton { }
interface IScoped { }
class Scoped : IScoped { }
class Program
{
    static void Main(string[] args)
    {
        IServiceCollection services = new ServiceCollection();
        services = services.AddTransient<ITransient, Transient>();
        services = services.AddScoped<IScoped, Scoped>();
        services = services.AddSingleton<ISingleton, Singleton>();
        IServiceProvider serviceProvider = services.BuildServiceProvider();
         
        Console.WriteLine(ReferenceEquals(serviceProvider.GetService<ITransient>(), serviceProvider.GetService<ITransient>()));
        Console.WriteLine(ReferenceEquals(serviceProvider.GetService<IScoped>(), serviceProvider.GetService<IScoped>()));
        Console.WriteLine(ReferenceEquals(serviceProvider.GetService<ISingleton>(), serviceProvider.GetService<ISingleton>()));

        IServiceProvider serviceProvider1 = serviceProvider.CreateScope().ServiceProvider;
        IServiceProvider serviceProvider2 = serviceProvider.CreateScope().ServiceProvider;

        Console.WriteLine(ReferenceEquals(serviceProvider1.GetService<IScoped>(), serviceProvider1.GetService<IScoped>()));
        Console.WriteLine(ReferenceEquals(serviceProvider1.GetService<IScoped>(), serviceProvider2.GetService<IScoped>()));
        Console.WriteLine(ReferenceEquals(serviceProvider1.GetService<ISingleton>(), serviceProvider2.GetService<ISingleton>()));

        /* False
         * True
         * True
         * True
         * False
         * True
         */
    }
}

(5)IServiceProvider


在这张类图中,有三个核心接口,IServiceProvider 表示容器,具备提供对象功能,IServiceScope 代表一个具有范围的节点容器,而 IServiceProviderEngine 是提供对象核心接口,具有一个根容器,每次创建对象的时候都会带上具有范围的节点容器。关于容器和引擎这两个概念如何判断呢?凡是实现了 IServiceProvider 的都可以称为容器,同样的,凡是实现了 IServiceProviderEngine 的都可以称为引擎,引擎仍然是一个容器,实际上,上图中所有的类型都是一个容器,因为他们都直接或者间接实现了 IServiceProvider 接口,然而最终负责创建服务实例的必然是一个引擎。
IServiceScope 的默认实现是 ServiceProviderEngineScope,ResolvedServices 存储已经实例化过的生命周期为 Scoped 的对象,_disposables 存储通过该根创建的所有实例类型(如果该类型实现了 IDisposable 接口),在调用 Dispose 方法时会释放 _disposables 中的对象,同时清空 ResolvedServices 中的实例。
ServiceProviderEngineScope 总在引擎被创建的时候初始化,并且在 GetService 方法中,传递的根容器正是这个 Root,所以根容器一般情况不会更改,然而我们可以 CreateScope 方法(来自 IServiceScopeFactory 接口)来创建一个新的根容器(实际上是一个新的节点)

Singleton:IServiceProvider创建的服务实例保存在作为根容器的IServiceProvider上,所有多个同根的IServiceProvider对象提供的针对同一类型的服务实例都是同一个对象。

Scoped:IServiceProvider创建的服务实例由自己保存,所以同一个IServiceProvider对象提供的针对同一类型的服务实例均是同一个对象。

Transient:针对每一次服务提供请求,IServiceProvider总是创建一个新的服务实例。
IServiceProvider除了为我们提供所需的服务实例之外,对于由它提供的服务实例,它还肩负起回收释放之责。这里所说的回收释放与.NET Core自身的垃圾回收机制无关,仅仅针对于自身类型实现了IDisposable接口的服务实例(下面简称为Disposable服务实例),针对服务实例的释放体现为调用它们的Dispose方法。IServiceProvider针对服务实例采用的回收释放策略取决于对应服务注册的生命周期模式,具体服务回收策略主要体现为如下两点:

Singleton:提供Disposable服务实例保存在作为根容器的IServiceProvider对象上,只有后者被释放的时候这些Disposable服务实例才能被释放。

Scoped和Transient:IServiceProvider对象会保存由它提供的Disposable服务实例,当自己被释放的时候,这些Disposable会被释放。
1、
如下面的代码片段所示,IServiceProvider接口定义了唯一的方法GetService方法根据指定的服务类型来提供对应的服务实例。当我们在利用包含服务注册的IServiceCollection对象创建对作为DI容器的IServiceProvider对象之后,我们只需要将服务注册的服务类型(对应于ServiceDescriptor的ServiceType属性)作为参数调用GetService方法,后者就能根据服务注册信息为我们提供对应的服务实例。

public interface IServiceProvider
{
    object GetService(Type serviceType);
}

public static class ServiceCollectionContainerBuilderExtensions
{
    public static ServiceProvider BuildServiceProvider(this IServiceCollection services);
}

2、构造函数的选择
对于通过调用IServiceCollection的BuildServiceProvider方法创建的IServiceProvider来说,当我们通过指定服务类型调用其GetService方法以获取对应的服务实例的时候,它总是会根据提供的服务类型从服务注册列表中找到对应的ServiceDescriptor对象,并根据后者提供所需的服务实例。

ServiceDescriptor具有三个不同的构造函数,分别对应着服务实例最初的三种创建方式,我们可以提供一个Func<IServiceProvider, object>对象作为工厂来创建对应的服务实例,也可以直接提供一个创建好的服务实例。如果我们提供的是服务的实现类型,那么最终提供的服务实例将通过调用该类型的某个构造函数来创建,那么构造函数时通过怎样的策略被选择出来的呢?

如果IServiceProvider对象试图通过调用构造函数的方式来创建服务实例,传入构造函数的所有参数必须先被初始化,最终被选择出来的构造函数必须具备一个基本的条件:IServiceProvider能够提供构造函数的所有参数。

我们在一个控制台应用中定义了四个服务接口(IFoo、IBar、IBaz和IGux)以及实现它们的四个服务类(Foo、Bar、Baz和Gux)。如下面的代码片段所示,我们为Gux定义了三个构造函数,参数均为我们定义了服务接口类型。为了确定IServiceProvider最终选择哪个构造函数来创建目标服务实例,我们在构造函数执行时在控制台上输出相应的指示性文字。

public interface IFoo {}
public interface IBar {}
public interface IBaz {}
public interface IGux {}

public class Foo : IFoo {}
public class Bar : IBar {}
public class Baz : IBaz {}
public class Gux : IGux
{
    public Gux(IFoo foo) => Console.WriteLine("Selected constructor: Gux(IFoo)");
    public Gux(IFoo foo, IBar bar) => Console.WriteLine("Selected constructor: Gux(IFoo, IBar)");
    public Gux(IFoo foo, IBar bar, IBaz baz) => Console.WriteLine("Selected constructor: Gux(IFoo, IBar, IBaz)");
}

在如下这段演示程序中我们创建了一个ServiceCollection对象并在其中添加针对IFoo、IBar以及IGux这三个服务接口的服务注册,针对服务接口IBaz的注册并未被添加。我们利用由它创建的IServiceProvider来提供针对服务接口IGux的实例,究竟能否得到一个Gux对象呢?如果可以,它又是通过执行哪个构造函数创建的呢?

class Program
{
    static void Main(string[] args)
    {       
        new ServiceCollection()
            .AddTransient<IFoo, Foo>()
            .AddTransient<IBar, Bar>()
            .AddTransient<IGux, Gux>()
            .BuildServiceProvider()
            .GetServices<IGux>();
    }
}

对于定义在Gux中的三个构造函数来说,由于创建IServiceProvider提供的IServiceCollection集合包含针对接口IFoo和IBar的服务注册,所以它能够提供前面两个构造函数的所有参数。由于第三个构造函数具有一个类型为IBaz的参数,这无法通过IServiceProvider来提供。根据我们上面介绍的第一个原则(IServiceProvider能够提供构造函数的所有参数),Gux的前两个构造函数会成为合法的候选构造函数,那么IServiceProvider最终会选择哪一个呢?

在所有合法的候选构造函数列表中,最终被选择出来的构造函数具有这么一个特征:每一个候选构造函数的参数类型集合都是这个构造函数参数类型集合的子集。如果这样的构造函数并不存在,一个类型为InvalidOperationException的异常会被抛出来。根据这个原则,Gux的第二个构造函数的参数类型包括IFoo和IBar,而第一个构造函数仅仅具有一个类型为IFoo的参数,最终被选择出来的会是Gux的第二个构造函数,所有运行我们的实例程序将会在控制台上产生如下图所示的输出结果。

接下来我们对实例程序略加改动。如下面的代码片段所示,我们只为Gux定义两个构造函数,它们都具有两个参数,参数类型分别为IFoo&IBar和IBar&IBaz。我们将针对IBaz/Baz的服务注册添加到创建的ServiceCollection对象上。

class Program
{
    static void Main(string[] args)
    {       
        new ServiceCollection()
            .AddTransient<IFoo, Foo>()
            .AddTransient<IBar, Bar>()
            .AddTransient<IBaz, Baz>()
            .AddTransient<IGux, Gux>()
            .BuildServiceProvider()
            .GetServices<IGux>();
    }
}

public class Gux : IGux
{
    public Gux(IFoo foo, IBar bar) {}
    public Gux(IBar bar, IBaz baz) {}
}

对于Gux的两个构造函数,虽然它们的参数均能够由IServiceProvider来提供,但是并没有一个构造函数的参数类型集合能够成为所有有效构造函数参数类型集合的超集,所以ServiceProvider无法选择出一个最佳的构造函数。运行该程序后会抛出如下图所示的InvalidOperationException异常,并提示无法从两个候选的构造函数中选择出一个最优的来创建服务实例。


三、使用ASP.NET Core自带DI

1.如何注入自己的服务
首先,我们编写我们自己的测试服务如下:

    public class TestService: ITestService
    {
        public TestService()
        {
            MyProperty = Guid.NewGuid();
        }
        public Guid MyProperty { get; set; }
        public List<string> GetList(string a)
        {
            return new List<string>() { "LiLei", "ZhangSan", "LiSi" };
        }
    }

编写对应的接口代码如下:

    public interface ITestService
    {
        Guid MyProperty { get; }
        List<string> GetList(string a);
    }

然后,我们要在Startup类引用 Microsoft.Extensions.DependencyInjection,
修改ConfigureServices方法,如下:

        public void ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

            //这里就是注入服务
            services.AddTransient<ITestService, TestService>();
        }

这样,我们就完成了初步的注入操作.

那么我们如何使用我们注入的服务呢?

我们到控制器,编写代码如下:

    public class DITestController : Controller
    {
            private readonly ITestService _testService;
            public DITestController(ITestService testService)
            {
                   _testService = testService;
             }
             public IActionResult Index()
            {
                ViewBag.date = _testService.GetList("");
                return View();
             }
    }

我们编写我们的index视图如下:

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>
@foreach (var item in ViewBag.date)
{

    <h2>@item</h2>
}

最终效果如下:


2.注入服务的生命周期
这里的生命周期针对每个HTTP请求的上下文,也就是服务范围的生命周期与每个请求上下文绑定在一起

Transient(瞬时的):每次请求时都会创建的瞬时生命周期服务。

Scoped(作用域的):在同作用域,服务每个请求只创建一次。

Singleton(唯一的):全局只创建一次,第一次被请求的时候被创建,然后就一直使用这一个。

四、使用Autofac

首先,我们需要从nuget引用相关的包:Autofac.Extensions.DependencyInjection
然后,我们修改Startup中的ConfigureServices代码如下:

        public IServiceProvider ConfigureServices(IServiceCollection services)
        {
            services.Configure<CookiePolicyOptions>(options =>
            {
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

            //加入Autofac
            var containerBuilder = new ContainerBuilder();
            containerBuilder.RegisterModule<DefaultModule>();
            containerBuilder.Populate(services);
            var container = containerBuilder.Build();

            return new AutofacServiceProvider(container);
        }

这里我们使用了AutoFac的功能之一,模块化注入.也就是RegisterModule 这里, DefaultModule是我们的注入模块,代码很简单,如下:

    public class DefaultModule : Module
    {
        protected override void Load(ContainerBuilder builder)
        {

            //注入测试服务
            builder.RegisterType<TestService>().As<ITestService>();
            
        }
    }

在上面的代码中,我们配置IServiceProvider从Autofac容器中解析(设置一个有效的Autofac服务适配器)。

然后在整个框架中使用它来解析控制器的依赖关系,并在HttpContext上公开所有其他用例的服务定位。

这样我们就完成了初步的Autofac容器替换.下面我们创建控制器来看看效果.代码如下:

public class DITestController : Controller
{
    private readonly ITestService _testService;

    public DITestController(ITestService testService)
    {
        _testService = testService;
    }

    public IActionResult Index()
    {
        ViewBag.date = _testService.GetList("Name");
        return View();
    }
}

一、类型注册

1、Autofac类型注册

//1.类型注册
containerBuilder.RegisterType<TestService>().As<ITestService>();

2.使用Module注册

//2.使用Module注册
containerBuilder.RegisterModule<DefaultModule>();

3.程序集批量注册
为了统一管理 IoC 相关的代码,并避免在底层类库中到处引用 Autofac 这个第三方组件,定义了一个专门用于管理需要依赖注入的接口与实现类的空接口 IDependency:

/// <summary>
/// 依赖注入接口,表示该接口的实现类将自动注册到IoC容器中
/// </summary>
public interface IDependency
{
}

这个接口没有任何方法,不会对系统的业务逻辑造成污染,所有需要进行依赖注入的接口,都要继承这个空接口,例如:

public interface ITestService : IDependency
{
        Guid MyProperty { get; }
        List<string> GetList(string a);
 }
public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    //加入Autofac
    var containerBuilder = new ContainerBuilder();

    //程序集批量注册
    Type baseType = typeof(IDependency);

    var dataAccess = Assembly.GetExecutingAssembly();

    containerBuilder.RegisterAssemblyTypes(dataAccess)
        .Where(type => baseType.IsAssignableFrom(type) && !type.IsAbstract)
        .AsImplementedInterfaces().InstancePerLifetimeScope();


    containerBuilder.Populate(services);
    var container = containerBuilder.Build();

    return new AutofacServiceProvider(container);
}

1、InstancePerDependency
对每一个依赖或每一次调用创建一个新的唯一的实例。这也是默认的创建实例的方式。

2、InstancePerLifetimeScope
在一个生命周期域中,每一个依赖或调用创建一个单一的共享的实例,且每一个不同的生命周期域,实例是唯一的,不共享的。

3、InstancePerMatchingLifetimeScope

在一个做标识的生命周期域中,每一个依赖或调用创建一个单一的共享的实例。打了标识了的生命周期域中的子标识域中可以共享父级域中的实例。若在整个继承层次中没有找到打标识的生命周期域,则会抛出异常:DependencyResolutionException。

4、InstancePerOwned

在一个生命周期域中所拥有的实例创建的生命周期中,每一个依赖组件或调用Resolve()方法创建一个单一的共享的实例,并且子生命周期域共享父生命周期域中的实例。若在继承层级中没有发现合适的拥有子实例的生命周期域,则抛出异常:DependencyResolutionException。

5、SingleInstance
每一次依赖组件或调用Resolve()方法都会得到一个相同的共享的实例。其实就是单例模式。

6、InstancePerHttpRequest
在一次Http请求上下文中,共享一个组件实例。仅适用于asp.net mvc开发

4.Lambda注册

//4.Lambda注册
containerBuilder.Register(cc =>
{
    var TestService = new TestService();
    return TestService;
}).As<ITestService>();

5.实例注册

var TestService = new TestService();
containerBuilder.RegisterInstance(TestService).As<ITestService>();

二、类型关联
1、As关联
我们在进行手动关联时,基本都是使用As进行关联的
2、批量关联AsImplementedInerfaces
程序集批量注册中使用的就是批量关联

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容