如何使用 Options Pattern for Microsoft.Extensions.Options

上篇,如何使用組態 Microsoft.Extensions.Configuration,最後我讓物件依賴 IConfiguration,不論是讀檔操作,還是重新載入檔案,它都可以完成,還可以更好嗎?.NET Core 的 Options Pattern 強化 IConfiguration,封裝了讀檔、轉強型別、重新載入、載入通知、驗證資料的行為,提供另一種使用參數的選擇。

開發環境

  • Rider 2021.3.4
  • Windows 10
  • .Net Fx 4.8 via 新版專案範本 .NET Project SDKs
  • Microsoft.Extensions.Options 5.0.0
Install-Package Microsoft.Extensions.Options -Version 5.0.0

Options Pattern

下圖出自:https://docs.microsoft.com/zh-tw/aspnet/core/fundamentals/configuration/options

 

實例化 Options 步驟

  1. 讀組態設定,參考 上篇
  2. 注入 Options 和 IConfiguration
  3. 需要使用組態的物件開一個注入點依賴  IOptions<TOptions>IOptionsSnapshot<TOptions>IOptionsMonitor<TOptions>

範例如下:

[TestMethod]
public void 注入Option()
{
    var builder = Host.CreateDefaultBuilder()
                      .ConfigureAppConfiguration((hosting, configBuilder) =>
                                                 {
                                                     // 1.讀組態檔 
                                                     var environmentName =
                                                         hosting.Configuration["ENVIRONMENT2"];
                                                     configBuilder.AddJsonFile("appsettings.json", false, true);
                                                     configBuilder
                                                         .AddJsonFile($"appsettings.{environmentName}.json",
                                                                      true, true);
                                                 })
                      .ConfigureServices((hosting, services) =>
                                         {
                                             // 2. 注入 Option 和 Configuration
                                             services.Configure<AppSetting1>(hosting.Configuration);

                                             //注入其他服務
                                             services.AddSingleton<AppWorkFlowWithOption>();
                                         })
        ;
    var host     = builder.Build();
    var service  = host.Services.GetService<AppWorkFlowWithOption>();
    var playerId = service.GetPlayerId();
    Console.WriteLine($"PlayerId = {playerId}");
}

 

Options 的方法都是 IServiceCollection 的擴充方法,這是微軟的 DI Container,有關 DI Container 的用法,可以參考

如何使用 DI Container for Microsoft.Extensions.DependencyInjection | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

如何使用 Microsoft.Extensions.DependencyInjection for Autofac | 余小章 @ 大內殿堂 - 點部落 (dotblogs.com.tw)

 

AppWorkFlowWithOption 開一個注入點,依賴 IOptions<AppSetting1>

public class AppWorkFlowWithOption : IAppWorkFlow
{
    private readonly AppSetting1 _appSetting;

    public AppWorkFlowWithOption(IOptions<AppSetting1> options)
    {
        this._appSetting = options.Value;
    }

    public string GetPlayerId()
    {
        return this._appSetting.Player.AppId;
    }
}

 

AppSetting1 這我從 AppSetting.cs 抄過來的,本來用 struct,現在改成 class

public class AppSetting1
{
    public Player Player { get; set; }

    public ConnectionStrings ConnectionStrings { get; set; }
}

ConnectionStrings.cs

Player.cs

注入Options 的幾個方法

services.Configure:可以決定要注入完整的組態節點或是部分組態節點

//注入 Options 和完整 IConfiguration
services.Configure<AppSetting>(this.Configuration);

//注入 Options 和 Configuration Section Name
services.Configure<Player>("Player1", this.Configuration.GetSection("Player1"));

 

services.PostConfigure:Configure 後再執行的動作,範例,依照 IConfigureNamedOptions 名稱的實例 Key 屬性變更

services.PostConfigure<Player>("Player1", myOptions =>
                                                 {
                                                     myOptions.Key = "post_configured_option1_value";
                                                 });

 

services.PostConfigureAll:Configure 後再執行的動作,範例,所有實例的 Key 屬性都會變更

services.PostConfigureAll<AppSetting>(config =>
                                     {
                                         config.Player.AppId = "post_configured_option1_value";
                                     });
上述這兩個 PostConfigure 方法,目前想不到適用情境

 

讀取 Options

在需要組態的 AppWorkFlowWithOption 物件依賴 IOptions<TOptoins>,只要依賴 Options Interfaces 就能使用他們的功能

Options Interfaces 有三種:IOptionsIOptionsSnapshotIOptionsMonitor,使用方式參考 Options Interfaces

在建構函數開注入點,如下圖:

private readonly AppSetting1 _appSetting;

public AppWorkFlowWithOption(IOptions<AppSetting1> options)
{
    this._appSetting = options.Value;
}

更多的範例,請參考以下

sample.dotblog/AppWorkFlowWithOption.cs at master · yaochangyu/sample.dotblog (github.com)

sample.dotblog/AppWorkFlowWithOptionsMonitor.cs at master · yaochangyu/sample.dotblog (github.com)

sample.dotblog/AppWorkFlowWithOptionsSnapshot.cs at master · yaochangyu/sample.dotblog (github.com)

 

在 ASP.NET Core 框架的注入取出 Service

[Route("options/appsettings")]
public IActionResult Get()
{
    var serviceProvider = this.HttpContext.RequestServices;
    var options         = serviceProvider.GetService<IOptions<AppSetting>>();
    return this.Ok(options?.Value);
}

詳細範例請參考

sample.dotblog/DefaultController.cs at master · yaochangyu/sample.dotblog (github.com)

 

Options Class 規則

