Configuration Bind

在上一篇文章中我們學到了該怎麼使用 Configuration 相關的 Package,程式將透過 DI 容器取得 ConfigurationRoot 可以方便 讀取 appsettings.json 中的設定值,不過目前是採用字典搭配 Key 值得方式來取回值有點不太方便而且有輸入錯誤的可能性, 因此我們可以先將設定值解析到一個專用的 Class 上在註冊到 DI 容器內,這樣之後就可以使用強型別方式來取值了,這也是昨天有提到 但沒有詳談的 Package Microsoft.Extensions.Configuration.Binder 背後會做的工作。

首先先建立一個新的 Web 專案

dotnet new web -o HelloWeb

根據昨天學到的知識我們已經知道專案有裝 Microsoft.Extensions.Hosting 並且註冊一個 IHost 其它事情 Dotnet 會幫我們註冊好 我們可以直接使用 appsettings.json 同時內部也有參考 Microsoft.Extensions.Configuration.Binder 所以我們可以直接使用。

接著修改 appsettings.json 的內容並建立 AppSettings.cs

// appsettings.json
{
  "JsonSettings": {
    "From": "MyJsonSettings"
  }
}
// AppSettings.cs
public class AppSettings
{
    public string From { get; set; }
}

根據目前現有的知識寫出以下程式,我們在昨天也有從 DI 容器內取出 IConfiguration 也就是 ConfigurationRoot,今天建立了一個 AppSettings 類別 在透過 Bind 方法把 ConfigurationRoot 的設定值映射到 AppSettings 上,本質上你自己手動映射也可以達到同樣的效果。

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

var configuration = app.Services.GetRequiredService<IConfiguration>();
var settings = new AppSettings();
configuration.GetSection("JsonSettings").Bind(settings);

//var settings = new AppSettings();
//settings.From = configuration.GetSection("JsonSettings:From").Value;

Console.WriteLine(settings.From);

但是這種寫法好像沒有解決什麼問題,我們還是需要從 DI 取回 ConfigurationRoot 並透過字串取得設定值最後進行映射,等於是你之後每個 Controller 想要 取得設定值都要重複這個流程,那這樣強型別反而會變得有點麻煩乾脆以後都透過字串來取值就好了。

所以我們需要換一個思路,那就是讀取完設定值並映射完後就直接註冊到 DI 容器內,這樣之後就可以不用每次都取回 ConfigurationRoot 只需要取回 AppSettings 類別即可。

這一個版本的程式改在 WebApplication Build 之前就先完成映射並註冊到 DI 容器內,這樣之後在任何一個 Controller 都能取得設定值

using HelloWeb;

var builder = WebApplication.CreateBuilder(args);

var settings = new AppSettings();
builder.Configuration.GetSection("JsonSettings").Bind(settings);

builder.Services.AddSingleton<AppSettings>(settings);

var app = builder.Build();

var mySetting = app.Services.GetRequiredService<AppSettings>();

Console.WriteLine(mySetting.From);

IOptions

其實在 Dotnet 有額外封裝了一系列的方法讓我們更加方便管理設定值, Options 相關的 Package 有以下幾個

  • Microsoft.Extensions.Options
  • Microsoft.Extensions.Options.ConfigurationExtensions
  • Microsoft.Extensions.Options.DataAnnotations

這幾個 Package 最主要的功能就是管理設定值的生命週期,關鍵在於以下這幾個 Interface 和 AddOptions 方法

  • IOptions
  • IOptionsSnapshot
  • IOptionsMonitor
public static IServiceCollection AddOptions(this IServiceCollection services)
{
    ThrowHelper.ThrowIfNull(services);

    services.TryAdd(ServiceDescriptor.Singleton(typeof(IOptions<>), typeof(UnnamedOptionsManager<>)));
    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 方法可以看出 IOptions 和 IOptionsMonitor 註冊為 Singleton, IOptionsSnapshot 註冊為 Scoped, 這三個 Interface 主要使用來判斷設定值是否有改變,也就是說程式在運行時有沒有人手動修改 appsettings.json

如果不在意 appsettings.json 是否有被修改可以使用 IOptions 注入,但這也代表你需要重啟程式設定值才會生效。 IOptionsSnapshot 和 IOptionsMonitor 則代表需要監控 appsettings.json 的設定值一但有修改就要取代就有的設定值, 其中 IOptionsMonitor 代表即時監控一有修改就要馬上反應,IOptionsSnapshot 則是在目前 Scope 結束後的下一個 Scope 才會使用新設定。

接下來我們修改一下專案並修改注入的方式和建立新的 Controller。 這裡直接使用 Configure 來註冊類別,要注意這種註冊設定值的方式之後要取回就必須使用上面提到的三種 Options Interface,才能從 DI 容器取回設定值。

using HelloWeb;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.Configure<AppSettings>(
    builder.Configuration.GetSection("JsonSettings"));
var app = builder.Build();
app.MapControllers();
app.Run();
// HelloController.cs

[ApiController]
[Route("[controller]")]
public class HelloController: ControllerBase
{
    private readonly IOptions<AppSettings> _options;

    public HelloController(IOptions<AppSettings> options)
    {
        _options = options;
    }

    [HttpGet(Name = "Get")]
    public string Get()
    {
        return _options.Value.From;
    }
}

完成後我們直接訪問 API https://localhost:7207/Hello 會回傳 MyJsonSettings 到網頁上,這時如果直接修改 appsettings.json 回傳值也不會時變動需要重啟程式設定值才會變動。

{
  "JsonSettings": {
    "From": "MyJsonSettings1"
  }
}

接下來將 IOptions 改成 IOptionsSnapshot 並再次運行剛剛的流程,會發現即使不重啟程式設定值也會馬上進行更動。

[ApiController]
[Route("[controller]")]
public class HelloController: ControllerBase
{
    private readonly IOptionsSnapshot<AppSettings> _options;

    public HelloController(IOptionsSnapshot<AppSettings> options)
    {
        _options = options;
    }

    [HttpGet(Name = "Get")]
    public string Get()
    {
        return _options.Value.From;
    }
}

Summary

今天學習了如何將設定值改成強型別的形式讀取,並且了解使用 IOptions 來管理我們的設定值,可以跳過映射與注入等流程而且還能夠 有即時監控設定檔的功能,這樣就不需要每次修改設定值就重啟服務了在某些場合會十分有用。