由於依賴注入具有舉足輕重的作用,所以《ASP.NET Core 6框架揭秘》的絕大部分章節都會涉及這一主題。本書第3章對.NET原生的依賴注入框架的設計和實現進行了系統的介紹,其中設計一些“鮮為人知”的細節,其中一部分就體現在本篇提供的這幾個實例演示上。(本篇提供的實例已經匯總到《ASP.NET Core 6框架揭秘-實例演示版》)
[308]構造函數的選擇(成功)(源代碼)
[309]構造函數的選擇(失敗)(源代碼)
[310]IDisposable和IAsyncDisposable接口的差異(錯誤編程)(源代碼)
[311]IDisposable和IAsyncDisposable接口的差異(正確編程)(源代碼)
[312]利用ActivatorUtilities提供服務實例(源代碼)
[313]ActivatorUtilities針對構造函數的“評分”(源代碼)
[314]ActivatorUtilities針對構造函數的選擇(源代碼)
[315]ActivatorUtilitiesConstructorAttribute特性的應用(源代碼)
[316]與第三方依賴注入框架Cat的整合(源代碼)
[308]構造函數的選擇(成功)
如果通過指定服務類型調用IServiceProvider對象的GetService方法,它總是會根據提供的服務類型從服務注冊列表中找到對應的ServiceDescriptor對象,並根據它來提供所需的服務實例。ServiceDescriptor對象具有三種構建方式,分別對應服務實例三種提供方式。我們既可以提供一個Func<IServiceProvider, object>對象作為工廠來創建對應的服務實例,也可以直接提供一個創建好的服務實例。如果提供的是服務的實現類型,最終提供的服務實例將通過該類型的某個構造函數來創建,那么構造函數是通過什么策略被選擇出來的?
如果IServiceProvider對象試圖通過調用構造函數的方式來創建服務實例,傳入構造函數的所有參數必須先被初始化,所以最終被選擇的構造函數必須具備一個基本的條件,那就是IServiceProvider對象能夠提供構造函數的所有參數。假設我們定義了如下四個服務接口(IFoo、IBar、IBaz和IQux)和對應的實現類型(Foo、Bar、Baz和Qux)。我們為Qux定義了三個構造函數,參數都定義成服務接口類型。為了確定最終選擇哪個構造函數來創建目標服務實例,我們在構造函數執行時在控制台上輸出相應的指示性文字。
public interface IFoo {} public interface IBar {} public interface IBaz {} public interface IQux {} public class Foo : IFoo {} public class Bar : IBar {} public class Baz : IBaz {} public class Qux : IQux { public Qux(IFoo foo) => Console.WriteLine("Selected constructor: Qux(IFoo)"); public Qux(IFoo foo, IBar bar) => Console.WriteLine("Selected constructor: Qux(IFoo, IBar)"); public Qux(IFoo foo, IBar bar, IBaz baz) => Console.WriteLine("Selected constructor: Qux(IFoo, IBar, IBaz)"); }
我們在如下所示的演示程序創建了一個ServiceCollection對象,並在其中添加針對IFoo、IBar及IQux接口的服務注冊,但針對IBaz接口的服務注冊並未添加。當利用構建的IServiceProvider來提供針對IQux接口的服務實例時,我們是否能夠得到一個Qux對象呢?如果可以,它又是通過執行哪個構造函數創建的呢?
using App; using Microsoft.Extensions.DependencyInjection; new ServiceCollection() .AddTransient<IFoo, Foo>() .AddTransient<IBar, Bar>() .AddTransient<IQux, Qux>() .BuildServiceProvider() .GetServices<IQux>();
對於定義在Qux中的三個構造函數來說, 由於存在針對IFoo和IBar接口的服務注冊,所前面兩個構造函數的所有參數能夠由容器提供,第三個構造函數的bar參數卻不能。根據前面介紹的第一個原則(IServiceProvider對象能夠提供構造函數的所有參數),Qux的前兩個構造函數會成為合法的候選構造函數,那么最終會選擇哪一個構造函數呢?在所有合法的候選構造函數列表中,最終被選擇的構造函數具有如下特征:所有候選構造函數的參數類型都能在這個構造函數中找到。如果這樣的構造函數並不存在,會直接拋出一個InvalidOperationException類型的異常。根據這個原則,Qux的第二個構造函數的參數類型包括IFoo和IBar兩個接口,而第一個構造函數只具有一個類型為IFoo的參數,所以最終被選擇的是Qux的第二個構造函數,運行實例程序,控制台上產生的輸出結果如圖1所示。
[309]構造函數的選擇(失敗)
我們接下來只為Qux類型定義兩個構造函數,它們都具有兩個參數,參數類型分別為IFoo & IBar和IBar & IBaz,我們同時將針對IBaz/Baz的服務注冊添加到創建的ServiceCollection集合中。
using App; using Microsoft.Extensions.DependencyInjection; new ServiceCollection() .AddTransient<IFoo, Foo>() .AddTransient<IBar, Bar>() .AddTransient<IBaz, Baz>() .AddTransient<IQux, Qux>() .BuildServiceProvider() .GetServices<IQux>(); public class Qux : IQux { public Qux(IFoo foo, IBar bar) {} public Qux(IBar bar, IBaz baz) {} }
雖然Qux的兩個構造函數的參數都可以由IServiceProvider對象來提供,但是並沒有某個構造函數擁有所有候選構造函數的參數類型,所以選擇一個最佳的構造函數。運行該程序后會拋出圖2所示的InvalidOperationException類型的異常,並提示無法從兩個候選的構造函數中選擇一個最優的來創建服務實例。
[310]IDisposable和IAsyncDisposable接口的差異(錯誤編程)
IServiceProvider對象除了提供所需的服務實例,它還需要負責在其生命周期終結的時候釋放它們(如果需要的話)。這里所說的回收釋放與 .NET的垃圾回收機制無關,僅僅針對自身類型實現了IDisposable或者IAsyncDisposable接口的服務實例(下面稱為Disposable服務實例),具體的釋放操作體現為調用它們的Dispose或者DisposeAsync方法。是當IServiceScope對象的Dispose方法被執行的時候,如果待釋放服務實例對應的類型僅僅實現了IAsyncDisposable接口,而沒有實現IDisposable接口,此時會拋出一個InvalidOperationException異常。
using Microsoft.Extensions.DependencyInjection; using var scope = new ServiceCollection() .AddScoped<Fooar>() .BuildServiceProvider() .CreateScope(); scope.ServiceProvider.GetRequiredService<Fooar>(); public class Fooar : IAsyncDisposable { public ValueTask DisposeAsync() => default; }
如上面的代碼片段所示,以Scoped模式注冊的Foobar類型實現了IAsyncDisposable接口。我們在一個創建的服務范圍內創建該服務實例之后,如圖3所示的InvalidOperationException異常會在服務范圍被釋放的時候拋出來。
圖3 IAsyncDisposable實例按照同步方式釋放時拋出的異常
[311]IDisposable和IAsyncDisposable接口的差異(正確編程)
不論采用怎樣的生命周期模式,服務實例的釋放總是在容器被釋放時完成的。容器的釋放具有同步和異步兩種形式,並由對應的服務范圍來決定。以異步方式釋放容器可以采用同步的方式釋放服務實例,反之則不成立。如果服務類型只實現了IAsyncDisposable接口,意味着我們只能采用異步的方式釋放容器,這正是圖3-11所示的異常消息試圖表達的意思。在這種情況下,我們應該按照如下的方式創建代表異步服務范圍的AsyncServiceScope對象,並調用DisposeAsync方法(await using)以異步的方式釋放容器。
using Microsoft.Extensions.DependencyInjection; await using var scope = new ServiceCollection() .AddScoped<Fooar>() .BuildServiceProvider() .CreateAsyncScope(); scope.ServiceProvider.GetRequiredService<Fooar>();
[312]利用ActivatorUtilities提供服務實例
IServiceProvider對象能夠提供指定類型服務實例的前提存在對應的服務注冊,但是有的時候我們需要利用容器創建一個對應類型不曾注冊的實例。一個最為典型的例子是MVC應用針對目標Controller實例的創建,因為Controller類型並未作為依賴服務進行注冊。這種情況我們就會使用到ActivatorUtilities這個靜態的工具類型。當我們調用定義在ActivatorUtilities類型中的如下這些靜態方法根據指定的IServiceProvider對象創建指定服務實例時,雖然不要求針對目標服務被預先注冊,但是要求指定的IServiceProvider對象能夠提供構造函數中必要的參數。
public static class ActivatorUtilities { public static object CreateInstance(IServiceProvider provider, Type instanceType, params object[] parameters); public static T CreateInstance<T>(IServiceProvider provider, params object[] parameters); public static object GetServiceOrCreateInstance(IServiceProvider provider, Type type); public static T GetServiceOrCreateInstance<T>(IServiceProvider provider); }
如下的程序演示了ActivatorUtilities的典型用法。如代碼片段所示,Foobar類型的構造函數除了注入Foo和Bar這兩個可以由容器提供的對象之外,還包含一個用來初始化Name屬性的字符串類型的參數。我們將IServiceProvider對象作為參數調用ActivatorUtilities的CreateInstance<T>方法創建一個Foobar對象,此時構造函數的第一個name參數必須顯式指定。
using Microsoft.Extensions.DependencyInjection; using System.Diagnostics; var serviceProviderr = new ServiceCollection() .AddSingleton<Foo>() .AddSingleton<Bar>() .BuildServiceProvider(); var foobar = ActivatorUtilities.CreateInstance<Foobar>(serviceProviderr, "foobar"); Debug.Assert(foobar.Name == "foobar"); public class Foo { } public class Bar { } public class Foobar { public string Name { get; } public Foo Foo { get; } public Bar Bar { get; } public Foobar(string name, Foo foo, Bar bar) { Name = name; Foo = foo; Bar = bar; } }
[313]ActivatorUtilities針對構造函數的“評分”
當我們調用ActivatorUtilities類型的CreateInstance方法創建指定類型的實例時,它總是會選擇選擇一個“適合”的構造函數。前面我們詳細討論過依賴注入容器對構造函數的選擇策略,那么這里的構造函數又是如何被選擇出來的呢?如果目標類型定義了多個候選的公共構造函數,最終哪一個被選擇取決於兩個因素:顯式指定的參數列表和構造函數被定義順序。具體來說,它會遍歷每一個候選的公共構造函數,並針對它們創建具有如下定義的ConstructorMatcher對象,然后將我們顯式指定的參數列表作為參數調用其Match方法,該方法返回的數字表示當前構造函數與指定的參數列表的匹配度。值越大,意味着匹配度越高,-1表示完全不匹配。
public static class ActivatorUtilities { private struct ConstructorMatcher { public ConstructorMatcher(ConstructorInfo constructor); public int Match(object[] givenParameters); } }
ActivatorUtilities最終會選擇匹配度不小於零且值最高的那個構造函數。如果多個構造函數同時擁有最高匹配度,遍歷的第一個構造函數會被選擇。我個人其實不太認可這樣的設計,既然匹配度相同,對應的構造函數就應該是平等的,為了避免錯誤的構造函數被選擇,拋出異常可能是更好的選擇。
對於根據構造函數創建的ConstructorMatcher對象來說,它的Match方法相當於為候選的構造函數針對當前調用場景打了一個匹配度分值,那么這個得分是如何計算的呢?具體的計算流程基本上體現在圖4中。假設構造函數參數類型依次為Foo、Bar和Baz,如果顯式指定的參數列表的某一個與這三個類型都不匹配,比如指定了一個Qux對象,並且Qux類型沒有繼承這三個類型中的任何一個,此時的匹配度得分就是-1。
圖4 構造函數針對參數數組的匹配度
如果指定的N個參數都與構造函數的前N個參數匹配得上,那么最終的匹配度得分就是N-1。假設foo、bar和baz分別為代碼類型為Foo、Bar和Baz的對象,那么只有三種匹配場景,即提供的參數分別為[foo]、[foo, bar]和[foo,bar, baz],最終的匹配度得分分別為0、1和2。如果指定的參數數組不能滿足上述的嚴格匹配規則,最終的得分就是0。為了驗證構造函數匹配規則,我們來做一個簡單的示例演示。如下面的代碼片段所示,我們定義了一個Foobarbaz類型,它的構造函數的參數類型依次為Foo、Bar和Baz。我們采用了反射的方式創建了針對這個構造函數的ConstructorMatcher對象。對於給出的幾種參數序列,我們調用ConstructorMatcher對象的Match方法計算該構造函數與它們的匹配度。
using Microsoft.Extensions.DependencyInjection; using System.Reflection; var constructor = typeof(Foobarbaz).GetConstructors().Single(); var matcherType = typeof(ActivatorUtilities).GetNestedType("ConstructorMatcher", BindingFlags.NonPublic) ?? throw new InvalidOperationException("It fails to resove ConstructorMatcher type"); var matchMethod = matcherType.GetMethod("Match"); var foo = new Foo(); var bar = new Bar(); var baz = new Baz(); var qux = new Qux(); Console.WriteLine($"[Qux] = {Match(qux)}"); Console.WriteLine($"[Foo] = {Match(foo)}"); Console.WriteLine($"[Foo, Bar] = {Match(foo, bar)}"); Console.WriteLine($"[Foo, Bar, Baz] = {Match(foo, bar, baz)}"); Console.WriteLine($"[Bar, Baz] = {Match(bar, baz)}"); Console.WriteLine($"[Foo, Baz] = {Match(foo, baz)}"); int? Match(params object[] args) { var matcher = Activator.CreateInstance(matcherType, constructor); return (int?)matchMethod?.Invoke(matcher, new object[] { args }); } public class Foo {} public class Bar {} public class Baz {} public class Qux {} public class Foobarbaz { public Foobarbaz(Foo foo, Bar bar, Baz baz) { } }
演示程序執行之后會在控制台上輸出如圖5所示的結果。對於第一個測試結果,由於我們指定了一個Qux對象,它與構造函數的任一個參數都不兼容,所以匹配度為-1。接下來的三個參數組合完全符合上述的匹配規則,所以得到的匹配度得分為N-1(0、1和2)。至於其他兩個,[Bar, Baz]雖然與構造函數的后兩個參數兼容(包括順序),由於Match方法從第一個參數進行匹配,得分依然是0。最后一個組合[Foo, Baz]由於漏掉一個,同樣得零分。
[314]ActivatorUtilities針對構造函數的選擇
我不確定構造函數選擇策略在今后的版本中會不會修改,就目前的設計來說,我是不認同的。我覺得這樣的選擇策略是不嚴謹的,就上面的演示實例驗證的構造函數來說,對於參數組合[Foo, Bar]和[Bar, Foo],以及[Foo, Bar]和[Bar, Baz],我不覺得它們在匹配程度上有什么不同。這樣的策略還會帶來另一個問題,那就是最終被選擇的構造函數不僅僅依賴於指定的參數組合,還決定於候選構造函數在所在類型中被定義的順序。
using Microsoft.Extensions.DependencyInjection; var serviceProvider = new ServiceCollection() .AddSingleton<Foo>() .AddSingleton<Bar>() .AddSingleton<Baz>() .BuildServiceProvider(); ActivatorUtilities.CreateInstance<Foobar>(serviceProvider); ActivatorUtilities.CreateInstance<BarBaz>(serviceProvider); public class Foo {} public class Bar {} public class Baz {} public class Foobar { public Foobar(Foo foo) => Console.WriteLine("Foobar(Foo foo)"); public Foobar(Foo foo, Bar bar) => Console.WriteLine("Foobar(Foo foo, Bar bar)"); } public class BarBaz { public BarBaz(Bar bar, Baz baz) => Console.WriteLine("BarBaz(Bar bar, Baz baz)"); public BarBaz(Bar bar) => Console.WriteLine("BarBaz(Bar bar)"); }
以如上的演示程序為例,Foobar和Barbaz都具有兩個構造函數,參數數量分別為1和2,不同的是Foobar中包含一個參數的構造函數被放在前面,而Barbaz則將其置於后面。當我們調用ActivatorUtilities的CreateInstance<T>構造函數分別創建Foobar和Barbaz對象的時候,總是第一個構造函數被執行(如圖6所示)。這意味着當我們無意中改變了構造函數的定義順序就會改變應用程序執行的行為,這在我看來是不能接受的。
[315]ActivatorUtilitiesConstructorAttribute特性的應用
默認的構造函數選擇策略過於模糊且不嚴謹,如果希望ActivatorUtilities選擇某個構造函數,我們可以通過在目標構造函數上標注ActivatorUtilitiesConstructorAttribute特性的方式來解決這個問題。就上面這個實例來說,如果我們希望ActivatorUtilities選擇FooBar具有兩個參數的構造函數,可以按照如下的方式在該構造函數上面標注ActivatorUtilitiesConstructorAttribute特性。
public class Foobar { public Foobar(Foo foo) => Console.WriteLine("Foobar(Foo foo)"); [ActivatorUtilitiesConstructor] public Foobar(Foo foo, Bar bar) => Console.WriteLine("Foobarbaz(Foo foo, Bar bar)"); }
[316]與第三方依賴注入框架Cat的整合
我們在第2章“依賴注入(上)”中創建了一個名為Cat的依賴注入框架,我們接下來就通過上述的方式將它引入到應用中。我們首選創建一個名為CatBuilder的類型作為對應的ContainerBuilder。由於需要涉及針對服務范圍的創建,我們在CatBuilder類中定義了如下兩個內嵌的私有類型。表示服務范圍的ServiceScope對象實際上就是對一個IServiceProvider對象的封裝,而ServiceScopeFactory類型為創建它的工廠,它是對一個Cat對象的封裝。
public class CatBuilder { private class ServiceScope : IServiceScope { public ServiceScope(IServiceProvider serviceProvider) => ServiceProvider = serviceProvider; public IServiceProvider ServiceProvider { get; } public void Dispose()=> (ServiceProvider as IDisposable)?.Dispose(); } private class ServiceScopeFactory : IServiceScopeFactory { private readonly Cat _cat; public ServiceScopeFactory(Cat cat) => _cat = cat; public IServiceScope CreateScope() => new ServiceScope(_cat); } }
一個CatBuilder對象是對一個Cat對象的封裝,它的BuildServiceProvider方法會直接返回這個Cat對象,並將它作為最終構建的依賴注入容器。CatBuilder對象在初始化過程中添加了針對IServiceScopeFactory/ServiceScopeFactory的服務注冊。為了實現程序集范圍內的批量服務注冊,我們為CatBuilder類型定義一個Register方法。
public class CatBuilder { private readonly Cat _cat; public CatBuilder(Cat cat) { _cat = cat; _cat.Register<IServiceScopeFactory>(c => new ServiceScopeFactory(c.CreateChild()), Lifetime.Transient); } public IServiceProvider BuildServiceProvider() => _cat; public CatBuilder Register(Assembly assembly) { _cat.Register(assembly); return this; } ... }
如下面的代碼片段所示,CatServiceProviderFactory類型實現了IServiceProviderFactory<CatBuilder>接口。在實現的CreateBuilder方法中,我們創建了一個Cat對象,並將IServiceCollection集合包含的服務注冊(ServiceDescriptor對象)轉換成Cat的服務注冊形式(ServiceRegistry對象)。在將轉換后的服務注冊應用到Cat對象上之后,我們最終利用這個Cat對象創建出返回的CatBuilder對象。在實現的CreateServiceProvider方法中,我們直接返回調用CatBuilder對象的CreateServiceProvider方法得到的IServiceProvider對象。
public class CatServiceProviderFactory : IServiceProviderFactory<CatBuilder> { public CatBuilder CreateBuilder(IServiceCollection services) { var cat = new Cat(); foreach (var service in services) { if (service.ImplementationFactory != null) { cat.Register(service.ServiceType, provider => service.ImplementationFactory(provider), service.Lifetime.AsCatLifetime()); } else if (service.ImplementationInstance != null) { cat.Register(service.ServiceType, service.ImplementationInstance); } else { cat.Register(service.ServiceType, service.ImplementationType, service.Lifetime.AsCatLifetime()); } } return new CatBuilder(cat); } public IServiceProvider CreateServiceProvider(CatBuilder containerBuilder) => containerBuilder.BuildServiceProvider(); }
對於服務實例的生命周期模式,Cat與 .NET依賴注入框架具有一致的表達,所以在將服務注冊從ServiceDescriptor類型轉化成ServiceRegistry類型時,我們可以簡單的完成兩者的轉換。具體的轉換實現如下所示的AsCatLifetime擴展方法中。
internal static class Extensions { public static Lifetime AsCatLifetime(this ServiceLifetime lifetime) { return lifetime switch { ServiceLifetime.Scoped => Lifetime.Self, ServiceLifetime.Singleton => Lifetime.Root, _ => Lifetime.Transient, }; } }
我們接下來演示如何利用CatServiceProviderFactory創建作為依賴注入容器的IServiceProvider對象。我們定義了Foo、Bar、Baz和Qux四個類型和它們實現的IFoo、IBar、IBaz與IQux接口。Qux類型上標注了一個MapToAttribute特性,並注冊了與對應接口IQux之間的映射。這些類型派生的基類Base實現了IDisposable接口,我們在其構造函數和實現的Dispose方法中輸出相應的文本,以確定實例被創建和釋放的時機。
public interface IFoo {} public interface IBar {} public interface IBaz {} public interface IQux {} public interface IFoobar<T1, T2> {} public class Base : IDisposable { public Base() => Console.WriteLine($"Instance of {GetType().Name} is created."); public void Dispose() => Console.WriteLine($"Instance of {GetType().Name} is disposed."); } public class Foo : Base, IFoo{ } public class Bar : Base, IBar{ } public class Baz : Base, IBaz{ } [MapTo(typeof(IQux), Lifetime.Root)] public class Qux : Base, IQux { } public class Foobar<T1, T2>: IFoobar<T1,T2> { public IFoo Foo { get; } public IBar Bar { get; } public Foobar(IFoo foo, IBar bar) { Foo = foo; Bar = bar; } }
在如下所示的演示程序中,我們先創建了一個ServiceCollection集合,並采用三種不同的生命周期模式分別添加了針對IFoo、IBar和IBaz接口的服務注冊。我們接下來根據ServiceCollection集合創建了一個CatServiceProviderFactory工廠,並調用其CreateBuilder方法創建出對應的CatBuilder對象。我們最后調用CatBuilder對象的Register方法完成了針對當前入口程序集的批量服務注冊,其目的在於添加針對IQux/Qux的服務注冊。
using App; using Microsoft.Extensions.DependencyInjection; var services = new ServiceCollection() .AddTransient<IFoo, Foo>() .AddScoped<IBar>(_ => new Bar()) .AddSingleton<IBaz>(new Baz()); var factory = new CatServiceProviderFactory(); var builder = factory.CreateBuilder(services).Register(typeof(Foo).Assembly); var container = factory.CreateServiceProvider(builder); GetServices(); GetServices(); Console.WriteLine("\nRoot container is disposed."); (container as IDisposable)?.Dispose(); void GetServices() { using var scope = container.CreateScope(); Console.WriteLine("\nService scope is created."); var child = scope.ServiceProvider; child.GetService<IFoo>(); child.GetService<IBar>(); child.GetService<IBaz>(); child.GetService<IQux>(); child.GetService<IFoo>(); child.GetService<IBar>(); child.GetService<IBaz>(); child.GetService<IQux>(); Console.WriteLine("\nService scope is disposed."); }
在調用CatServiceProviderFactory工廠的CreateServiceProvider方法來創建出作為依賴注入容器的IServiceProvider對象之后,我們先后兩次調用了本地方法GetServices,后者會利用這個IServiceProvider對象來創建一個服務范圍,並利用此服務范圍內的IServiceProvider提供兩組服務實例。利用CatServiceProviderFactory創建的IServiceProvider對象最終通過調用其Dispose方法進行釋放。該程序運行之后在控制台上輸出的結果如圖7所示,輸出結果體現的服務生命周期與演示程序體現的生命周期是完全一致的。
圖7 利用CatServiceProviderFactory創建IServiceProvider對象







