配置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