Options<TOptoins>

TOptions 有以下規則:

  • 具名公開、無參數、非抽象類別。
  • 所有公開屬性都會繫結,預設公開屬性繫結組態節點名稱
  • 欄位不會繫結。

IConfigureNamedOptions

  • 不同的組態節點綁定相同的物件
  • 區分大小寫

怎麼使用?注入Options 時可以針對不同的 Configuration  Setion Name,給予一組對應的名字,下列範例注入了兩個 Options

[TestMethod]
public void 注入OptionMonitor()
{
    ….
                      .ConfigureServices((hosting, services) =>
                                         {
                                             // 注入 Option 和完整 Configuration
                                             services.Configure<AppSetting1>(hosting.Configuration);

                                             // 注入 Option 和特定 Configuration Section Name
                                             services.Configure<Player1>("Player",
                                                 hosting.Configuration.GetSection("Player"));

                                             //注入其他服務
                                             services.AddScoped<AppWorkFlowWithOptionsMonitor>();
                                         })
        ;
…
}

 

取出時指定 Name

public AppWorkFlowWithOptionsMonitor(IOptionsMonitor<AppSetting1> appSettingOption,
                                    IOptionsMonitor<Player1>     playerOption)
{
    this._player     = playerOption.Get("Player");
    this._appSetting = appSettingOption?.CurrentValue;
}

 

Options Interfaces

有以下幾種介面,請根據你的需求挑選,差異在於物件的生命週期

IOptions

  • 生命週期註冊為 Singleton (AddSingleton / ServiceLifetime.Singleton),可以注入所有生命週期的服務
  • 不支援
    • 在應用程式啟動後讀取組態。
    • 不同的組態節點綁定相同的物件(IConfigureNamedOptions)
       

IOptionsSnapshot

  • 生命週期註冊為 Scope (AddScope),不可以注入 Singleton 生命週期的服務
  • 在應用程式啟動後讀取組態。
  • 不同的組態節點綁定相同的物件(IConfigureNamedOptions)

 

IOptionsMonitor

 

重新讀取組態,依賴 IConfigurationBuilder.AddJsonFile(reloadOnChange = true),當 reloadOnChange = false 時,重新讀取將會失效

var configBuilder = new ConfigurationBuilder()
                      .SetBasePath(Directory.GetCurrentDirectory())
                      .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                      ;
var configRoot = configBuilder.Build();
... read config

 

驗證 Options

Install-Package Microsoft.Extensions.Options.DataAnnotations -Version 5.0.0

注入 Validate

services.AddOptions 方法有兩個擴充方法可以啟用驗證

  • ValidateDataAnnotations:驗證屬性有掛 Attribute
  • Validate:可以寫更複雜的驗證
//驗證
services.AddOptions<AppSetting1>()
        .ValidateDataAnnotations()
        .Validate(p =>
                  {
                      if (p.ConnectionStrings
                           .DefaultConnectionString == null)
                      {
                          return false;
                      }

                      return true;
                  },
                  "DefaultConnectionString must be value"); // Failure message.
;

 

調用 option.Value 屬性觸發驗證,驗證失敗則噴出 OptionsValidationException 例外

public AppWorkFlowWithOption(IOptions<AppSetting1> options)
{
    try
    {
        this._appSetting = options.Value;
    }
    catch (OptionsValidationException ex)
    {
        foreach (var failure in ex.Failures)
        {
            Console.WriteLine(failure);
        }
    }
}

 

直接注入 AppSetting

IConfiguration / IOptions 對我而言,最重要的功能是重新讀取組態,若不需要重新讀取組態,也可以讓物件直接依賴組態強型別物件,以下範例是透過 IConfiguration 讀檔後綁定,直接注入 AppSetting 物件

[TestMethod]
public void 直接注入組態物件()
{
    var builder = Host.CreateDefaultBuilder()
                      .ConfigureAppConfiguration((hosting, configBuilder) =>
                                                 {
                                                     // 1.讀組態檔 
                                                     var environmentName =
                                                         hosting.Configuration["ENVIRONMENT2"];
                                                     configBuilder.AddJsonFile("appsettings.json", false, true);
                                                     configBuilder
                                                         .AddJsonFile($"appsettings.{environmentName}.json",
                                                                      true, true);
                                                 })
                      .ConfigureServices((hosting, services) =>
                                         {
                                             var appSetting = hosting.Configuration.Get<AppSetting>();
                                             services.AddSingleton(typeof(AppSetting), appSetting);
                                             
                                             //注入其他服務
                                             services.AddSingleton<AppWorkFlow1>();
                                         })
        ;
    var host     = builder.Build();
    var service  = host.Services.GetService<AppWorkFlow1>();
    var playerId = service.GetPlayerId();
    Console.WriteLine($"PlayerId = {playerId}");
}

 public class AppWorkFlow1 : IAppWorkFlow
{
    private AppSetting _appSetting;

    public AppWorkFlow1(AppSetting appSetting)
    {
        this._appSetting = appSetting;
    }
    public string GetPlayerId()
    {
        return this._appSetting.Player.AppId;
    }
}

 

AppWorkFlow1 建構函數依賴 AppSetting

public class AppWorkFlow1 : IAppWorkFlow
{
    private AppSetting _appSetting;

    public AppWorkFlow1(AppSetting appSetting)
    {
        this._appSetting = appSetting;
    }
    public string GetPlayerId()
    {
        return this._appSetting.Player.AppId;
    }
}

 

範例位置

sample.dotblog/SurveyOptionTests.cs at master · yaochangyu/sample.dotblog (github.com)

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo