基於IHostBuilder/IHost的服務承載系統建立在依賴注入框架之上,它在服務承載過程中依賴的服務(包括作為宿主的IHost對象)都由代表依賴注入容器的IServiceProvider對象提供。在定義承載服務時,也可以采用依賴注入方式來消費它所依賴的服務。作為依賴注入容器的IServiceProvider對象能否提供我們需要的服務實例,取決於相應的服務注冊是否預先添加到依賴注入框架中。服務注冊可以通過調用IHostBuilder接口或者IWebHostBuilder接口相應的方法來完成,前者在《服務承載系統》已經有詳細介紹,下面介紹基於IWebHostBuilder接口的服務注冊。[本文節選自《ASP.NET Core 3框架揭秘》第11章, 更多關於ASP.NET Core的文章請點這里]
目錄
一、服務注冊
二、服務的消費
在Startup中注入服務
在中間件中注入服務
三、生命周期
兩個IServiceProvider對象
基於服務范圍的驗證
四、集成第三方依賴注入框架
一、服務注冊
ASP.NET Core應用提供了兩種服務注冊方式,一種是調用IWebHostBuilder接口的ConfigureServices方法。如下面的代碼片段所示,IWebHostBuilder定義了兩個Configure
Services方法重載,它們的參數類型分別是Action<IServiceCollection>和Action<WebHostBuilderContext, IServiceCollection>,我們注冊的服務最終會被添加到作為這兩個委托對象輸入的IServiceCollection集合中。WebHostBuilderContext代表當前IWebHostBuilder在構建WebHost過程中采用的上下文,我們可以利用它得到當前應用的配置和與承載環境相關的信息。
public interface IWebHostBuilder { IWebHostBuilder ConfigureServices(Action<IServiceCollection> configureServices); IWebHostBuilder ConfigureServices(Action<WebHostBuilderContext, IServiceCollection> configureServices); ... } public class WebHostBuilderContext { public IConfiguration Configuration { get; set; } public IWebHostEnvironment HostingEnvironment { get; set; } }
除了直接調用IWebHostBuilder接口的ConfigureServices方法注冊服務,還可以利用注冊的Startup類型來完成服務的注冊。所謂的Startup類型就是通過調用如下兩個擴展方法注冊到IWebHostBuilder接口上用來對應用程序進行初始化的。由於ASP.NET Core應用針對請求的處理能力與方式完全取決於注冊的中間件,所以這里所謂的針對應用程序的初始化主要體現在針對中間件的注冊上。
public static class WebHostBuilderExtensions { public static IWebHostBuilder UseStartup<TStartup>(this IWebHostBuilder hostBuilder) where TStartup: class; public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Type startupType); }
對於注冊的中間件來說,它往往具有針對其他服務的依賴。當ASP.NET Core框架在創建具體的中間件對象時,會利用依賴注入框架來提供注入的依賴服務。中間件依賴的這些服務自然需要被預先注冊,所以中間件和服務注冊成為Startup對象的兩個核心功能。與中間件類型類似,我們在大部分情況下會采用約定的形式來定義Startup類型。如下所示的代碼片段就是一個典型的Startup的定義,中間件和服務的注冊分別實現在Configure方法和ConfigureServices方法中。由於並不是在任何情況下都有服務注冊的需求,所以ConfigureServices方法並不是必需的。Startup對象的ConfigureServices方法的調用發生在整個服務注冊的最后階段,在此之后,ASP.NET Core應用就會利用所有的服務注冊來創建作為依賴注入容器的IServiceProvider對象。
public class Startup { public void ConfigureServices(IServiceCollection servives); public void Configure(IApplicationBuidler app); }
除了可以采用上述兩種方式為應用程序注冊所需的服務,ASP.NET Core框架本身在構建請求處理管道之前也會注冊一些服務,這些公共服務除了供框架自身消費,也可以供應用程序使用。那么ASP.NET Core框架究竟預先注冊了哪些服務?為了得到這個問題的答案,我們編寫了如下這段簡單的程序。
class Program { static void Main() { Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builder.UseStartup<Startup>()) .Build() .Run(); } } public class Startup { public void ConfigureServices(IServiceCollection services) { var provider = services.BuildServiceProvider(); foreach (var service in services) { var serviceTypeName = GetName(service.ServiceType); var implementationType = service.ImplementationType ?? service.ImplementationInstance?.GetType() ?? service.ImplementationFactory?.Invoke(provider)?.GetType(); if (implementationType != null) { Console.WriteLine($"{service.Lifetime,-15} {GetName(service.ServiceType),-50}{GetName(implementationType)}"); } } } public void Configure(IApplicationBuilder app) { } private string GetName(Type type) { if (!type.IsGenericType) { return type.Name; } var name = type.Name.Split('`')[0]; var args = type.GetGenericArguments().Select(it => it.Name); return $"{name}<{string.Join(",", args)}>"; } }
在如上所示的Startup類型的ConfigureServices方法中,我們從作為參數的IServiceCollection對象中獲取當前注冊的所有服務,並打印每個服務對應的聲明類型、實現類型和生命周期。這段程序執行之后,系統注冊的所有公共服務會以圖11-7所示的方式輸出到控制台上,我們可以從這個列表中發現很多熟悉的類型。
二、服務的消費
ASP.NET Core框架中的很多核心對象都是通過依賴注入方式提供的,如用來對應用進行初始化的Startup對象、中間件對象,以及ASP.NET Core MVC應用中的Controller對象和View對象等,所以我們可以在定義它們的時候采用注入的形式來消費已經注冊的服務。下面簡單介紹幾種服務注入的應用場景。
在Startup中注入服務
構成HostBuilderContext上下文的兩個核心對象(表示配置的IConfiguration對象和表示承載環境的IHostEnvironment對象)可以直接注入Startup構造函數中進行消費。由於ASP.NET Core應用中的承載環境通過IWebHostEnvironment接口表示,IWebHostEnvironment接口派生於IHostEnvironment接口,所以也可以通過注入IWebHostEnvironment對象的方式得到當前承載環境相關的信息。
我們可以通過一個簡單的實例來驗證針對Startup的構造函數注入。如下面的代碼片段所示,我們在調用IWebHostBuilder接口的Startup<TStartup>方法時注冊了自定義的Startup類型。在定義Startup類型時,我們在其構造函數中注入上述3個對象,提供的調試斷言不僅證明了3個對象不為Null,還表明采用IHostEnvironment接口和IWebHostEnvironment接口得到的其實是同一個實例。
class Program { static void Main() { Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builder.UseStartup<Startup>()) .Build() .Run(); } } public class Startup { public Startup(IConfiguration configuration, IHostEnvironment hostingEnvironment, IWebHostEnvironment webHostEnvironment) { Debug.Assert(configuration != null); Debug.Assert(hostingEnvironment != null); Debug.Assert(webHostEnvironment != null); Debug.Assert(ReferenceEquals(hostingEnvironment, webHostEnvironment)); } public void Configure(IApplicationBuilder app) { } }
依賴服務還可以直接注入用於注冊中間件的Configure方法中。如果構造函數注入還可以對注入的服務有所選擇,那么對於Configure方法來說,通過任意方式注冊的服務都可以注入其中,包括通過調用IHostBuilder、IWebHostBuilder和Startup自身的ConfigureServices方法注冊的服務,還包括框架自行注冊的所有服務。
如下面的代碼片段所示,我們分別調用IWebHostBuilder和Startup的ConfigureServices方法注冊了針對IFoo接口與IBar接口的服務,這兩個服務直接注入Startup的Configure方法中。另外,Configure方法要求提供一個用來注冊中間件的IApplicationBuilder對象作為參數,但是對該參數出現的位置並未做任何限制。
class Program { static void Main() { Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builder .UseStartup<Startup>() .ConfigureServices(svcs => svcs.AddSingleton<IFoo, Foo>())) .Build() .Run(); } } public class Startup { public void ConfigureServices(IServiceCollection services) => services.AddSingleton<IBar, Bar>(); public void Configure(IApplicationBuilder app, IFoo foo, IBar bar) { Debug.Assert(foo != null); Debug.Assert(bar != null); } }
在中間件中注入服務
ASP.NET Core請求處理管道最重要的對象是真正用來處理請求的中間件。由於ASP.NET Core在創建中間件對象並利用它們構建整個請求處理管道時,所有的服務都已經注冊完畢,所以注冊的任何一個服務都可以注入中間件類型的構造函數中。如下所示的代碼片段體現了針對中間件類型的構造函數注入。(S1107)
class Program { static void Main() { Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builder.ConfigureServices(svcs => svcs .AddSingleton<FoobarMiddleware>() .AddSingleton<IFoo, Foo>() .AddSingleton<IBar, Bar>()) .Configure(app => app.UseMiddleware<FoobarMiddleware>())) .Build() .Run(); } } public class FoobarMiddleware: IMiddleware { public FoobarMiddleware(IFoo foo, IBar bar) { Debug.Assert(foo != null); Debug.Assert(bar != null); } public Task InvokeAsync(HttpContext context, RequestDelegate next) { Debug.Assert(next != null); return Task.CompletedTask; } }
如果采用基於約定的中間件類型定義方式,注冊的服務還可以直接注入真正用於處理請求的InvokeAsync方法或者Invoke方法中。另外,將方法命名為InvokeAsync更符合TAP(Task-based Asynchronous Pattern)編程模式,之所以保留Invoke方法命名,主要是出於版本兼容的目的。如下所示的代碼片段展示了針對InvokeAsync方法的服務注入。
class Program { static void Main() { Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builder.ConfigureServices(svcs => svcs .AddSingleton<IFoo, Foo>() .AddSingleton<IBar, Bar>()) .Configure(app => app.UseMiddleware<FoobarMiddleware>())) .Build() .Run(); } } public class FoobarMiddleware { private readonly RequestDelegate _next; public FoobarMiddleware(RequestDelegate next) => _next = next; public Task InvokeAsync(HttpContext context, IFoo foo, IBar bar) { Debug.Assert(context != null); Debug.Assert(foo != null); Debug.Assert(bar != null); return _next(context); } }
雖然約定定義的中間件類型和Startup類型采用了類似的服務注入方式,它們都支持構造函數注入和方法注入,但是它們之間有一些差別。中間件類型的構造函數、Startup類型的Configure方法和中間件類型的Invoke方法或者InvokeAsync方法都具有一個必需的參數,其類型分別為RequestDelegate、IApplicationBuilder和HttpContext,對於該參數在整個參數列表的位置,前兩者都未做任何限制,只有后者要求表示當前請求上下文的參數HttpContext必須作為方法的第一個參數。按照上述約定,如下這個中間件類型FoobarMiddleware的定義是不合法的,但是Startup類型的定義則是合法的。對於這一點,筆者認為可以將這個限制放開,這樣不僅可以使中間件類型的定義更加靈活,還能保證注入方式的一致性。
public class FoobarMiddleware { public FoobarMiddleware(RequestDelegate next); public Task InvokeAsync(IFoo foo, IBar bar, HttpContext context); } public class Startup { public void Configure(IFoo foo, IBar bar, IApplicationBuilder app); }
對於基於約定的中間件,構造函數注入與方法注入存在一個本質區別。由於中間件被注冊為一個Singleton對象,所以我們不應該在它的構造函數中注入Scoped服務。Scoped服務只能注入中間件類型的InvokeAsync方法中,因為依賴服務是在針對當前請求的服務范圍中提供的,所以能夠確保Scoped服務在當前請求處理結束之后被釋放。
三、生命周期
當我們調用IServiceCollection相關方法注冊服務的時候,總是會指定一種生命周期。由第3章和第4章的介紹可知,作為依賴注入容器的多個IServiceProvider對象通過ServiceScope 構成一種層次化結構。Singleton服務實例保存在作為根容器的IServiceProvider對象上,而Scoped服務實例以及需要回收釋放的Transient服務實例則保存在當前IServiceProvider對象中,只有不需要回收的Transient服務才會用完就被丟棄。
至於服務實例是否需要回收釋放,取決於服務實現類型是否實現IDisposable接口,服務實例的回收釋放由保存它的IServiceProvider對象負責。具體來說,當IServiceProvider對象因自身的Dispose方法被調用而被回收釋放時,它會調用自身維護的所有服務實例的Dispose方法。對於一個非根容器的IServiceProvider對象來說,其生命周期決定於對應的IServiceScope對象,調用ServiceScope的Dispose方法會導致對封裝IServiceProvider對象的回收釋放。
兩個IServiceProvider對象
如果在一個具體的ASP.NET Core應用中討論服務生命周期會更加易於理解:Singleton是針對應用程序的生命周期,而Scoped是針對請求的生命周期。換句話說,Singleton服務的生命周期會一直延續到應用程序關閉,而Scoped服務的生命周期僅僅與當前請求上下文綁定在一起,那么這樣的生命周期模式是如何實現的?
ASP.NET Core應用針對服務生命周期管理的實現原理其實也很簡單。在應用程序正常啟動后,它會利用注冊的服務創建一個作為根容器的IServiceProvider對象,我們可以將它稱為ApplicationServices。如果應用在處理某個請求的過程中需要采用依賴注入的方式激活某個服務實例,那么它會利用這個IServiceProvider對象創建一個代表服務范圍的IServiceScope對象,后者會指定一個IServiceProvider對象作為子容器,請求處理過程中所需的服務實例均由它來提供,我們可以將它稱為RequestServices。
在處理完當前請求后,這個IServiceScope對象的Dispose方法會被調用,與它綁定的這個IServiceProvider對象也隨之被回收釋放,由它提供的實現了IDisposable接口的Transient服務實例也會隨之被回收釋放,最終由它提供的Scoped服務實例變成可以被GC回收的垃圾對象。表示當前請求上下文的HttpContext類型具有如下所示的RequestServices屬性,它返回的就是這個針對當前請求的IServiceProvider對象。
public abstract class HttpContext { public abstract IServiceProvider RequestServices { get; set; } ... }
為了使讀者對注入服務的生命周期有深刻的認識,下面演示一個簡單的實例。這是一個ASP.NET Core MVC應用,我們在該應用中定義了3個服務接口(IFoo、IBar和IBaz)和對應的實現類(Foo、Bar和Baz),后者派生於實現了IDisposable接口的基類Base。我們分別在Base的構造函數和實現的Dispose方法中輸出相應的文字,以確定服務實例被創建和釋放的時間。
class Program { static void Main() { Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builder .ConfigureServices(svcs => svcs .AddSingleton<IFoo, Foo>() .AddScoped<IBar, Bar>() .AddTransient<IBaz, Baz>() .AddControllersWithViews()) .Configure(app => app .Use(next => httpContext => { Console.WriteLine($"Receive request to {httpContext.Request.Path}"); return next(httpContext); }) .UseRouting() .UseEndpoints(endpoints => endpoints.MapControllers()))) .ConfigureLogging(builder=>builder.ClearProviders()) .Build() .Run(); } } public class HomeController: Controller { private readonly IHostApplicationLifetime _lifetime; public HomeController(IHostApplicationLifetime lifetime, IFoo foo, IBar bar1, IBar bar2, IBaz baz1, IBaz baz2) =>_lifetime = lifetime; [HttpGet("/index")] public void Index() {} [HttpGet("/stop")] public void Stop() => _lifetime.StopApplication(); } public interface IFoo {} public interface IBar {} public interface IBaz {} public class Base : IDisposable { public Base()=> Console.WriteLine($"{this.GetType().Name} is created."); public void Dispose() => Console.WriteLine($"{this.GetType().Name} is disposed."); } public class Foo : Base, IFoo {} public class Bar : Base, IBar {} public class Baz : Base, IBaz {}
在注冊ASP.NET Core MVC框架相關的服務之前,我們采用不同的生命周期對這3個服務進行了注冊。為了確定應用程序何時開始處理接收的請求,可以利用注冊的中間件打印出當前請求的路徑。我們在HomeController的構造函數中注入了上述3個服務和1個用來遠程關閉應用的IHostApplicationLifetime服務,其中IBar和IBaz被注入了兩次。HomeController包含Index和Stop兩個Action方法,它們的路由指向的路徑分別為“/index”和“/stop”,Stop方法利用注入的IHostApplicationLifetime服務關閉當前應用。
我們先采用命令行的形式來啟動該應用程序,然后利用瀏覽器依次向該應用發送3個請求,前兩個請求指向Action方法Index(“/index”),后一個指向Action方法Stop(“ /stop”),此時控制台上出現的輸出結果如下圖所示。由輸出結果可知:由於IFoo服務采用的生命周期模式為Singleton,所以在整個應用的生命周期中只會創建一次。對於每個接收的請求,雖然IBar和IBaz都被注入了兩次,但是采用Scoped模式的Bar對象只會被創建一次,而采用Transient模式的Baz對象則被創建了兩次。再來看釋放服務相關的輸出,采用Singleton模式的IFoo服務會在應用被關閉的時候被釋放,而生命周期模式分別為Scoped和Transient的IBar服務與IBaz服務都會在應用處理完當前請求之后被釋放。(S1110)
基於服務范圍的驗證
由《依賴注入[8]:服務實例的生命周期》的介紹可知,Scoped服務既不應該由作為根容器的ApplicationServices來提供,也不能注入一個Singleton服務中,否則它將無法在請求結束之后釋放。如果忽視了這個問題,就容易造成內存泄漏,下面是一個典型的例子。
如下所示的實例程序使用了一個名為FoobarMiddleware的中間件。在該中間件初始化過程中,它需要從數據庫中加載由Foobar類型表示的數據。在這里我們采用Entity Framework Core提供的基於SQL Server的數據訪問,所以可以為實體類型Foobar定義對應的FoobarDbContext,它以服務的形式通過調用IServiceCollection的AddDbContext<TDbContext>擴展方法進行注冊,注冊的服務默認采用Scoped生命周期。
class Program { static void Main() { Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builder .UseDefaultServiceProvider(options=>options.ValidateScopes = false) .ConfigureServices(svcs => svcs.AddDbContext<FoobarDbContext>(options=>options.UseSqlServer("connection string"))) .Configure(app =>app.UseMiddleware<FoobarMiddleware>())) .Build() .Run(); } } public class FoobarMiddleware { private readonly RequestDelegate _next; private readonly Foobar _foobar; public FoobarMiddleware(RequestDelegate next, FoobarDbContext dbContext) { _next = next; _foobar = dbContext.Foobar.SingleOrDefault(); } public Task InvokeAsync(HttpContext context) { ... return _next(context); } } public class Foobar { [Key] public string Foo { get; set; } public string Bar { get; set; } } public class FoobarDbContext : DbContext { public DbSet<Foobar> Foobar { get; set; } public FoobarDbContext(DbContextOptions options) : base(options){} }
采用約定方式定義的中間件實際上是一個Singleton對象,而且它是在應用初始化過程中由根容器的IServiceProvider對象創建的。由於FoobarMiddleware的構造函數中注入了FoobarDbContext對象,所以該對象自然也由同一個IServiceProvider對象來提供。這就意味着FoobarDbContext對象的生命周期會延續到當前應用程序被關閉的那一刻,造成的后果就是數據庫連接不能及時地被釋放。
在一個ASP.NET Core應用中,如果將服務的生命周期注冊為Scoped模式,那么我們希望服務實例真正采用基於請求的生命周期模式。由第4章的介紹可知,我們可以通過啟用針對服務范圍的驗證來避免采用作為根容器的IServiceProvider對象來提供Scoped服務實例。我們只需要調用IWebHostBuilder接口的兩個UseDefaultServiceProvider方法重載將ServiceProviderOptions的ValidateScopes屬性設置為True即可。
public static class WebHostBuilderExtensions { public static IWebHostBuilder UseDefaultServiceProvider(this IWebHostBuilder hostBuilder, Action<ServiceProviderOptions> configure); public static IWebHostBuilder UseDefaultServiceProvider(this IWebHostBuilder hostBuilder, Action<WebHostBuilderContext, ServiceProviderOptions> configure); } public class ServiceProviderOptions { public bool ValidateScopes { get; set; } public bool ValidateOnBuild { get; set; } }
出於性能方面的考慮,如果在Development環境下調用Host的靜態方法CreateDefaultBuilder來創建IHostBuilder對象,那么該方法會將ValidateScopes屬性設置為True。在上面演示的實例中,我們刻意關閉了針對服務范圍的驗證,如果將這行代碼刪除,在開發環境下啟動該程序之后會出現下圖所示的異常。
如果確實需要在中間件中注入Scoped服務,可以采用強類型(實現IMiddleware接口)的中間件定義方式,並將中間件以Scoped服務進行注冊即可。如果采用基於約定的中間件定義方式,我們有兩種方案來解決這個問題:第一種解決方案就是按照如下所示的方式在InvokeAsync方法中利用HttpContext的RequestServices屬性得到基於當前請求的IServiceProvider對象,並利用它來提供依賴的服務。
public class FoobarMiddleware { private readonly RequestDelegate _next; public FoobarMiddleware(RequestDelegate next)=> _next = next; public Task InvokeAsync(HttpContext context) { var dbContext = context.RequestServices.GetRequiredService<FoobarDbContext>(); Debug.Assert(dbContext != null); return _next(context); } }
第二種解決方案則是按照如下所示的方式直接在InvokeAsync方法中注入依賴的服務。我們在上面介紹兩種中間件定義方式時已經提及:InvokeAsync方法注入的服務就是由基於當前請求的IServiceProvider對象提供的,所以這兩種解決方案其實是等效的。
public class FoobarMiddleware { private readonly RequestDelegate _next; public FoobarMiddleware(RequestDelegate next)=> _next = next; public Task InvokeAsync(HttpContext context) { var dbContext = context.RequestServices.GetRequiredService<FoobarDbContext>(); Debug.Assert(dbContext != null); return _next(context); } }
四、集成第三方依賴注入框架
由《服務承載系統[6]: 承載服務啟動流程[下篇]》的介紹可知,通過調用IHostBuilder接口的UseServiceProviderFactory<TContainerBuilder> 方法注冊IServiceProviderFactory<TContainerBuilder>工廠的方式可以實現與第三方依賴注入框架的整合。該接口定義的ConfigureContainer<TContainerBuilder>方法可以對提供的依賴注入容器做進一步設置,這樣的設置同樣可以定義在注冊的Startup類型中。
《依賴注入[4]:一個Mini版的依賴注入框架》創建了一個名為Cat的簡易版依賴注入框架,並在第4章為其創建了一個IServiceProviderFactory<TContainerBuilder>實現,具體類型為CatServiceProvider,下面演示如何通過注冊這個CatServiceProvider實現與第三方依賴注入框架Cat的整合。如果使用Cat框架,我們可以通過在服務類型上標注MapToAttribute特性的方式來定義服務注冊信息。在創建的演示程序中,我們采用如下方式定義了3個服務(Foo、Bar和Baz)和對應的接口(IFoo、IBar和IBaz)。
public interface IFoo { } public interface IBar { } public interface IBaz { } [MapTo(typeof(IFoo), Lifetime.Root)] public class Foo : IFoo { } [MapTo(typeof(IBar), Lifetime.Root)] public class Bar : IBar { } [MapTo(typeof(IBaz), Lifetime.Root)] public class Baz : IBaz { }
在如下所示的代碼片段中,我們調用IHostBuilder接口的UseServiceProviderFactory方法注冊了CatServiceProviderFactory工廠。我們將針對Cat框架的服務注冊實現在注冊Startup類型的ConfigureContainer方法中,這是除Configure方法和ConfigureServices方法外的第三個約定的方法。我們將CatBuilder對象作為該方法的參數,並調用它的Register方法實現了針對當前程序集的批量服務注冊。
class Program { static void Main() { Host.CreateDefaultBuilder() .ConfigureWebHostDefaults(builder => builder.UseStartup<Startup>()) .UseServiceProviderFactory(new CatServiceProviderFactory()) .Build() .Run(); } } public class Startup { public void Configure(IApplicationBuilder app, IFoo foo, IBar bar, IBaz baz) { app.Run(async context => { var response = context.Response; response.ContentType = "text/html"; await response.WriteAsync($"foo: {foo}<br/>"); await response.WriteAsync($"bar: {bar}<br/>"); await response.WriteAsync($"baz: {baz}<br/>"); }); } public void ConfigureContainer(CatBuilder container) => container.Register(Assembly.GetEntryAssembly()); }
為了檢驗ASP.NET Core能否利用Cat框架來提供所需的服務,我們將注冊的3個服務直接注入Startup類型的Configure方法中。我們在該方法中利用注冊的中間件將這3個注入的服務實例的類型寫入相應的HTML文檔中。如果利用瀏覽器訪問該應用,得到的輸出結果如下圖所示。
ASP.NET Core編程模式[1]:管道式的請求處理
ASP.NET Core編程模式[2]:依賴注入的運用
ASP.NET Core編程模式[3]:配置多種使用形式
ASP.NET Core編程模式[4]:基於承載環境的編程
ASP.NET Core編程模式[5]:如何放置你的初始化代碼