[ASP.NET Core 3框架揭秘] Options[5]: 依賴注入

  • 2020 年 2 月 13 日
  • 筆記

《Options模型》介紹了組成Options模型的4個核心對象以及它們之間的交互關係,讀者對如何得到Options對象的實現原理可能不太了解,本篇文章主要介紹依賴注入的相關內容。既然我們能夠利用IServiceProvider對象提供的IOptions<TOptions>服務、IOptionsSnapshot<TOptions>服務和IOptionsMonitorCache<TOptions>服務來獲取對應的Options對象,那麼在這之前必然需要註冊相應的服務。回顧《配置選項的正確使用方式》演示的幾個實例可以發現,Options模式涉及的API其實不是很多,大都集中在相關服務的註冊上。Options模型的核心服務實現在IServiceCollection介面的AddOptions擴展方法。

一、AddOptions

AddOptions擴展方法的完整定義如下所示,由此可知,該方法將Options模型中的幾個核心類型作為服務註冊到了指定的IServiceCollection對象之中。由於它們都是調用TryAdd方法進行服務註冊的,所以我們可以在需要Options模式支援的情況下調用AddOptions方法,而不需要擔心是否會添加太多重複服務註冊的問題。

public static class OptionsServiceCollectionExtensions  {      public static IServiceCollection AddOptions(this IServiceCollection services)      {          services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(OptionsManager<>)));          services.TryAdd(ServiceDescriptor.Scoped(typeof(IOptionsSnapshot<>), typeof(OptionsManager<>)));          services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitor<>), typeof(OptionsMonitor<>)));          services.TryAdd(ServiceDescriptor.Transient(typeof(IOptionsFactory<>), typeof(OptionsFactory<>)));          services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptionsMonitorCache<>), typeof(OptionsCache<>)));          return services;      }  }

從給出的程式碼片段可以看出,AddOptions擴展方法實際上註冊了5個服務。由於這5個服務註冊非常重要,所以筆者採用表格的形式列出了它們的Service Type(服務介面)、Implementation(實現類型)和Lifetime(生命周期)(見下表)。雖然服務介面IOptions<TOptions>和IOptionsSnapshot<TOptions>映射的實現類型都是OptionsManager<TOptions>,但是它們具有不同的生命周期。具體來說,前者的生命周期為Singleton,後者的生命周期則是Scoped,後續內容會單獨講述不同生命周期對Options對象產生什麼樣的影響。

Service Type

Implementation

Lifetime

IOptions<TOptions>

OptionsManager<TOptions>

Singleton

IOptionsSnapshot<TOptions>

OptionsManager<TOptions>

Scoped

IOptionsMonitor<TOptions>

OptionsMonitor<TOptions>

Singleton

IOptionsFactory<TOptions>

OptionsFactory<TOptions>

Transient

IOptionsMonitorCache<TOptions>

OptionsCache<TOptions>

Singleton

按照上表列舉的服務註冊,如果以IOptions<TOptions>和IOptionsSnapshot<TOptions>作為服務類型從IServieProvidere對象中提取對應的服務實例,得到的都是OptionsManager<TOptions>對象。當OptionsManager<TOptions>對象被創建時,OptionsFactory<TOptions>對象會被自動創建出來並以構造器注入的方式提供給它並且被用來創建Options對象。但是由於表7-1中並沒有針對服務IConfigureOptions<TOptions>和IPostConfigureOptions<TOptions>的註冊,所以創建的Options對象無法被初始化。

二、Configure<TOptions>與PostConfigure<TOptions>

針對IConfigureOptions<TOptions>和IPostConfigureOptions<TOptions>的服務註冊是通過如下這些擴展方法來完成的。具體來說,針對IConfigureOptions<TOptions>服務的註冊實現在Configure<TOptions>方法中,而PostConfigure<TOptions>擴展方法則幫助我們完成針對IPostConfigureOptions<TOptions>的註冊。

public static class OptionsServiceCollectionExtensions  {      public static IServiceCollection Configure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions)          where TOptions : class          => services.Configure(Options.Options.DefaultName, configureOptions);        public static IServiceCollection Configure<TOptions>(this IServiceCollection services, string name, Action<TOptions> configureOptions) where TOptions : class          => services.AddSingleton<IConfigureOptions<TOptions>>(          new ConfigureNamedOptions<TOptions>(name, configureOptions));          return services;        public static IServiceCollection PostConfigure<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions)          where TOptions : class          => services.PostConfigure(Options.Options.DefaultName, configureOptions);        public static IServiceCollection PostConfigure<TOptions>(this IServiceCollection services, string name,Action<TOptions> configureOptions) where TOptions : class          => services.AddSingleton<IPostConfigureOptions<TOptions>>(new PostConfigureOptions<TOptions>(name, configureOptions));  }

