ASP.NET Core應用的7種依賴注入方式
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); }