[ .NET Core ] 使用擴充方法擺脫 IOptions<T> 的組態設定方式

開發的過程中難免會於組態檔中存取應用程式的特定資訊,

.NET Core 中拋棄了過去存於 Web.Config 的方式,

而將組態預設存放在 appsetting.json 中。

官方預設提供了 IOptions<T> 讓我們能夠以強型別的方式繫結組態,

但是使用起來總覺得不是那麼順手,

本文介紹如何透過自訂擴充方法簡化組態注入方式。

筆者開發環境使用 .NET Core 3.1MVC 範本專案。

 

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。