設定IConfiguration

2022-08-31 18:03:04

前言

設定是我們必不可少的功能,我們在開發中,經常會遇到需要獲取設定資訊的需求,那麼如何才能優雅的獲取設定資訊?

我們希望新的設定:

  1. 支援強型別
  2. 設定變更後通知
  3. 學習難度低

快速入門

根據使用場景我們將設定分為本地設定以及遠端設定,下面我們就來看一下本地設定與遠端設定是如何來使用的?

本地設定

  1. 新建ASP.NET Core 空專案Assignment.MasaConfiguration,並安裝Masa.Contrib.Configuration
dotnet new web -o Assignment.MasaConfiguration
cd Assignment.MasaConfiguration
dotnet add package Masa.Contrib.Configuration --version 0.6.0-preview.7
  1. 新建類AppConfigConnectionStrings,用於儲存資料庫設定
/// <summary>
/// 應用設定類
/// </summary>
public class AppConfig : LocalMasaConfigurationOptions
{
    public ConnectionStrings ConnectionStrings { get; set; }
}

public class ConnectionStrings
{
    public string DefaultConnection { get; set; }
}
  1. 修改檔案appsettings.json
{
  "AppConfig": {
    "ConnectionStrings": {
      "DefaultConnection": "server=localhost;uid=sa;pwd=P@ssw0rd;database=identity"
    }
  }
}
  1. 註冊MasaConfiguration,修改類Program
builder.AddMasaConfiguration();
  1. 如果使用?修改類Program
app.MapGet("/AppConfig", (IOptions<AppConfig> appConfig)
{
    return appConfig.Value.ConnectionStrings.DefaultConnection);
});

如果希望監聽設定變更事件,則可使用IOptionsMonitorOnChange方法

遠端設定

目前我們遠端設定的能力僅實現了Dcc, 下面就讓我們看看如何來使用它

  1. 選中Assignment.MasaConfiguration,並安裝Masa.Contrib.Configuration.ConfigurationApi.Dcc
dotnet add package Masa.Contrib.Configuration.ConfigurationApi.Dcc --version 0.6.0-preview.7
  1. 修改appsettings.json
{
  //Dcc設定,擴充套件Configuration能力,支援遠端設定
  "DccOptions": {
    "ManageServiceAddress ": "http://localhost:8890",
    "RedisOptions": {
      "Servers": [
        {
          "Host": "localhost",
          "Port": 8889
        }
      ],
      "DefaultDatabase": 0,
      "Password": ""
    }
  }
}
  1. 新建類RedisOptions, 用於設定業務專案中使用的快取地址
public class RedisOptions : ConfigurationApiMasaConfigurationOptions
{
    public string Host { get; set; }

    public int Port { get; set; }

    public string Password { get; set; }

    public int DefaultDatabase { get; set; }
}
  1. 修改類Program
var app = builder.AddMasaConfiguration(configurationBuilder =>
{
    configurationBuilder.UseDcc();
}).Build();
  1. 如何使用?
// 推薦使用,通過IOptions<TOptions>獲取設定,支援強型別
app.MapGet("/AppConfig", (IOptions<RedisOptions> options)
{
    return options.Value.Host;
});

進階

到目前為止,我們已經學會了如何使用Masa提供的設定,但只有瞭解原理,我們才敢在專案中大膽的用起來,出現問題後才能快速的定位並解決問題,下面我們就來深入瞭解下

分類

根據使用場景我們將設定劃分為:

  • 本地設定(設定儲存在本地組態檔中,後期設定變更不變)
  • 遠端設定(設定在遠端設定中心、例如Dcc、Apollo、其它設定中心)

IConfiguration結構

在使用MasaConfiguration後,IConfiguration的檔案結構變更為:

IConfiguration
├── Local                                本地節點(固定)
│   ├── Platforms                        自定義設定
│   ├── ├── Name                         引數
├── ConfigurationAPI                     遠端節點(固定)
│   ├── AppId                            替換為你的AppId
│   ├── AppId ├── Platforms              自定義節點
│   ├── AppId ├── Platforms ├── Name     引數

除了一下設定源以及設定的提供者提供的設定除外,其餘的設定會遷移到Local節點下

全域性設定

MasaConfiguration中提供了全域性設定的功能,並預設支援AppIdEnvironmentCluster

  1. 優先順序

獲取引數值的優先順序為:

