1. 程式人生 > 其它 >《ASP.NET Core 6 框架揭祕》第三章讀書筆記 - 依賴注入(下)

《ASP.NET Core 6 框架揭祕》第三章讀書筆記 - 依賴注入(下)

整個 ASP.NET Core 是建立在依賴注入框架之上的。

3.1 利用容器提供服務

本章主要介紹這個獨立的基礎框架,不涉及它在 ASP.NET Core 框架中的應用。

3.1.1 服務的註冊與消費 

這個框架主要涉及兩個 NuGet 包,介面和型別都定義在 “Microsoft.Extensions.DependencyInjection.Abstraction” 包,具體實現在“Microsoft.Extensions.DependencyInjection”包。

新增的服務註冊被儲存在 IServiceCollection 介面表示的集合中,並通過集合建立表示依賴注入容器的 IServiceProvider 物件。

依賴注入框架使用 ServiceLifetime 列舉表示 Singleton,Scoped 和 Transient 這三種生命週期模式。

具體的服務註冊方式主要體現為以下三種形式:

  1)指定具體的服務實現型別;

  2)提供一個現成的服務例項;

  3)指定一個建立服務例項的工廠。

 1     var provider = new ServiceCollection()
 2                             .AddTransient<IFoo, Foo>()
 3                             .AddScoped<IBar>(_ => new
Bar()) 4 .AddSingleton<IBaz, Baz>() 5 .AddTransient(typeof(IFoobar<,>), typeof(Foobar<,>)) 6 .BuildServiceProvider(); 7 8 Debug.Assert(provider.GetService<IFoo>() is Foo); 9 Debug.Assert(provider.GetService<IBar>() is
Bar); 10 Debug.Assert(provider.GetService<IBaz>() is Baz); 11 12 var foobar = (Foobar<IFoo, IBar>)provider.GetService<IFoobar<IFoo, IBar>>(); 13 14 Debug.Assert(foobar?.Foo is Foo); 15 16 provider.Dispose();

輸出如下:

可以為同一型別新增多個服務註冊,所有服務註冊都是有效的,但 GetService<T> 擴充套件方法只能返回一個例項,框架採用的是“後來居上”的策略。GetServices<T> 擴充套件方法將指定服務型別的所有註冊服務來提供一組服務例項。

1   var baseServices = provider.GetServices<Base>();
2    Debug.Assert(baseServices.OfType<Foo>().Any());
3    Debug.Assert(baseServices.OfType<Bar>().Any());
4    Debug.Assert(baseServices.OfType<Baz>().Any());

甚至,呼叫 GetService<T> 或 GetServices<T> 時將 T 設定為 IServiceProvider,我們可以得到容器物件本身。

3.1.2 生命週期

表示依賴注入容器的 IServiceProvider 物件之間的層次關係組成了服務例項的 3 種生命週期。Singleton 服務例項儲存在作為根容器的 IServiceProvider 物件上,所以能在多個同根 IServiceProvider 物件之間保證真正的單例。Scoped 服務例項被儲存在當前服務範圍對應的 IServiceProvider 上,從而保證當前服務範圍之內提供單例。對應型別沒有實現 IDisposable 的 Transient 服務例項,遵循“即用即建,用後即棄”的渣男策略。

 1     var provider = new ServiceCollection()
 2                .AddTransient<IFoo, Foo>()
 3                         .AddScoped<IBar>(_ => new Bar())
 4                         .AddSingleton<IBaz, Baz>()
 5                         .BuildServiceProvider();
 6 
 7        Console.WriteLine("Root provider start working..");
 8 
 9        provider.GetService<IFoo>();
10        provider.GetService<IBar>();
11        provider.GetService<IBaz>();
12 
13        Console.WriteLine();
14 
15        Console.WriteLine("Child1 provider start working..");
16 
17        var childProvider1 = provider.CreateScope().ServiceProvider;
18        var childProvider2 = provider.CreateScope().ServiceProvider;
19 
20        GetServices<IFoo>(childProvider1);
21        GetServices<IBar>(childProvider1);
22        GetServices<IBaz>(childProvider1);
23 
24        Console.WriteLine();
25 
26        Console.WriteLine("Child2 provider start working..");
27 
28        GetServices<IFoo>(childProvider2);
29        GetServices<IBar>(childProvider2);
30        GetServices<IBaz>(childProvider2);

