配置IConfiguration

前言

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

我們希望新的配置:

  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 ": "//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 ": "//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

//github.com/zhenlei520/MasaFramework.Practice

開源地址

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

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

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

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

16373211753064.png