使用.NET 6開發TodoList應用(26)——實現Configuration和Option的強類型綁定

系列導航及源代碼

需求

在上一篇文章使用.NET 6開發TodoList應用(25)——實現RefreshToken中,我們通過使用Configuration獲取方法GetSection拿到寫在appsettings.Development.json中JWT的相關配置字段,這樣實現沒有問題,但是我們有更好的選擇:通過使用強類型的Configuration綁定方法,或者通過Options相關方法來實現。在本文中我們將會分別來看一下這兩種方法的實現。

目標

實現配置字段的強類型綁定,分別通過Configuration綁定和Options實現。

原理與思路

要實現強類型綁定,首先我們需要定義這個配置類型。然後根據需求,選擇使用Configuration綁定實現或者使用Options配置實現。二者實現的功能上有一些區別:使用Options模式提供了更多的功能,如校驗、熱加載,也更方便進行測試。

實現

定義配置類型

根據我們在appsettings.Development.json中的配置:

"JwtSettings": {
  "validIssuer": "TodoListApi",
  "validAudience": "//localhost:5050",
  "expires": 5
}

Application/Configurations中添加JwtConfiguration類如下:

  • JwtConfiguration.cs
namespace TodoList.Application.Common.Configurations;

public class JwtConfiguration
{
    public string Section { get; set; } = "JwtSettings";
    public string? ValidIssuer { get; set; }
    public string? ValidAudience { get; set; }
    public string? Expires { get; set; }
}

方法1: 通過Configuration綁定實現

修改Infrastructure項目中的DependencyInjection添加認證方法的邏輯:

  • DependencyInjection.cs
// 省略其他...
// 添加認證方法為JWT Token認證
var jwtConfiguration = new JwtConfiguration();
configuration.Bind(jwtConfiguration.Section, jwtConfiguration);

services
    .AddAuthentication(opt =>
    {
        opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            // 改為使用配置類成員獲取
            ValidIssuer = jwtConfiguration.ValidIssuer,
            ValidAudience = jwtConfiguration.ValidAudience,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("SECRET") ?? "TodoListApiSecretKey"))
        };
    });

修改IdentityService中的邏輯,添加一個私有字段

  • IdentityService.cs
// 添加JWT配置類字段
private readonly JwtConfiguration _jwtConfiguration;

// 構造函數中進行初始化
// 初始化配置對象
_jwtConfiguration = new JwtConfiguration();
configuration.Bind(_jwtConfiguration.Section, _jwtConfiguration);

並將所有之前使用json對象獲取字段值的地方都修改成通過私有字段的成員變量獲取:

// 省略其他...
ValidIssuer = _jwtConfiguration.ValidIssuer,
ValidAudience = _jwtConfiguration.ValidAudience

驗證如下,我們還是通過獲取refresh token來檢查配置是否成功:
image

看起來沒什麼問題。下面我們看第二種方法,也是相對比較推薦的做法。

方法2: 通過IOptions配置實現

我們在Infrastructure/DependencyInjection.cs中添加使用IOptions的配置:

  • DependencyInjection.cs
// 使用IOptions配置
services.Configure<JwtConfiguration>(configuration.GetSection("JwtSettings"));

然後通過依賴注入的方式去修改IdentityService

  • IdentityService.cs
public IdentityService(
    ILogger<IdentityService> logger,
    IConfiguration configuration,
    UserManager<ApplicationUser> userManager,
    IOptions<JwtConfiguration> jwtOptions)
{
    _logger = logger;
    _userManager = userManager;
    // 初始化配置對象
    _jwtConfiguration = jwtOptions.Value;
}

其他的不需要再進行修改。下面來驗證一下效果,驗證方法和剛才一致:
image
可以看到依然是沒有問題的。

一點擴展

擴展1: 關於配置熱加載

綜合我們剛才提到的和所演示的可以看到,我們並沒有演示關於配置熱加載的功能,如果在程序的運行過程中,我們希望配置文件的改動能夠直接反映到應用中,不需要重啟應用,可以預想到這個功能還是很重要的。

這個功能是通過IOptionsSnapshot或者IOptionsMonitor來實現的,我們所需要做的就是在依賴注入的時候使用IOptionsSnapshot<JwtConfiguration>或者IOptionsMonitor<JwtConfiguration>代替我們之前使用的IOptions<JwtConfiguration>。在替換的過程中之需要注意以下兩點即可:

  1. IOptionsSnapshot本身是註冊為ScopedService,所以不能注入到SingletonService中使用;
  2. IOptionsMonitor本身註冊為了SingletonService,所以可以注入到SingletonService中使用,但是在取值的時候不是使用Value而是使用CurrentValue

我們使用IOptionsMonitor來舉例子驗證,只需要修改IdentityService中構造函數的注入部分:

  • IdentityService.cs
public IdentityService(
    ILogger<IdentityService> logger,
    UserManager<ApplicationUser> userManager,
    IOptionsMonitor<JwtConfiguration> jwtOptions)
{
    _logger = logger;
    _userManager = userManager;
    // 使用IOptionsMonitor加載配置
    _jwtConfiguration = jwtOptions.CurrentValue;
}

重新運行項目,我們先直接請求Token:
image

解析出的payload如下,過期時間是之前設置的5分鐘後:
image

在不重啟應用的情況下,我們去修改appsettings.Development.json中關於過期時間的配置,將過期時間設置為10分鐘:

"JwtSettings": {
  "validIssuer": "TodoListApi",
  "validAudience": "//localhost:5050",
  "expires": 10
}

再次執行獲取Token的請求,查看Header里的Date字段值:
image

並把token解析:
image

這個過期時間已經變成10分鐘後了,大家可以自己動手試一下。

擴展2: 關於相同類型的多個配置Section處理

有一種情況是在appsettings.Development.json中我們可能會做這樣的配置:

"JwtSettings": {
  "validIssuer": "TodoListApi",
  "validAudience": "//localhost:5050",
  "expires": 5
},
"JwtApiV2Settings": {
  "validIssuer": "TodoListApiV2",
  "validAudience": "//localhost:5050",
  "expires": 10
}

面對這種情況,我們可以在進行IOptions配置時指定配置名稱,像這樣:

// 使用IOptions配置
services.Configure<JwtConfiguration>("JwtSettings", configuration.GetSection("JwtSettings"));
services.Configure<JwtConfiguration>("JwtApiV2Settings", configuration.GetSection("JwtApiV2Settings"));

而在需要注入使用的地方也指定對應要進行配置的名稱即可:

// 使用IOptionsMonitor加載配置
_jwtConfiguration = jwtOptions.Get("JwtApiV2Settings");

這樣就可以正確地使用相應的配置了,就不再繼續演示了。

總結

關於三種IOptions的對比見下表:

類型 依賴注入類型 是否支持配置熱加載 配置加載更新時機 是否支持名稱配置
IOptions<T> Singleton注入 只在程序運行開始時綁定一次,以後每次獲取的都是相同值
IOptionsSnapshot<T> Scoped注入 每次請求時都會重新加載配置
IOptionsMonitor<T> Singleton注入 配置的值被緩存起來了,當原始配置發生變化時立即發生更新

在本文中我們介紹了如何使用強類型綁定配置項,以及如何實現配置的熱加載。對於沒有涉及到的諸如配置項的校驗等內容(可以通過Annotation實現校驗)可以參考官方文檔:Options validation

參考資料

  1. Options validation