在對ASP.NET Core管道中關於依賴注入的兩個核心對象(ServiceCollection和ServiceProvider)有了足夠的認識之後,我們將關注的目光轉移到編程層面。在ASP.NET Core應用中基於依賴注入的編程主要涉及到兩個方面,它們分別是將服務注冊到ServiceCollection中,和采用注入的方式利用ServiceProvider提供我們所需的服務。我們先來討論ASP.NET Core應用中如何進行服務注冊。[本文已經同步到《ASP.NET Core框架揭秘》之中]
目錄
一、服務注冊
系統自動注冊的服務
手工注冊的服務
二、以注入的形式提取服務
啟動類型的構造函數和Configure方法種注入服務
中間件類型的構造函數和Invoke方法中注入服務
Controller類型的構造函數中注入服務
View中注入服務
三、與第三方DI框架的整合
就注冊的主體來劃分,ASP.NET Core應用中注冊的服務大體可以分為兩種類型,一種是WebHostBuilder在創建WebHost之前自動注冊的服務,這些服務確保了後續管道能夠順利構建並能提供基本的請求處理能力。另一種則是用戶根據自身的需要注冊的,如果系統自動注冊的服務不符合我們的需求,我們也可以注冊自己的服務來覆蓋它。
那麼系統在構建ASP.NET Core管道的時候到底自行注冊了那些服務呢?對於這個問題,我們不用去查看相關的源代碼如何編寫,而只需要編寫如下一個簡單的程序就可以將這些服務輸出來。
1: public class Program
2: {
3: public static void Main()
4: {
5: Console.WriteLine("{0,-30}{1,-15}{2}", "Service Type", "Lifetime", "Implementation");
6: Console.WriteLine("-------------------------------------------------------------");
7:
8: new WebHostBuilder()
9: .UseKestrel()
10: .Configure(app => { })
11: .ConfigureServices(svcs =>
12: {
13: IServiceProvider serviceProvider = svcs.BuildServiceProvider();
14: foreach (var svc in svcs)
15: {
16: if (null != svc.ImplementationType)
17: {
18: Console.WriteLine("{0,-30}{1,-15}{2}", svc.ServiceType.Name, svc.Lifetime, svc.ImplementationType.Name);
19: continue;
20: }
21: object instance = serviceProvider.GetService(svc.ServiceType);
22: Console.WriteLine("{0,-30}{1,-15}{2}", svc.ServiceType.Name, svc.Lifetime, instance.GetType().Name);
23: }
24:
25: })
26: .Build();
27: }
28: }
如上面的代碼片斷所示,我們利用WebHostBuilder創建了一個WebHost對象。在此之前,我們調用擴展方法UseKestrel注冊了一個KestrelServer類型的服務器,指定一個空的Action<IApplicationBuilder>對象作為參數調用了它的Configure方法,我們只得到這樣的方法調用會創建了一個DelegateStartup對象。我們直接利用ConfigureServices方法得到所有自動注冊的服務,並打印出每個服務的注冊類型、生命周期模式和實現類型。當我們運行這個程序之後,控制台上將打印出如下圖所示的服務列表。對於列出的這些服務,我們是不是看到很多熟悉的身影?
如果具體的項目需要采用依賴注入的方式來完成一些業務功能的實現,那就需要在應用初始化的過程中手工注冊相應的服務。初次之外,我們也可以采用手工注冊服務的方式來覆蓋系統自動注冊的服務。總的來說,我們可以采用種方式實現對服務的手工注冊,其中一種就是按照如下的形式調用WebHostBuilder的ConfigureServices方法來注冊服務,而另一種則是將服務注冊實現在啟動類的ConfigureServices方法中。
注冊方式1:
1: new WebHostBuilder()
2: .ConfigureServices(svcs => svcs
3: .AddTransient<IFoo, Foo>()
4: .AddScoped<IBar, IBar>()
5: .AddSingleton<IBaz, Baz>())
6: …
注冊方式2:
1: public class Startup
2: {
3: public void ConfigureServices(IServiceCollection svcs)
4: {
5: svcs.AddTransient<IFoo, Foo>()
6: .AddScoped<IBar, IBar>()
7: .AddSingleton<IBaz, Baz>();
8: }
9: …
10: }
通過前面的介紹,我們知道這兩種方式真正執行服務注冊的時機是不同的。第一種形式的服務注冊發生在WebHostBuilder創建WebHost之前,包含這些服務的ServiceCollection以及由此創建的ServiceProvider將直接提供給後續創建的WebHost。而第二種形式的服務注冊則發生在WebHost初始化過程中,實際上是借助一個ConventionBasedStartup對象來完成的。
依賴注入的最終目錄在於實現以注入的形式來消費預先注冊的服務。在一個ASP.NET Core應用中,我們在很多地方都可以采用這種編程方式,我們在前一章中對此也有所提及。經過我的總結,我們常用的依賴注入編程主要應用在如下幾個方面:
當我們在定義啟動類型的時候,通過調用WebHostBuilder的ConfigureServices方法注冊的服務可以在啟動類的構造函數中進行注入,而啟動類的Configure方法不但可以注入調用WebHostBuilder的ConfigureServices方法注冊的服務,也可以注入自身ConfigureServices方法注冊的服務。如下所示的代碼片斷展示了一個比較典型的例子。
1: new WebHostBuilder()
2: .UseKestrel()
3: .ConfigureServices(svcs => svcs
4: .AddSingleton<IFoo, Foo>()
5: .AddSingleton<IBar, Bar>())
6: .UseStartup<Startup>()
7: …
8:
9: public class Startup
10: {
11: public Startup(IFoo foo, IBar bar)
12: {
13: Debug.Assert(typeof(Foo).IsInstanceOfType(foo));
14: Debug.Assert(typeof(Bar).IsInstanceOfType(bar));
15: }
16:
17: public void ConfigureServices(IServiceCollection svcs)
18: {
19: svcs.AddTransient<IBaz, Baz>()
20: .AddTransient<IGux, Gux>();
21: }
22:
23: public void Configure(IApplicationBuilder app, IFoo foo, IBar bar, IBaz baz, IGux gux)
24: {
25: Debug.Assert(typeof(Foo).IsInstanceOfType(foo));
26: Debug.Assert(typeof(Bar).IsInstanceOfType(bar));
27: Debug.Assert(typeof(Baz).IsInstanceOfType(baz));
28: Debug.Assert(typeof(Gux).IsInstanceOfType(gux));
29: }
30: }
當我們按照約定定義中間件類型的時候,我們可以在構造函數定義相應的參數來注入通過任何形式注冊的服務。如下面的代碼片斷所示,中間件類型的構造函數和Invoke方法都定義了相應的參數來以注入的形式和獲取通過調用WebHostBuilder的ConfigureServices方法注冊的兩個服務。
1: new WebHostBuilder()
2: .UseKestrel()
3: .ConfigureServices(svcs => svcs
4: .AddSingleton<IFoo, Foo>()
5: .AddSingleton<IBar, Bar>())
6: .Configure(app=>app.UseMiddleware<FoobarMiddleware>())
7: ...
8:
9: public class FoobarMiddleware
10: {
11: private RequestDelegate _next;
12: public FoobarMiddleware(RequestDelegate next, IFoo foo, IBar bar)
13: {
14: _next = next;
15: Debug.Assert(typeof(Foo).IsInstanceOfType(foo));
16: Debug.Assert(typeof(Bar).IsInstanceOfType(bar));
17: }
18:
19: public async Task Invoke(HttpContext context, IFoo foo, IBar bar)
20: {
21: Debug.Assert(typeof(Foo).IsInstanceOfType(foo));
22: Debug.Assert(typeof(Bar).IsInstanceOfType(bar));
23: await _next(context);
24: }
25: }
在ASP.NET Core MVC應用中,我們經常在Controller類型的構造函數定義相應的參數來以注入的方式獲取預先注冊的服務。如下所示的這個HomeController就采用構造器注入的方式獲取通過調用WebHostBuilder的ConfigureServices方法注冊的兩個服務。
1: new WebHostBuilder()
2: .UseKestrel()
3: .ConfigureServices(svcs => svcs
4: .AddSingleton<IFoo, Foo>()
5: .AddSingleton<IBar, Bar>()
6: .AddMvc())
7: .Configure(app => app.UseMvc())
8: ...
9:
10: public class HomeController
11: {
12: public HomeController(IFoo foo, IBar bar)
13: {
14: Debug.Assert(typeof(Foo).IsInstanceOfType(foo));
15: Debug.Assert(typeof(Bar).IsInstanceOfType(bar));
16: }
17: ...
18: }
如果我們在ASP.NET Core MVC應用的View中以注入的方式進行服務消費,我們有兩種解決方案。第一種方案就是先按照上面這種方式將服務注入到Controller中,在將注入的服務通過ViewData或者ViewBag傳遞到View。另一種方式就是按照如下的方式直接使用@inject指令將注入的服務定義成當前View類型的屬性。
1: new WebHostBuilder()
2: .UseKestrel()
3: .UseContentRoot(Directory.GetCurrentDirectory())
4: .ConfigureServices(svcs => svcs
5: .AddSingleton<IFoo, Foo>()
6: .AddSingleton<IBar, Bar>()
7: .AddMvc())
8: .Configure(app => app.UseMvc())
9: ...
10:
11: @using System.Reflection
12: @using System.Diagnostics
13: @inject IFoo Foo
14: @inject IBar Baz
15: @{
16: Debug.Assert(typeof(Foo).IsInstanceOfType(this.Foo));
17: Debug.Assert(typeof(Bar).IsInstanceOfType(this.Bar));
18: }
我們知道啟動類型的ConfigureServices方法是可以返回一個ServiceProvider對象的,並且這個對象將直接作為WebHost的Services屬性,成為一個全局單例的服務提供者。這個特性可以幫助我們實現與第三方DI框架的整合(比如Castle、Ninject、Autofac等)。在這裡我不想“節外生枝”地引入某一個DI框架,而是自行創建一個簡單的DI容器來演示這個主題。這個DI容器通過如下所示的Cat類型(這麼名字來源於“機器貓”),它直接實現了IServiceProvider接口,所以一個Cat對象同時也是一個ServiceProvider對象。
1: public class Cat : IServiceProvider
2: {
3: private static readonly Cat _instance = new Cat();
4: private ConcurrentDictionary<Type, Func<Cat, object>> _registrations = new ConcurrentDictionary<Type, Func<Cat, object>>();
5: private IServiceProvider _backup;
6:
7: private Cat()
8: {
9: _backup = new ServiceCollection().BuildServiceProvider();
10: }
11:
12: public static Cat Instance
13: {
14: get { return _instance; }
15: }
16:
17: public Cat Register(IServiceCollection svcs)
18: {
19: _backup = svcs.BuildServiceProvider();
20: return this;
21: }
22:
23: public Cat Register(Type serviceType, Func<Cat, object> instanceAccessor)
24: {
25: _registrations[serviceType] = instanceAccessor;
26: return this;
27: }
28:
29: public object GetService(Type serviceType)
30: {
31: Func<Cat, object> instanceAccessor;
32: return _registrations.TryGetValue(serviceType, out instanceAccessor)? instanceAccessor(this): _backup.GetService(serviceType);
33: }
34: }
如上面的代碼片斷所示,Cat具有一個類型為ConcurrentDictionary<Type, Func<Cat, object>>類型的字段(_registrations)用來保存注冊的服務,而服務的注冊體現為服務類型與一個提供服務實例的委托對對象的映射,該映射通過調用第一個Register方法重載進行注冊。除此之外,我還為這個類型定義了一個IServiceProvider接口類型的字段(_backup),如果實現的GetService方法不能根據指定的服務類型找到一個對應的Func<Cat, object>對象來提供服務對象,它將使用這個作為“後備”的ServiceProvider來提供這個服務。我們采用單例模式來使用Cat,這個單例對象通過只讀屬性Instance返回。
針對Cat這個DI容器的整體體現在如下這段程序中。如下面的代碼片段所示,我們一共注冊了三個服務,其中針對IFoo接口的服務直接注冊在Cat單例對象上,針對IBar接口的服務通過調用ConfigureServices方法注冊到WebHostBuilder上,而針對IBaz接口的服務則通過啟動類的ConfiguresServices進行注冊。值得注意的是,啟動類的ConfigureServices方法返回的ServiceProvider正是這個Cat單例對象,在這之前我們調用它的Register方法將當前的ServiceCollection進行了注冊。
1: public class Program
2: {
3: public static void Main()
4: {
5: Cat.Instance.Register(typeof(IFoo), _ => new Foo());
6: new WebHostBuilder()
7: .UseKestrel()
8: .ConfigureServices(svcs => svcs.AddSingleton<IBar, Bar>())
9: .UseStartup<Startup>()
10: .Build()
11: .Run();
12: }
13: }
14:
15: public class Startup
16: {
17: public IServiceProvider ConfigureServices(IServiceCollection svcs)
18: {
19: return Cat.Instance.Register(svcs.AddSingleton<IBaz, Baz>());
20: }
21:
22: public void Configure(IApplicationBuilder app, IFoo foo, IBar bar, IBaz baz)
23: {
24: app.Run(async context =>{
25: context.Response.ContentType = "text/html";
26: await context.Response.WriteAsync($"IFoo => {foo.GetType().Name}<br/>");
27: await context.Response.WriteAsync($"IBar => {bar.GetType().Name}<br/>");
28: await context.Response.WriteAsync($"IBaz => {baz.GetType().Name}<br/>");
29: });
30: }
31: }
我們為啟動類的Configure方法定了三個參數以注入的形式獲取預先注冊的這三個服務對象,並利用注冊的中間件將服務的接口類型和真實類型之間的映射作為了響應的內容。我們啟動應用並利用浏覽器訪問目標地址,這個類型映射關系將會按照如圖5所示的形式出現在浏覽器上。