自定義全域性設定 > 從IConfiguration中獲取(支援命令、環境變數、組態檔) > 約定設定
  1. 自定義全域性設定
service.Configure<MasaAppConfigureOptions>(options => 
{
  options.AppId = "Replace-With-Your-AppId";
  options.Environment = "Replace-With-Your-Environment";
  options.Cluster = "Replace-With-Your-Cluster";

  options.TryAdd("Replace-With-Your-ConfigKey", "Replace-With-Your-ConfigValue");// 自定義全域性設定鍵、值
})
  1. IConfiguration中獲取

當未指定設定的值時,將會從設定中獲取得到設定的值,預設設定與Key的關係為:

  • AppId: AppId
  • Environment: ASPNETCORE_ENVIRONMENT
  • Cluster: Cluster

當命令列與環境變數獲取引數失敗後,則會嘗試從組態檔根據設定的Key獲取對應的值

  1. 約定預設值

當未自定義設定,且無法從IConfiguration中獲取到相對應引數的設定後,我們將根據約定好的規則生成對應的值

  • AppId: 啟動程式名.Replace(".", "-")
  • Environment: Production
  • Cluster: Default

設定對映

在快速入門的例子中,看似很簡單就可以通過IOptions<TOptions>獲取到AppConfig的設定資訊以及Dcc中設定的Redis資訊,這一切是如何做到的呢?

在MasaConfiguration中提供了兩種對映方式,用來對映設定與類的對應關係,分別是:自動對映、手動對映。

  1. 自動對映

分為本地設定以及遠端設定的自動對映

  • 本地設定: 由Masa.Contrib.Configuration提供
  • 遠端設定
    • Dcc: 由Masa.Contrib.Configuration.ConfigurationApi.Dcc提供

1.1 當設定儲存在本地時,則將對應的設定類繼承LocalMasaConfigurationOptions

// <summary>
/// 應用設定類
/// </summary>
public class AppConfig : LocalMasaConfigurationOptions
{
    // /// <summary>
    // /// 如果當前設定掛載在根節點(一級節點)時,則無需過載,如果掛載在二級節點時,則需要過載ParentSection並賦值為一級節點名
    // /// 根節點名:預設為一級節點,可不寫,格式:一級節點:二級節點:三級節點……
    // /// </summary>
    // [JsonIgnore]
    // public override string? ParentSection => null;

    // /// <summary>
    // /// 如果類名與節點名保持一致,則可忽略不寫,否則重寫`Section`並賦值為節點名
    // /// </summary>
    // [JsonIgnore]
    // public override string? Section => "RabbitMq";

    public ConnectionStrings ConnectionStrings { get; set; }
}

public class ConnectionStrings
{
    public string DefaultConnection { get; set; }
}

當設定中的引數直接平鋪掛載根節點下,而不是掛載到跟節點下的某個指定節點時,ParentSection無需過載,Section需要過載並賦值為空字串

1.2 當設定儲存在Dcc,則將對應的設定類繼承ConfigurationApiMasaConfigurationOptions

public class RedisOptions : ConfigurationApiMasaConfigurationOptions
{
    /// <summary>
    /// 設定所屬的AppId,當AppId與預設AppId一致時,可忽略
    /// </summary>
    // public virtual string AppId { get; }

    /// <summary>
    /// Dcc的設定物件名稱,當設定物件名稱與類名一致時,可忽略
    /// </summary>
    // public virtual string? ObjectName { get; }

    public string Host { get; set; }

    public int Port { get; set; }

    public string Password { get; set; }

    public int DefaultDatabase { get; set; }
}
  1. 手動對映

雖然自動對映的方式很簡單,也很方便,但總是有一些場景使得我們無法通過自動對映來做,那如何手動指定對映關係呢?

為了方便大家理解,手動對映仍然使用AppConfig以及Redis來舉例

builder.AddMasaConfiguration(configurationBuilder =>
{
    configurationBuilder.UseDcc();//使用Dcc 擴充套件Configuration能力,支援遠端設定

    configurationBuilder.UseMasaOptions(options =>
    {

        options.MappingLocal<AppConfig>("AppConfig");//其中引數"AppConfig"可不寫(當類與節點名稱一致時可忽略)
        options.MappingConfigurationApi<RedisOptions>("{替換為Dcc中設定所屬的AppId}", "{設定物件名稱}");//其中設定物件名稱可不寫(當設定物件名與類名一致時可忽略)
    });
});