容器不僅負責構建並提供服務例項,還負責管理服務例項的生命週期。策略如下:

  1)Transient 和 Scoped:所有實現了 IDisposable 介面的服務例項會被當前 IServiceProvider 物件儲存,當 IServiceProvider 的 Dispose 被呼叫時,這些服務例項                      的 Dispose  會隨之被呼叫。

  2)Singleton:當根容器的 Dispose 被呼叫時,這些服務例項的 Dispose  才會隨之被呼叫。

根容器與應用具有一致的生命週期因而被稱為 ApplicationServices。服務範圍所在的 IServiceProvider 物件被稱為 RequestServices,請求處理完成之後(怎麼算完成?比如使用 using),RequestServices 被釋放,在該範圍內建立的 Scoped 服務例項和實現了 IDisposable 介面的 Transient 服務例項被及時釋放。

 1     using(var rootProvider = new ServiceCollection()
 2                                        .AddTransient<IFoo, Foo>()
 3                                        .AddScoped<IBar, Bar>()
 4                                        .AddSingleton<IBaz, Baz>()
 5                                        .BuildServiceProvider()){
 6                 
 7          using(var scope = rootProvider.CreateScope()){
 8             var scopedProvider = scope.ServiceProvider;
 9 
10              scopedProvider.GetService<IFoo>();
11              scopedProvider.GetService<IBar>();
12              scopedProvider.GetService<IBaz>();
13 
14              Console.WriteLine("Scoped container is disposing...");
15           }
16 
17           Console.WriteLine("Root container is disposing...");
18        }

3.1.3 服務註冊的驗證

根容器提供的 Scoped 服務也是單例的。如果一個單例服務依賴另一個 Scoped 服務,那麼這個  Scoped 服務會被一個 Singleton 服務所引用,這意味著這個  Scoped 服務例項也成為一個單例服務。

 1 using(var rootProvider = new ServiceCollection()
 2                                             .AddTransient<IFoo, Foo>()
 3                                             .AddScoped<IBar, Bar>()
 4                                             .AddSingleton<IBaz, Baz>()
 5                                             .AddSingleton(typeof(IFoobar<,>), typeof(Foobar<,>))
 6                                             .BuildServiceProvider()){
 7                 using(var scope = rootProvider.CreateScope()){
 8                     var scopedProvider = scope.ServiceProvider;
 9 
10                     Console.WriteLine("Scoped container created instances..");
11 
12                     scopedProvider.GetService<IFoo>();
13                     scopedProvider.GetService<IBar>();
14                     scopedProvider.GetService<IBaz>();
15 
16                     var foobar = (Foobar<IFoo, IBar>)scopedProvider.GetService<IFoobar<IFoo, IBar>>();
17 
18                     Console.WriteLine("Scoped container is disposing...");                    
19                 }
20 
21                 Console.WriteLine();
22 
23                 Console.WriteLine("Root container is disposing...");
24             }

注意第 16 行程式碼,這裡的單例 IFoobar<,> 引用了 transient 服務 IFoo 和 scoped 服務 IBar,看輸出可以發現在根容器 Dispose 時的資訊輸出。

在 ASP.NET Core 應用中,一般只會將請求具有一致生命週期的服務註冊為 Scope 模式。一旦出現上面提到的情況,可能會造成嚴重的記憶體洩漏問題,框架提供了對應的驗證機制。只需要在呼叫 BuildServiceProvider 方法時提供一個引數為 true 即可。

這裡的執行結果和書上的不一致,只有前兩次次是失敗了,目前還不知道原因。

服務範圍的檢驗體現在 ServiceProviderOptions 配置選項的 ValidateScopes 屬性上。ServiceProviderOptions 還有一個屬性名為 ValidateOnBuild 的屬性。如果設定為 true,意味著 IServiceProvider 物件被構建時會對每個 ServiceDescriptor 物件實施有效性驗證。

 1   try{
 2      var options = new ServiceProviderOptions{
 3         ValidateOnBuild = true
 4       };
 5 
 6       var provider = new ServiceCollection()
 7                            .AddSingleton<IFoo, Foo2>()
 8                            .BuildServiceProvider(options);
 9 
10       Console.WriteLine($"Status: Success.");
11   }
12   catch(Exception ex){
13     Console.WriteLine($"Status: Fail.");
14     Console.WriteLine($"Error: {ex.Message}");
15   }

這裡用到的 Foo2 只有一個私有構造器,因此建立容器時報錯。

3.2 服務註冊

IServiceCollection 物件是一個存放服務註冊資訊的集合,具體的服務註冊體現為 ServiceDescriptor 物件。

3.2.1 ServiceDescriptor

ServiceDescriptor 是對單個服務註冊項的描述。

採用現成的服務例項建立的 ServiceDescriptor 物件預設採用 Singleton 生命週期模式。對於其它兩種形式建立 ServiceDescriptor 物件,需要顯式指定生命週期模式。

可以利用定義在 ServiceDescriptor 型別中的一系列靜態方法來建立 ServiceDescriptor 物件,比如 Describe 方法。

3.2.2 IServiceCollection

服務註冊的本質是將建立的 ServiceDescriptor 物件新增到指定 IServiceCollection 集合中的過程。

為了避免重複註冊,TryAdd 擴充套件方法只會在指定型別的服務註冊不存在的前提下才將 ServiceDescriptor 物件新增到集合中。TryAddEnumerable 擴充套件方法在用於重複性檢驗時會同時考慮服務型別和實現型別。

想測試擴充套件方法但沒有找到。。,是擴充套件方法在不同的包嗎?發現名稱空間不同,需要新增“using Microsoft.Extensions.DependencyInjection.Extensions;”。

 1   var services = new ServiceCollection();
 2    services.TryAdd(ServiceDescriptor.Describe(typeof(IFoo), typeof(Foo), ServiceLifetime.Singleton));
 3    Console.WriteLine(services.Count);
 4    services.TryAdd(ServiceDescriptor.Describe(typeof(IFoo), typeof(Foo2), ServiceLifetime.Scoped));            
 5    Console.WriteLine(services.Count);
 6 
 7    services.TryAddEnumerable(ServiceDescriptor.Singleton<IFoo, Foo2>());
 8    Console.WriteLine(services.Count);
 9 
10    services.TryAddEnumerable(ServiceDescriptor.Singleton<IFoo, Foo2>());
11    Console.WriteLine(services.Count);

 IServiceCollection 實現了 IList<ServiceDescriptor>介面,所以可以呼叫 Clear、Remove、RemoveAll 方法刪除現有的 ServiceDescriptor 物件。還有擴充套件方法可以刪除指定的服務型別的 ServiceDescriptor 物件。

3.3 服務的消費

3.3.1 IServiceProvider

如果對應的服務註冊不存在,GetService 方法會返回 Null。但呼叫 GetRequiredService 或 GetRequiredService<T> 會丟擲一個 InvalidOperationException。

作者不打算詳細介紹 ServiceProvider 型別,因為在該型別中提供服務例項的機制一直在變化。

3.3.2 服務例項的建立

ServiceDescriptor 物件具有 3 種構建方式,分別對應服務例項的 3 種提供方式。

如果提供的是服務的實現型別,那麼最終提供的服務例項將通過該型別的某個建構函式來建立,那麼是根據什麼策略選的建構函式? 傳入建構函式的所有引數必須先被初始化。在所有合法的候選建構函式列表中,最終被選中的建構函式具有如下特徵:所有候選建構函式的引數型別都能在這個容器中(原文為建構函式中,感覺不對)找到,如果這樣的建構函式不存在,那麼丟擲 InvalidOperationException。

如果有兩個符合條件的引數個數相等的建構函式,也會丟擲 InvalidOperationException,具體資訊為“Unable to activate type 'MM.Martin.Qux'. The following constructors are ambiguous”。

3.3.3 生命週期

生命週期決定了 IServiceProvider 怎麼提供和釋放服務例項。

1. 服務範圍

Scoped 是指由 IServiceScope 介面表示的服務範圍,該範圍由 IServiceScopeFactory 物件構建。

如果釋放過程中涉及一些非同步操作,則相應的型別往往會實現 IAsyncDisposable 介面,所以服務範圍也有一個通過 AsyncServiceScope 表示的非同步版本。

AsyncServiceScope 是一個只讀的 struct,派生於 IServiceScope 介面,同時實現了 IAsyncDisposable 介面。本質上是對一個 IServiceScope  的封裝。

IServiceScopeFactory 和 IServiceProvider 都定義了 CreateAsyncScope 擴充套件方法建立表示非同步服務範圍的 AsyncServiceScope 。

2. 3 種生命週期模式

IServiceProvider 負責在服務例項在其生命週期終結時釋放服務例項(如果需要),這裡所說的釋放和垃圾回收無關。

每個作為依賴注入容器的 IServiceProvider  物件都具有兩個列表來儲存服務例項,Realized Services 和 Disposable Services。

當 IServiceScope 物件的 Dispose 被呼叫時,當前範圍的 IServiceProvider  的 Dispose 也會被呼叫,後者從自身的 Disposable Services 列表中提取所有服務例項並呼叫它們的 Dispose 。在這之後,兩個列表同時被清空,列表中的服務例項和 IServiceProvider  自身會成為垃圾物件被 GC 回收。

當 IServiceScope 物件的 Dispose 被呼叫時,如果服務例項僅僅實現了 IAsyncDisposable,那麼會丟擲 InvalidOperationException。當以非同步方式釋放容器時,可以採用同步的方式釋放服務例項,反之不行。

3.3.4 ActivatorUtilities

有時需要利用容器建立一個對應型別不曾註冊的例項,典型的例項就是 MVC 的 Controller。這時就會利用 ActivatorUtilities 這個靜態的工具型別,要求容器能否提供建構函式中必要的引數。

1   var provider = new ServiceCollection()
2                         .AddTransient<IFoo, Foo>()
3                         .AddTransient<IBar, Bar>()
4                         .BuildServiceProvider();
5 
6    var test = ActivatorUtilities.CreateInstance<Test>(provider, "Martin Fu");
7    Console.WriteLine(test);

這裡也會選擇一個合適的建構函式。如果目標型別定義了多個公共建構函式,那麼取決於兩個因素:顯式指定的引數列表和建構函式的定義順序。首先會遍歷每一個候選的公共建構函式,並對它們建立 ConstructorMatcher 物件,然後將顯式指定的引數列表作為引數呼叫其 Match 方法,該方法返回的數字表示匹配度,值越大匹配度越高,-1 表示完全不匹配。

由於建構函式的定義順序有影響,所以當希望 ActivatorUtilities 選擇某個建構函式時,可以設定 ActivatorUtlitiesConstructorAttribute 特性。

 1   public class Test{
 2         private string Name{get;set;}
 3         private IFoo Foo{get;set;}
 4         private IBar Bar{get;set;}
 5 
 6         public Test(IFoo foo, IBar bar){
 7             Console.WriteLine("2-1");
 8             this.Foo = foo;
 9             this.Bar = bar;
10         }
11 
12         public Test(string name, IFoo foo){
13             Console.WriteLine("2-2");
14             this.Name = name;
15             this.Foo = foo;
16         }
17 
18         [ActivatorUtilitiesConstructor]
19         public Test(string name, IFoo foo, IBar bar){
20             Console.WriteLine("3");
21             this.Name = name;
22             this.Foo = foo;
23             this.Bar = bar;
24         }
25     }

3.4 擴充套件

作者認為原生的依賴注入框架就是最好的選擇,但也可以通過擴充套件來整合別的開源框架。

3.4.1 適配

對承載系統來說,原始的服務註冊和最終的依賴注入容器分別體現為一個 IServiceCollection 物件和 IServiceProvider 物件,而且承載系統在初始化過程中會將自身的服務註冊新增到由它建立的 IServiceCollection 集合中。因此要將第三方依賴注入框架整合進來,需要解決 IServiceCollection 與 IServiceProvider  的適配問題。

3.4.2 IServiceProviderFactory<TContainerBuilder>

CreateBuilder 方法利用指定的 IServiceCollection 集合建立對應的 TContainerBuilder 物件,CreateServiceProvider 方法進一步利用 TContainerBuilder  建立作為容器的 IServiceProvider 。預設註冊的是 DefaultServiceProviderFactory。