開發的過程中難免會於組態檔中存取應用程式的特定資訊,
在 .NET Core 中拋棄了過去存於 Web.Config 的方式,
而將組態預設存放在 appsetting.json 中。
官方預設提供了 IOptions<T> 讓我們能夠以強型別的方式繫結組態,
但是使用起來總覺得不是那麼順手,
本文介紹如何透過自訂擴充方法簡化組態注入方式。
筆者開發環境使用 .NET Core 3.1 的 MVC 範本專案。
IOption<T>
IOption<T> 為 .NET Core 內建的組態設定注入方式,
可從多個組態提供者 ( ConfigurationProvider ) 中以強型別的方式繫結組態。
這邊我們修改預設的 appsetting.json ,內容如下:
{
"MyAuth": {
"LoginUrl": "https://mysso.test.com/login",
"RedirectTo": "/Home/Index",
"AppName": "my_web",
"AppSecret": "my_app_secret"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
為了進行強型別的資料繫結,必須建立一個屬性與其對應的類別。
AuthConfig.cs
public class AuthConfig
{
public string LoginUrl { get; set; }
public string RedirectTo { get; set; }
public string AppName { get; set; }
public string AppSecret { get; set; }
}
接著到 Startup.cs 中註冊組態注入服務。
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.Configure<AuthConfig>(Configuration.GetSection("MyAuth"));
//...
}
//...
}
其中 IConfiguration 的 GetSection 方法可根據 key 取得某區塊的組態物件,
並回傳一個代表組態區塊的 IConfigurationSection。
而 Configure<T> 則需要給定一個代表組態資訊的 IConfiguration,
由於 IConfigurationSection 實作了 IConfiguration 介面,
所以這邊我們可以直接將 GetSection 的結果作為參數傳入。
然後在 HomeController.cs 中使用 IOptions<AuthConfig> 的方式進行注入。
public class HomeController : Controller
{
private AuthConfig _authConfig;
public HomeController(IOptions<AuthConfig> authConfig)
{
_authConfig = authConfig.Value;
}
}
接著透過偵錯模式觀察注入結果。
這樣就完成基本的組態注入了。
但這邊有幾個我覺得比較弔詭的地方,
首先,既然我已經自訂了一個 AuthConfig 的組態類別,
為什麼還需要依賴 IOptions<T> 來完成注入呢?
而看 IOptions<T> 的原始碼可以看到它只有提供一個 Value 的屬性而以。
也就是說多透過這層依賴或許是沒有必要。
而另外一個問題是生命週期的管理,
當使用 IOptions<T> 進行注入時預設的注入生命週期為 Singleton,
如果你的組態有 Reload 的需求時必須改用 IOptionSnapshot<T> 注入,
這樣注入週期則會改為 Scoped 。
但對於組態注入的呼叫端而言,
本身應該只需關注於注入物件的取用,
而無須涉及注入物件的生命週期管理。
使用內建 DI 注入組態
為了消除對 IOptions<T> 的依賴,
我們可以使用內建的 DI 直接註冊組態物件。
但這邊還需要藉由 IConfiguration 介面所提供的 Bind 方法,
它能夠幫我們將組態資訊繫結到另外一個物件對象身上,範例程式如下。
Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
//services.Configure<AuthConfig>(Configuration.GetSection("MyAuth"));
services.AddSingleton(p =>
{
var config = new AuthConfig();
var section = Configuration.GetSection("MyAuth");
section.Bind(config);
return config;
});
//...
}
//...
}
測試注入結果如下。
使用自訂擴充方法
現在注入組態已經不需要依賴 IOptions<T> 了,
而且也可以從服務註冊端控制注入組態的生命週期,
但寫起來還是稍嫌麻煩,
所以我們可以透過簡單的反射及擴充方法的方式再包一層,
擴充 IServiceCollection 介面以滿足所有注入的生命週期。
方法如下:
- AddSigletonConfig<TConfig>(IConfiguration section)
- AddScopedConfig<TConfig>(IConfiguration section)
- AddTransientConfig<TConfig>(IConfiguration section)
- AddConfig<TConfig>(IConfiguration section, ServiceLifetime lifetime)
實作程式碼如下:
ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddSingletonConfig<TConfig>(this IServiceCollection services, IConfiguration section) where TConfig : class
{
services.AddSingleton(p => BindConfigInstance<TConfig>(section));
return services;
}
public static IServiceCollection AddScopedConfig<TConfig>(this IServiceCollection services, IConfiguration section) where TConfig : class
{
services.AddScoped(p => BindConfigInstance<TConfig>(section));
return services;
}
public static IServiceCollection AddTransientConfig<TConfig>(this IServiceCollection services, IConfiguration section) where TConfig : class
{
services.AddTransient(p => BindConfigInstance<TConfig>(section));
return services;
}
public static IServiceCollection AddConfig<TConfig>(this IServiceCollection services, IConfiguration section, ServiceLifetime lifetime) where TConfig : class
{
switch (lifetime)
{
case ServiceLifetime.Singleton:
services.AddSingleton(p => BindConfigInstance<TConfig>(section));
break;
case ServiceLifetime.Scoped:
services.AddScoped(p => BindConfigInstance<TConfig>(section));
break;
case ServiceLifetime.Transient:
services.AddTransient(p => BindConfigInstance<TConfig>(section));
break;
default:
throw new UnexpectedEnumValueException($"Value of enum {typeof(ServiceLifetime)}: {nameof(ServiceLifetime)} is not supported.");
}
return services;
}
private static TConfig BindConfigInstance<TConfig>(IConfiguration section) where TConfig : class
{
var instance = Activator.CreateInstance<TConfig>();
section.Bind(instance);
return instance;
}
}
public class UnexpectedEnumValueException : Exception
{
public UnexpectedEnumValueException(string message) : base(message)
{
}
}
如此一來就可以使組態註冊方式再更簡單一點,
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
//services.Configure<AuthConfig>(Configuration.GetSection("MyAuth"));
//services.AddSingleton(p =>
//{
// var config = new AuthConfig();
// var section = Configuration.GetSection("MyAuth");
// section.Bind(config);
// return config;
//});
services.AddSingletonConfig<AuthConfig>(Configuration.GetSection("MyAuth"));
services.AddScopedConfig<AuthConfig>(Configuration.GetSection("MyAuth"));
services.AddTransientConfig<AuthConfig>(Configuration.GetSection("MyAuth"));
services.AddConfig<AuthConfig>(Configuration.GetSection("MyAuth"), ServiceLifetime.Singleton);
services.AddConfig<AuthConfig>(Configuration.GetSection("MyAuth"), ServiceLifetime.Scoped);
services.AddConfig<AuthConfig>(Configuration.GetSection("MyAuth"), ServiceLifetime.Transient);
//...
}
這邊重複註冊不同的生命週期只是為了示範,
請依實際需求修改後再使用。
完整程式碼詳如 Github: https://github.com/robersonliou/NetCoreOptionSample
如有謬誤歡迎指正,覺得有幫助請賞個 Star。