ASP.NET Core框架中的很多核心對象都是通過依賴注入方式提供的,如用來對應用進行初始化的Startup對象、中間件對象,以及ASP.NET Core MVC應用中的Controller對象和View對象等,所以我們可以在定義它們的時候采用注入的形式來消費已經注冊的服務。下面簡單介紹幾種服務注入的應用場景。本篇文章節選自《ASP.NET Core 3框架揭秘》,針對本書的5折優惠還有最后2天,有興趣可以掃描右邊二維碼或者從這里入群購買。。
一、在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) { } }
二、在Startup類型的Configure方法中注入
依賴服務還可以直接注入用於注冊中間件的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在創建中間件對象並利用它們構建整個請求處理管道時,所有的服務都已經注冊完畢,所以任何一個注冊的服務都可以注入中間件類型的構造函數中。如下所示的代碼片段體現了針對中間件類型的構造函數注入。
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; } }
四、在中間件類型的Invoke/InvokeAsync方法中注入
如果采用基於約定的中間件類型定義方式,注冊的服務還可以直接注入真正用於處理請求的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的定義是不合法的,但是Starup類型的定義則是合法的。對於這一點,筆者認為可以將這個限制放開,這樣不僅使中間件類型的定義更加靈活,還能保證注入方式的一致性。
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服務在當前請求處理結束之后被釋放。
五、在Controller類型的構造函數中注入
在一個ASP.NET Core MVC應用中,我們可以在定義的Controller中以構造函數注入的方式注入所需的服務。在如下所示的代碼片段中,我們將IFoobar服務注入到HomeController的構造函數中。
class Program { static void Main() { Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builder .ConfigureServices(svcs => svcs .AddSingleton<IFoobar, Foobar>() .AddSingleton<IBar, Bar>() .AddControllersWithViews()) .Configure(app => app .UseRouting() .UseEndpoints(endpoints => endpoints.MapControllers()))) .Build() .Run(); } } public class HomeController : Controller { public HomeController(IFoobar foobar) => Debug.Assert(foobar != null); }
六、在Controller的Action方法中注入
借助於ASP.NET Core MVC基於模型綁定的參數綁定機制,我們可以將注冊的服務綁定到目標Action方法的參數上,進而實現針對Action方法的依賴注入。在采用這種類型的注入方式時,我們需要在注入參數上按照如下的方式標注FromServicesAttribute特性,用以確定參數綁定的來源是注冊的服務。
class Program { static void Main() { Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builder .ConfigureServices(svcs => svcs .AddSingleton<IFoobar, Foobar>() .AddControllersWithViews()) .Configure(app => app .UseRouting() .UseEndpoints(endpoints => endpoints.MapControllers()))) .Build() .Run(); } } public class HomeController: Controller { [HttpGet("/")] public void Index([FromServices]IFoobar foobar) { Debug.Assert(foobar != null); } }
七、在視圖中注入
在ASP.NET Core MVC應用中,我們還可以將服務注冊到現的View中。假設我們定義了如下這個簡單的MVC程序,並定義了一個簡單的HomeController。
class Program { static void Main() { Host.CreateDefaultBuilder().ConfigureWebHostDefaults(builder => builder .ConfigureServices(svcs => svcs .AddSingleton<IFoobar, Foobar>() .AddControllersWithViews()) .Configure(app => app .UseRouting() .UseEndpoints(endpoints => endpoints.MapControllers()))) .Build() .Run(); } } public class HomeController: Controller { [HttpGet("/")] public IActionResult Index() => View(); }
我們為HomeController定義了一個路由指向根路徑(“/”)的Action方法Index,該方法在調用View方法呈現默認的View之前,將注入的IFoo服務以ViewBag的形式傳遞到View中。如下所示的代碼片段是這個Action方法對應View(/Views/Home/Index.cshtml)的定義,我們通過@inject指令注入了IFoobar服務,並將屬性名設置為Foobar,這意味着當前View對象將添加一個Foobar屬性來引用注入的服務。
@inject IFoobar Foobar @ { Debug.Assert(Foobar!= null); }