.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閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容