文章出處

ASP.NET Core的核心是通過一個Server和若干注冊的Middleware構成的管道,不論是管道自身的構建,還是Server和Middleware自身的實現,以及構建在這個管道的應用,都需要相應的服務提供支持,ASP.NET Core自身提供了一個DI容器來實現針對服務的注冊和消費。換句話說,不只是ASP.NET Core底層框架使用的服務是由這個DI容器來注冊和提供,應用級別的服務的注冊和提供也需要以來這個DI容器,所以正如本文標題所說的——學習ASP.NET Core,你必須了解無處不在的“依賴注入”。

目錄一、依賴注入簡介
二、依賴注入在管道構建過程中的應用
三、依賴服務的注冊與注入
四、讓Startup的ConfigureServices方法返回一個ServiceProvider
五、ASP.NET Core默認注冊了哪些服務
六、ASP.NET Core MVC中的依賴注入

一、依賴注入簡介

說到依賴注入(Dependency Injection,以下簡稱DI),就必須說IoC(Inverse of Control),很多人將這兩這混為一談,其實這是兩個完全不同的概念,或者是不同“層次”的兩個概念,我曾在《控制反轉(IoC)》和《依賴注入(DI)》對這兩個概念做過詳細介紹。ASP.NET Core使用的DI框架由“Micorosoft.Extensions.DependencyInjection”這個NuGet包來承載,我們也可以非ASP.NET Core應用或者你自己的框架上單獨使用它,對于這個DI框架的設計、實現以及編程相關的內容,我在系列文章《ASP.NET Core 中的依賴注入 [共7篇]》對此有過詳細的介紹。

DI框架具有兩個核心的功能,即服務的注冊和提供,這兩個功能分別由對應的對象來承載, 它們分別是ServiceCollection和ServiceProvider。如下圖所示,我們將相應的服務以不同的生命周期模式(Transient、Scoped和Singleton)注冊到ServiceCollection對象之上,在利用后者創建的ServiceProvider根據注冊的服務類型提取相應的服務對象。

image

二、依賴注入在管道構建過程中的使用

在ASP.NET Core管道的構架過程中主要涉及三個對象/類型,作為宿主的WebHost和他的創建者WebHostBuilder,以及注冊到WebHostBuilder的Startup類型。 如下的代碼片段體現了啟動ASP.NET Core應用采用的典型編程模式:我們首先創建一個WebHostBuilder對象,并將采用Server和Startup類型注冊到它之上。在調用Build方法創建WebHost之前,我們還可以調用相應的方式做其他所需的注冊工作。當我們調用WebHost的Run方法之后,后者會利用注冊的Startup類型來構建完整的管道。那么在管道的構建過程中,DI是如何被應用的呢?

   1: new WebHostBuilder()
   2:     .UseKestrel()
   3:     .UseStartup<Startup>()
   4:     .Xxx
   5:     .Build()
   6:     .Run()

DI在管道ASP.NET Core管道構建過程中的應用基本體現下面這個序列圖中。當我們調用WebHostBuilder的Build方法創建對應的WebHost的時候,前者會創建一個ServiceCollection對象,并將一系列預定義的服務注冊在它之上。接下來WebHostBuilder會利用這個ServiceCollection對象創建出對應的ServieProvider,這個ServiceProvider和ServiceCollection對象會一并傳遞給最終創建WebHost對象。當我們調用WebHost的Run方法啟動它的時候,如果注冊的Startup是一個實例類型,它會利用這個ServiceProvider以構造器注入的方式創建對應的Startup對象。說的具體一點,我們注冊的Startup類型的構造函數是允許定義參數的,但是參數類型必須是預先注冊到ServiceCollection中的服務類型。

image

注冊的Startup方法可以包含一個可選的ConfigureServices方法,這個方法具有一個類型為IServiceCollection接口的參數。WebHost會將WebHostBuilder傳遞給它的ServiceCollection作為參數調用這個ConfigureServices方法,而我們則利用這個方法將注冊的中間件和應用所需的服務注冊到這個ServiceCollection對象上。在這之后,所有需要的服務(包括框架和應用注冊的服務)都注冊到這個ServiceCollection上面,WebHost會利用它創建一個新的ServiceProvider。WebHost會利用這個ServiceProvider對象以方法注入的方式調用Startup對象/類型的Configure方法,最終完成你對整個管道的建立。換句話會說,定義在Startup類型中旨在用于注冊Middleware的Configure方法除了采用IApplicationBuilder作為第一個參數之外,它依然可以采用注冊的任何一個服務類型作為后續參數的類型。

服務的注冊除了是現在注冊的Startup類型的ConfigureServices方法之外,實際上還具有另一個實現方式,那就是調用WebHostBuilder具有如下定義的ConfigureServices方法。當WebHostBuilder創建出ServiceCollection對象并完成了默認服務的注冊后,我們通過調用這個方法所傳入的所有Action<IServiceCollection>對象將最終應用到這個ServiceCollection對象上。

   1: public interface IWebHostBuilder
   2: {
   3:     IWebHostBuilder ConfigureServiecs(Action<IServiceCollection> configureServices);
   4: }