Dcc設定

完整的Dcc設定如下:

{
  "DccOptions": {
    "ManageServiceAddress ": "http://localhost:8890",
    "RedisOptions": {
      "Servers": [
        {
          "Host": "localhost",
          "Port": 8889
        }
      ],
      "DefaultDatabase": 0,
      "Password": ""
    },
    "AppId": "Replace-With-Your-AppId",
    "Environment": "Development",
    "ConfigObjects": [ "Platforms" ],
    "Secret": "", 
    "Cluster": "Default",
    "ExpandSections" : [
        {
            "AppId": "Replace-With-Your-AppId",
            "Environment": "Development",
            "ConfigObjects": [ "Platforms" ], 
            "Secret": "",
            "Cluster": "Default",
        }
    ],
    "PublicId": "Replace-With-Your-Public-AppId",
    "PublicSecret": "Replace-With-Your-Public-AppId-Secret"
  }
}
  • ManageServiceAddress: 用於更新遠端設定使用,非必填
  • RedisOptions:Dcc會在Redis中儲存設定的副本,此處是儲存Dcc設定的的Redis地址(*)
  • AppId:專案中需要獲取設定的AppId,也被稱為Dcc的預設AppId,當未賦值時從全域性設定中獲取
  • Environment:專案中需要獲取設定的環境資訊,當未賦值時從全域性設定中獲取
  • ConfigObjects:專案中需要使用的設定物件名稱,未賦值時預設獲取當前環境、當前叢集、當前AppId下的全部設定物件
  • Secret:祕鑰,用於更新遠端設定,每個AppId有一個祕鑰,非必填(不可使用更新遠端設定的能力)
  • Cluster:需要載入設定的叢集,後面我們簡稱為Dcc的預設叢集,未賦值時從全域性設定中獲取
  • PublicId:Dcc中公共設定的AppId,預設:public-$Config,非必填
  • PublicSecret:Dcc中公共設定的AppId的祕鑰,非必填
  • ExpandSections:擴充套件設定的集合,適用於當前應用需要獲取多個AppId下的設定時使用,其中AppId為必填項、Environment、Cluster為非必填項,當不存在時將與Dcc預設環境、叢集一致,非必填

擴充套件其它的設定中心

上面提到了目前的遠端設定能力僅支援Dcc,那如果我希望接入自己開發的設定中心或者其它更優秀的設定中心需要接入如何做?

Apollo為例:

  1. 新建類庫Masa.Contrib.Configuration.ConfigurationApi.Apollo

  2. 新建ApolloConfigurationRepository並實現類AbstractConfigurationRepository

internal class ApolloConfigurationRepository : AbstractConfigurationRepository
{
    private readonly IConfigurationApiClient _client;
    public override SectionTypes SectionType => SectionTypes.ConfigurationAPI;

    public DccConfigurationRepository(
        IConfigurationApiClient client,
        ILoggerFactory loggerFactory)
        : base(loggerFactory)
    {
        _client = client;
        
        //todo: 藉助 IConfigurationApiClient 獲取需要掛載到遠端節點的設定資訊並監聽設定變化
        // 當設定變更時觸發FireRepositoryChange(SectionType, Load());
    }

    public override Properties Load()
    {
        //todo: 返回當前掛載到遠端節點的設定資訊
    }
}
  1. 新建類ConfigurationApiClient,為ConfigurationApi提供獲取基礎設定的能力
public class ConfigurationApiClient : IConfigurationApiClient
{
    public Task<(string Raw, ConfigurationTypes ConfigurationType)> GetRawAsync(string configObject, Action<string>? valueChanged = null)
    {
        throw new NotImplementedException();
    }

    public Task<(string Raw, ConfigurationTypes ConfigurationType)> GetRawAsync(string environment, string cluster, string appId, string configObject, Action<string>? valueChanged = null)
    {
        throw new NotImplementedException();
    }

    public Task<T> GetAsync<T>(string configObject, Action<T>? valueChanged = null);
    {
        throw new NotImplementedException();
    }  
    public Task<T> GetAsync<T>(string environment, string cluster, string appId, string configObject, Action<T>? valueChanged = null);
    {
        throw new NotImplementedException();
    }  
    public Task<dynamic> GetDynamicAsync(string environment, string cluster, string appId, string configObject, Action<dynamic> valueChanged)
    {
        throw new NotImplementedException();
    }