從上述程式碼可以看出,這些方法註冊的服務實現類型為ConfigureNamedOptions<TOptions>和PostConfigureOptions<TOptions>,採用的生命周期模式均為Singleton。不論是ConfigureNamedOptions<TOptions>還是PostConfigureOptions<TOptions>,都需要指定一個具體的名稱,對於沒有指定具體Options名稱的Configure<TOptions>和PostConfigure<TOptions>方法重載來說,最終指定的是代表默認名稱的空字元串。

三、ConfigureAll<TOptions>與PostConfigureAll<TOptions>

雖然ConfigureAll<TOptions>和PostConfigureAll<TOptions>擴展方法註冊的同樣是ConfigureNamedOptions<TOptions>和PostConfigureOptions<TOptions>類型,但是它們會將名稱設置為Null。通過《Options模型》的內容可知,OptionsFactory對象在進行Options對象的初始化過程中會將名稱為Null的IConfigureNamedOptions<TOptions>和IPostConfigureOptions<TOptions>對象作為公共的配置對象,並且無條件執行。

public static class OptionsServiceCollectionExtensions  {      public static IServiceCollection ConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions)          where TOptions : class          => services.Configure(name: null, configureOptions: configureOptions);        public static IServiceCollection PostConfigureAll<TOptions>(this IServiceCollection services, Action<TOptions> configureOptions)          where TOptions : class          => services.PostConfigure(name: null, configureOptions: configureOptions);  }

四、ConfigureOptions

對於上面這幾個將Options類型作為泛型參數的方法來說,它們總是利用指定的Action<Options>對象來創建註冊的ConfigureNamedOptions<TOptions>對象和PostConfigureOptions<TOptions>對象 。對於自定義的實現了IConfigureOptions<TOptions>介面或者IPostConfigureOptions<TOptions>介面的類型,我們可以調用如下所示的3個ConfigureOptions擴展方法來對它們進行註冊。筆者在如下所示的程式碼片段中通過簡化的程式碼描述了這3個擴展方法的實現邏輯。

public static class OptionsServiceCollectionExtensions  {      public static IServiceCollection ConfigureOptions(this IServiceCollection services, object configureInstance)      {          Array.ForEach(FindIConfigureOptions(configureInstance.GetType()), it => services.AddSingleton(it, configureInstance));          return services;      }        public static IServiceCollection ConfigureOptions(this IServiceCollection services, Type configureType)      {          Array.ForEach(FindIConfigureOptions(configureType), it => services.AddTransient(it, configureType));          return services;      }        public static IServiceCollection ConfigureOptions<TConfigureOptions>(this IServiceCollection services) where TConfigureOptions : class          => services.ConfigureOptions(typeof(TConfigureOptions));        private static Type[] FindIConfigureOptions(Type type)      {          Func<Type, bool> valid = it => it.IsGenericType && (it.GetGenericTypeDefinition() == typeof(IConfigureOptions<>) || it.GetGenericTypeDefinition() == typeof(IPostConfigureOptions<>));          var types = type.GetInterfaces().Where(valid).ToArray();          if (types.Any())          {              throw new InvalidOperationException();          }          return types;      }  }

五、OptionsBuilder<TOptions>

Options模式涉及針對非常多的服務註冊,並且這些服務都是針對具體某個Options類型的,為了避免定義過多針對IServiceCollection介面的擴展方法,最新版本的Options模型採用Builder模式來完成相關的服務註冊。具體來說,可以將用來存儲服務註冊的IServiceCollection集合封裝到下面的OptionsBuilder<TOptions>對象中,並利用它提供的方法間接地完成所需的服務註冊。

public class OptionsBuilder<TOptions> where TOptions : class  {      public string Name { get; }      public IServiceCollection Services { get; }      public OptionsBuilder(IServiceCollection services, string name);        public virtual OptionsBuilder<TOptions> Configure(Action<TOptions> configureOptions);      public virtual OptionsBuilder<TOptions> Configure<TDep>(Action<TOptions, TDep> configureOptions) where TDep : class;      public virtual OptionsBuilder<TOptions> Configure<TDep1, TDep2>(Action<TOptions, TDep1, TDep2> configureOptions) where TDep1 : class where TDep2 : class;      public virtual OptionsBuilder<TOptions> Configure<TDep1, TDep2, TDep3>(Action<TOptions, TDep1, TDep2, TDep3> configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class;      public virtual OptionsBuilder<TOptions> Configure<TDep1, TDep2, TDep3, TDep4>(Action<TOptions, TDep1, TDep2, TDep3, TDep4> configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class where TDep4 : class;      public virtual OptionsBuilder<TOptions> Configure<TDep1, TDep2, TDep3, TDep4, TDep5>(Action<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5> configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class where TDep4 : class where TDep5 : class;        public virtual OptionsBuilder<TOptions> PostConfigure(Action<TOptions> configureOptions);      public virtual OptionsBuilder<TOptions> PostConfigure<TDep>(Action<TOptions, TDep> configureOptions) where TDep : class;      public virtual OptionsBuilder<TOptions> PostConfigure<TDep1, TDep2>(Action<TOptions, TDep1, TDep2> configureOptions) where TDep1 : class where TDep2 : class;      public virtual OptionsBuilder<TOptions> PostConfigure<TDep1, TDep2, TDep3>(Action<TOptions, TDep1, TDep2, TDep3> configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class;      public virtual OptionsBuilder<TOptions> PostConfigure<TDep1, TDep2, TDep3, TDep4>(Action<TOptions, TDep1, TDep2, TDep3, TDep4> configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class where TDep4 : class;      public virtual OptionsBuilder<TOptions> PostConfigure<TDep1, TDep2, TDep3, TDep4, TDep5>(Action<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5> configureOptions) where TDep1 : class where TDep2 : class where TDep3 : class where TDep4 : class where TDep5 : class;        public virtual OptionsBuilder<TOptions> Validate(Func<TOptions, bool> validation);      public virtual OptionsBuilder<TOptions> Validate<TDep>(Func<TOptions, TDep, bool> validation);      public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2>(Func<TOptions, TDep1, TDep2, bool> validation);      public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2, TDep3>(Func<TOptions, TDep1, TDep2, TDep3, bool> validation);      public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2, TDep3, TDep4>(Func<TOptions, TDep1, TDep2, TDep3, TDep4, bool> validation);      public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2, TDep3, TDep4, TDep5>(Func<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5, bool> validation);        public virtual OptionsBuilder<TOptions> Validate<TDep>(Func<TOptions, TDep, bool> validation, string failureMessage);      public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2>(Func<TOptions, TDep1, TDep2, bool> validation, string failureMessage);      public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2, TDep3>(Func<TOptions, TDep1, TDep2, TDep3, bool> validation, string failureMessage);      public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2, TDep3, TDep4>(Func<TOptions, TDep1, TDep2, TDep3, TDep4, bool> validation, string failureMessage);      public virtual OptionsBuilder<TOptions> Validate<TDep1, TDep2, TDep3, TDep4, TDep5>(Func<TOptions, TDep1, TDep2, TDep3, TDep4, TDep5, bool> validation, string failureMessage);  }

如下面的程式碼片段所示,OptionsBuilder<TOptions>對象不僅通過泛型參數關聯對應的Options類型,還利用Name屬性提供了Options的名稱。從上面的程式碼片段可以看出,OptionsBuilder<TOptions>類型提供的3組方法分別提供了針對IConfigureOptions<TOptions>介面、IPostConfigureOptions<TOptions>介面和IValidateOptions<TOptions>介面的18個實現類型的註冊。

當利用Builder模式來註冊這些服務的時候,只需要調用IServiceCollection介面的如下這兩個AddOptions<TOptions>擴展方法根據指定的名稱(默認名稱為空字元串)創建出對應的OptionsBuilder<TOptions>對象即可。從如下所示的程式碼片段可以看出,這兩個方法最終都需要調用非泛型的AddOptions方法,由於該方法調用TryAdd擴展方法註冊Options模式的5個核心服務,所以不會導致服務的重複註冊。

public static class OptionsServiceCollectionExtensions  {      public static OptionsBuilder<TOptions> AddOptions<TOptions>( this IServiceCollection services) where TOptions : class => services.AddOptions<TOptions>(Options.DefaultName);        public static OptionsBuilder<TOptions> AddOptions<TOptions>(this IServiceCollection services, string name) where TOptions : class      {          services.AddOptions();          return new OptionsBuilder<TOptions>(services, name);      }  }

六、IOptions<TOptions>與IOptionsSnapshot<TOptions>

通過對註冊服務的分析可知,服務介面IOptions<TOptions>和IOptionsSnapshot<TOptions>的默認實現類型都是OptionsManager<TOptions>,兩者的不同之處體現在生命周期上,前者採用的生命周期模式為Singleton,後者採用的生命周期模式則是Scoped。對於一個ASP.NET Core應用來說,Singleton和Scoped對應的是針對當前應用和當前請求的生命周期,所以通過IOptions<TOptions>介面獲取的Options對象在整個應用的生命周期內都是一致的,而通過IOptionsSnapshot<TOptions>介面獲取的Options對象則只能在當前請求上下文中保持一致。這也是後者命名的由來,它表示針對當前請求的Options快照

下面通過一個實例來演示IOptions<TOptions>和IOptionsSnapshot<TOptions>之間的差異。下面定義了FoobarOptions類型,簡單起見,我們僅僅為它定義了兩個整型的屬性(Foo和Bar),並重寫了ToString方法。

public class FoobarOptions  {      public int Foo { get; set; }      public int Bar { get; set; }      public override string ToString() => $"Foo:{Foo}, Bar:{Bar}";  }

整個演示程式體現在如下所示的程式碼片段中。我們創建了一個ServiceCollection對象,在調用AddOptions擴展方法註冊Options模型的基礎服務之後,調用Configure<FoobarOptions>方法利用定義的本地函數Print將FoobarOptions對象的Foo屬性和Bar屬性設置為一個隨機數。

class Program  {      static void Main()      {          var random = new Random();          var serviceProvider = new ServiceCollection()              .AddOptions()              .Configure<FoobarOptions>(foobar =>              {                  foobar.Foo = random.Next(1, 100);                  foobar.Bar = random.Next(1, 100);              })              .BuildServiceProvider();            Print(serviceProvider);          Print(serviceProvider);            static void Print(IServiceProvider provider)          {              var scopedProvider = provider                  .GetRequiredService<IServiceScopeFactory>()                  .CreateScope()                  .ServiceProvider;                var options = scopedProvider                  .GetRequiredService<IOptions<FoobarOptions>>()                  .Value;              var optionsSnapshot1 = scopedProvider                  .GetRequiredService<IOptionsSnapshot<FoobarOptions>>()                  .Value;              var optionsSnapshot2 = scopedProvider                  .GetRequiredService<IOptionsSnapshot<FoobarOptions>>()                  .Value;              Console.WriteLine($"options:{options}");              Console.WriteLine($"optionsSnapshot1:{optionsSnapshot1}");              Console.WriteLine($"optionsSnapshot2:{optionsSnapshot2}n");          }      }  }

我們並沒有直接利用ServiceCollection對象創建的IServiceProvider對象來提供服務,而是利用它創建了一個代表子容器的IServiceProvider對象,該對象就相當於ASP.NET Core應用中針對當前請求創建的IServiceProvider對象(RequestServices)。在利用這個IServiceProvider對象分別針對IOptions<TOptions>介面和IOptionsSnapshot<TOptions>介面得到對應的FoobarOptions對象之後,我們將配置選項輸出到控制台上。上述操作先後執行了兩次,相當於ASP.NET Core應用分別處理了兩次請求。

下圖展示了該演示程式執行後的輸出結果,由此可知,只有從同一個IServiceProvider對象獲取的IOptionsSnapshot<TOptions>服務才能提供一致的Options對象,但是對於所有源自同一個根的所有IServiceProvider對象來說,從中提取的IOptions<TOptions>服務都能提供一致的Options對象。

OptionsManager<Options>會利用一個自行創建的OptionsCache<TOptions>對象來快取Options對象,也就說,OptionsManager<Options>提供的Options對象存放在其私有快取中。雖然OptionsCache<TOptions>提供了清除快取的能力,但是OptionsManager<Options>自身無法感知原始Options數據是否發生變化,所以不會清除快取的Options對象。

這個特性決定了在一個ASP.NET Core應用中,以IOptions<TOptions>服務的形式提供的Options在整個應用的生命周期內不會發生改變,但是若使用IOptionsSnapshot<TOptions>服務,提供的Options對象只能在同一個請求上下文中提供一致的保障。如果希望即使在同一個請求處理周期內也能及時應用最新的Options屬性,就只能使用IOptionsMonitor<TOptions>服務來提供Options對象。

[ASP.NET Core 3框架揭秘] Options[1]: 配置選項的正確使用方式[上篇] [ASP.NET Core 3框架揭秘] Options[2]: 配置選項的正確使用方式[下篇] [ASP.NET Core 3框架揭秘] Options[3]: Options模型[上篇] [ASP.NET Core 3框架揭秘] Options[4]: Options模型[下篇] [ASP.NET Core 3框架揭秘] Options[5]: 依賴注入 [ASP.NET Core 3框架揭秘] Options[6]: 擴展與訂製 [ASP.NET Core 3框架揭秘] Options[7]: 與配置系統的整合