值得一提的是,Startup類型的ConfigureServices方法是允許具有一個IServiceProvider類型的返回值,如果這個方法返回一個具體的ServiceProrivder,那么WebHost將不會利用ServiceCollection來創建ServiceProvider,而是直接使用這個返回的ServiceProvider來調用Startup對象/類型的Configure方法。這實際上是一個很有用的擴展點,我們使用它可以實現針對其它DI框架的集成。

三、依賴服務的注冊與注入

接下來我們通過一個實例來演示如何利用Startup類型的ConfigureServices來注冊服務,以及發生在Startup類型上的兩種依賴注入形式。如下面的代碼片段所示,我們定義了兩個服務接口(IFoo和IBar)和對應的實現類型(Foo和Bar)。其中其中服務Foo是通過調用WebHostBuilder的ConfigureServices方法進行注冊的,而另一個服務Bar的注冊則發生在Startup的ConfigureServices方法上。對于Startup來說,它具有一個類型為IFoo的只讀屬性,該屬性在構造函數利用傳入的參數進行初始化,不用說這體現了針對Startup的構造器注入。Startup的Configure方法除了ApplicationBuilder作為第一個參數之外,還具有另一個類型為IBar的參數,我們利用它來演示方法注入。

   1: public interface IFoo { }
   2: public interface IBar { }
   3: public class Foo : IFoo { }
   4: public class Bar : IBar { }
   5:  
   6: public class Program
   7: {
   8:     public static void Main(string[] args)
   9:     {
  10:         new WebHostBuilder()
  11:             .ConfigureServices(services=>services.AddSingleton<IFoo, Foo>())
  12:             .UseKestrel()
  13:             .UseStartup<Startup>()
  14:             .Build()
  15:             .Run();
  16:     }
  17: }
  18: public class Startup
  19: {
  20:     public IFoo Foo { get; private set; }
  21:     public Startup(IFoo foo)
  22:     {
  23:         this.Foo = foo;
  24:     }    
  25:     public void ConfigureServices(IServiceCollection services)
  26:     {
  27:         services.AddTransient<IBar, Bar>();
  28:     }
  29:     
  30:     public void Configure(IApplicationBuilder app, IBar bar)
  31:     {
  32:         app.Run(async context =>
  33:         {
  34:             context.Response.ContentType = "text/html";
  35:             await context.Response.WriteAsync($"IFoo=>{this.Foo}<br/>");
  36:             await context.Response.WriteAsync($"IBar=>{bar}");
  37:         });
  38:     }
  39: }

在Startup的Configure方法中,我們調用ApplicationBulder的Run方法注冊了一個Middleware,后者將兩個注入的服務的類型作為響應的內容。當我們運行這個應用,并利用瀏覽器訪問默認的監聽地址(http://localhost:5000)時,瀏覽器會將注入的兩個服務對象的類型以下圖的方式展現出來。

image

四、讓Startup的ConfigureServices方法返回一個ServiceProvider

我們說注冊的Startup類型的ConfigureServices允許返回一個ServiceProvider,這個特性的重要意義在于它使我們可以實現與第三方DI框架(比如Unity、Castle、Ninject和AutoFac等)的集成。我們照例采用一個實例對此做一個演示,簡單起見,我們并不會真正利用某個具體的DI框架來創建這個ServiceProvider,而是直接創建一個新的ServiceCollection來創建它,為此我們對上面這個程序進行了如下的改寫。

   1: public class Program
   2: {
   3:     public static void Main(string[] args)
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .UseStartup<Startup>()
   8:             .Build()
   9:             .Run();
  10:     }
  11: }
  12: public class Startup
  13: {   
  14:     public IServiceProvider ConfigureServices(IServiceCollection services)
  15:     {
  16:         IServiceCollection newServices = new ServiceCollection();
  17:         foreach (ServiceDescriptor service in services)
  18:         {
  19:             newServices.Add(service);
  20:         }
  21:  
  22:         return newServices
  23:             .AddSingleton<IFoo, Foo>()
  24:             .AddSingleton<IBar, Bar>()
  25:             .BuildServiceProvider();
  26:     }
  27:     
  28:     public void Configure(IApplicationBuilder app, IFoo foo, IBar bar)
  29:     {
  30:         app.Run(async context =>
  31:         {
  32:             context.Response.ContentType = "text/html";
  33:             await context.Response.WriteAsync($"IFoo=>{foo}<br/>");
  34:             await context.Response.WriteAsync($"IBar=>{bar}");
  35:         });
  36:     }
  37: }

如上面的代碼片段所示,在Startup的ConfigureServices方法中,我們通過拷貝注冊到現有ServiceCollection的所有ServiceDescriptor生成了一個新的ServiceCollection,兩個服務Foo和Bar被注冊到后者之上。該方法最終返回由這個新ServiceCollection創建的ServiceProvider。在另一個Configure方法中,我們添加了兩個類型分別為IFoo和IBar的參數,并以相同的方式將它們的真實類型名稱和注冊服務類型的映射關系作為響應內容。程序運行之后,我們利用瀏覽器進行訪問照樣會得到一樣的結果。