    public Task<dynamic> GetDynamicAsync(string key)
    {
        throw new NotImplementedException();
    }
}
  1. 新建類ConfigurationApiManage,為ConfigurationApi提供管理設定的能力
public class ConfigurationApiManage : IConfigurationApiManage
{

    // 通過管理端初始化AppId下的遠端設定
    public Task InitializeAsync(string environment, string cluster, string appId, Dictionary<string, string> configObjects)
    {
        throw new NotImplementedException();
    }

    // 通過管理端更新指定設定的資訊
    public Task UpdateAsync(string environment, string cluster, string appId, string configObject, object value)
    {
        throw new NotImplementedException();
    }
}
  1. 新建ConfigurationApiMasaConfigurationOptions類,並繼承MasaConfigurationOptions

我們希望其它自定義設定也能根據約定實現自動對映,我們也清楚不同的設定中心中儲存設定的名稱是不一樣的,例如在Apollo中設定物件名稱叫做名稱空間,因此為了方便開發人員可以使用起來更方便,我們建議不同的設定中心可以有自己專屬的屬性,比如ApolloNamespace,以此來降低開發人員的學習成本

public abstract class ConfigurationApiMasaConfigurationOptions : MasaConfigurationOptions
{
    /// <summary>
    /// The name of the parent section, if it is empty, it will be mounted under SectionType, otherwise it will be mounted to the specified section under SectionType
    /// </summary>
    [JsonIgnore]
    public sealed override string? ParentSection => AppId;

    //
    public virtual string AppId => StaticConfig.AppId;

    /// <summary>
    /// The section null means same as the class name, else load from the specify section
    /// </summary>
    [JsonIgnore]
    public sealed override string? Section => Namespace;

    /// <summary>
    /// 
    /// </summary>
    public virtual string? Namespace { get; }

    /// <summary>
    /// Configuration object name
    /// </summary>
    [JsonIgnore]
    public sealed override SectionTypes SectionType => SectionTypes.ConfigurationApi;
}
  1. 選中類庫Masa.Contrib.BasicAbility.Apollo,並新建IMasaConfigurationBuilder的擴充套件方法UseApollo
public static class MasaConfigurationExtensions
{
    public static IMasaConfigurationBuilder UseApollo(this IMasaConfigurationBuilder builder)
    {
        //todo:將IConfigurationApiClient、IConfigurationApiManage註冊到到服務集合中,並通過builder.AddRepository()新增ApolloConfigurationRepository
        return builder;
    }
}

總結

  1. 如何使用MasaConfiguration?

    • 新增:builder.AddMasaConfiguration()
  2. 為何通過IOptions獲取到的設定為空,但通過IConfiguration或者IMasaConfiguration根據節點可以獲取到?

    • 檢查下是否沒有繫結節點關係,如何繫結節點關係請檢視問題2
    • 檢查節點繫結是否錯誤
  3. IConfigurationApiClientIConfiguration之間有什麼關係?

    • IConfigurationApiClientIConfigurationApiManage分別是管理遠端Api的使用者端以及管理端,與IConfiguration相比,IConfigurationApiClient的資訊更全,每次獲取設定需要像設定中心請求獲取資料,而IConfiguration是通過呼叫IConfigurationApiClient將需要使用的設定物件獲取並新增到IConfiguration中,後續使用者獲取設定時無需向設定中心請求資料
  4. 遠端設定物件更新後,IConfiguration中的資訊會更新嗎?為什麼?

    • 會更新、遠端設定更新後會通過valueChanged通知遠端設定的提供者,然後遠端設定的提供者會重新整理原生的遠端設定並通知IConfiguration重新重新整理資料

Dcc: Distributed Configuration Center 是一個以DDD為指導思想、使用.Net6.0開發的分散式設定中心

本章原始碼

Assignment08

https://github.com/zhenlei520/MasaFramework.Practice

開源地址

MASA.Framework:https://github.com/masastack/MASA.Framework

MASA.EShop:https://github.com/masalabs/MASA.EShop

MASA.Blazor:https://github.com/BlazorComponent/MASA.Blazor

如果你對我們的 MASA Framework 感興趣,無論是程式碼貢獻、使用、提 Issue,歡迎聯絡我們