五、ASP.NET Core默認注冊了哪些服務

WebHostBuilder在創建ServiceCollection之后,會注冊一些默認的服務。這些服務和我們自行注冊的服務并沒有任何區別,只要我們知道對應的服務類型,就可以通過注入的方式獲取并使用它們。那么具體由哪些服務被默認注冊了呢?如下所示的是這些服務對應的類型,至于這些服務各自有何用途,我們在這里就先不深究了。

  • IHostingEnvironment
  • ILoggerFactory
  • ILogger<>
  • IApplicationBuilderFactory
  • IHttpContextFactory
  • IOptions<>
  • DiagnosticSource
  • DiagnosticListener
  • IStartupFilter
  • ObjectPoolProvider
  • IStartup

如果我們需要這些預注冊的服務,我們可以按照我們熟悉的方式以依賴注入的方式來使用它們。如下面的代碼片段所示,我們在Startup的Configure方法中直接采用方法注入的方式來使用這些預定義的服務。

   1: public class Program
   2: {
   3:     public static void Main(string[] args)
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .UseStartup<Startup>()
   8:             .Build()
   9:             .Run();
  10:     }
  11: }
  12: public class Startup
  13: {     
  14:     public void Configure(
  15:         IApplicationBuilder app,
  16:         IHostingEnvironment environment,
  17:         ILoggerFactory loggerFactory,
  18:         IHttpContextFactory httpContextFactory,
  19:         DiagnosticSource diagnosticSource,
  20:         DiagnosticListener diagnosticListener)
  21:     {
  22:         app.Run(async context =>
  23:         {
  24:             context.Response.ContentType = "text/html";
  25:             await context.Response.WriteAsync($"IApplicationBuilder=>{app}<br/>");
  26:             await context.Response.WriteAsync($"IHostingEnvironment=>{environment}<br/>");
  27:             await context.Response.WriteAsync($"ILoggerFactory=>{loggerFactory}<br/>");
  28:             await context.Response.WriteAsync($"IHttpContextFactory=>{httpContextFactory}<br/>");
  29:             await context.Response.WriteAsync($"DiagnosticSource=>{diagnosticSource}<br/>");
  30:             await context.Response.WriteAsync($"DiagnosticListener=>{diagnosticListener}");
  31:         });
  32:     }
  33: }

由于Configure方法注冊的Middleware直接將注入服務的注冊類型和真實類型的映射關系作為響應內容,所以我們訪問應用會的得到如下所示的輸出結果。

image

六、ASP.NET Core MVC中的依賴注入

對于ASP.NET MVC 5機器以及之前的版本,在默認情況下定義的Controller都具有一個要求,那就是Controller類型必須具有一個無參數的默認構造函數,否則Controller實例將無法激活。對于自身具有依賴注入功能的ASP.NET Core MVC來說,定義Controller將沒有了這個限制。對于預注冊的服務,我們完全可以采用構造器注入的方式在定義的Controller中使用它們。作為演示,我們對上面這個應用作了如下的改寫。

   1: public class Program
   2: {
   3:     public static void Main(string[] args)
   4:     {
   5:         new WebHostBuilder()
   6:             .UseKestrel()
   7:             .ConfigureServices(services=>services
   8:                 .AddSingleton<IFoo,Foo>()
   9:                 .AddSingleton<IBar,Bar>()
  10:                 .AddMvc())
  11:             .Configure(app=>app.UseMvc())
  12:             .Build()
  13:             .Run();
  14:     }
  15: }
  16:  
  17: public class HomeController
  18: {
  19:     public IFoo Foo { get; private set; }
  20:     public IBar Bar { get; private set; }
  21:  
  22:     public HomeController(IFoo foo, IBar bar)
  23:     {
  24:         this.Foo = foo;
  25:         this.Bar = bar;
  26:     }
  27:  
  28:     [HttpGet("/")]
  29:     public string Index()
  30:     {
  31:         this.HttpContext.Response.ContentType = "text/html";
  32:         return $"IFoo=>{this.Foo}<br/>IBar=>{this.Bar}";
  33:     }       
  34: }

上面這個代碼與之前有一個顯著的區別,那就是我們根本就沒有定義Startup類型,我們將原本實現在它的兩個方法(ConfigureServices和Configure)中的功能移植到了WebHostBuilder的同名方法中,這兩種形式的編程方式其實是等效的。在調用ConfigureServices方法的時候,我們除了注冊MVC相關的服務之外,Foo和Bar這兩個服務也一并進行了注冊。至于另一個Configure方法,我們直接調用其擴展方法MVC注冊與MVC相關的Middleware。

我們定義了一個默認的HomeController,它具有兩個類型分別為IFoo和IBar的只讀屬性,后者在構造函數由傳入的參數進行初始化,我們知道這是構造器注入的編程方式。在Action方法Index中 ,我們依然將這兩個服務的注冊類型和真實類型之間的匹配關系作為響應內容,所以我們訪問這個應用依然會得到如下所示的輸出結果。

image


文章列表


不含病毒。www.avast.com
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()