2002年以前,配置主流是Ini文件,简单易用;2002年以后,配置主流是Xml文件,结构化数据更严格,SOA更是把它带向更高的层次;2012年以后,Json随着Web流程而兴起,Json配置文件成为主流;在这个过程中,中大型系统逐步向分布式系统过渡,配置中心逐步成为主流。

总结近20年的经验,抽象建立了一套配置系统IConfigProvider。

Nuget包:NewLife.Core

源码:https://github.com/NewLifeX/X/tree/master/NewLife.Core/Configuration

视频:https://www.bilibili.com/video/BV1We4y1q7Wi


最佳实践

实践表明,Xml配置文件相对简单易用,只需要写个配置类,如果配置文件不存在,系统能够自动创建一个。例如,核心库配置类 NewLife.Setting.Current.LogLevel 即可访问配置项,自动创建 Config/Core.config 配置文件。

    /// <summary>核心设置</summary>
    [DisplayName("核心设置")]
    [Config("Core")]
    public class Setting : Config<Setting>
    {
        #region 属性
        /// <summary>是否启用全局调试。默认启用</summary>
        [Description("全局调试。XTrace.Debug")]
        public Boolean Debug { get; set; } = true;

        /// <summary>日志等级,只输出大于等于该级别的日志,All/Debug/Info/Warn/Error/Fatal,默认Info</summary>
        [Description("日志等级。只输出大于等于该级别的日志,All/Debug/Info/Warn/Error/Fatal,默认Info")]
        public LogLevel LogLevel { get; set; } = LogLevel.Info;

        /// <summary>文件日志目录。默认Log子目录</summary>
        [Description("文件日志目录。默认Log子目录")]
        public String LogPath { get; set; } = "";

        /// <summary>日志文件上限。超过上限后拆分新日志文件,默认10MB,0表示不限制大小</summary>
        [Description("日志文件上限。超过上限后拆分新日志文件,默认10MB,0表示不限制大小")]
        public Int32 LogFileMaxBytes { get; set; } = 10;

        /// <summary>日志文件备份。超过备份数后,最旧的文件将被删除,默认100,0表示不限制个数</summary>
        [Description("日志文件备份。超过备份数后,最旧的文件将被删除,默认100,0表示不限制个数")]
        public Int32 LogFileBackups { get; set; } = 100;

        /// <summary>日志文件格式。默认{0:yyyy_MM_dd}.log,支持日志等级如 {1}_{0:yyyy_MM_dd}.log</summary>
        [Description("日志文件格式。默认{0:yyyy_MM_dd}.log,支持日志等级如 {1}_{0:yyyy_MM_dd}.log")]
        public String LogFileFormat { get; set; } = "{0:yyyy_MM_dd}.log";

        /// <summary>网络日志。本地子网日志广播udp://255.255.255.255:514,或者http://xxx:80/log</summary>
        [Description("网络日志。本地子网日志广播udp://255.255.255.255:514,或者http://xxx:80/log")]
        public String NetworkLog { get; set; } = "";

        /// <summary>数据目录。本地数据库目录,默认Data子目录</summary>
        [Description("数据目录。本地数据库目录,默认Data子目录")]
        public String DataPath { get; set; } = "";

        /// <summary>备份目录。备份数据库时存放的目录,默认Backup子目录</summary>
        [Description("备份目录。备份数据库时存放的目录,默认Backup子目录")]
        public String BackupPath { get; set; } = "";

        /// <summary>插件目录</summary>
        [Description("插件目录")]
        public String PluginPath { get; set; } = "Plugins";

        /// <summary>插件服务器。将从该网页上根据关键字分析链接并下载插件</summary>
        [Description("插件服务器。将从该网页上根据关键字分析链接并下载插件")]
        public String PluginServer { get; set; } = "http://x.newlifex.com/";
        #endregion

        #region 方法
        /// <summary>加载完成后</summary>
        protected override void OnLoaded()
        {
            if (LogPath.IsNullOrEmpty()) LogPath = "Log";
            if (DataPath.IsNullOrEmpty()) DataPath = "Data";
            if (BackupPath.IsNullOrEmpty()) BackupPath = "Backup";
            if (LogFileFormat.IsNullOrEmpty()) LogFileFormat = "{0:yyyy_MM_dd}.log";

            if (PluginServer.IsNullOrWhiteSpace()) PluginServer = "http://x.newlifex.com/";

            base.OnLoaded();
        }

        /// <summary>获取插件目录</summary>
        /// <returns></returns>
        public String GetPluginPath() => PluginPath.GetBasePath();
        #endregion
    }


配置核心IConfigProvider

整个配置系统的核心是IConfigProvider接口,内置有 JsonConfigProvider/XmlConfigProvider/IniConfigProvider/HttpConfigProvider等多个配置实现。

全貌如下:

/// <summary>配置提供者</summary>
/// <remarks>
/// 建立树状配置数据体系,以分布式配置中心为核心,支持基于key的索引读写,也支持Load/Save/Bind的实体模型转换。
/// key索引支持冒号分隔的多层结构,在配置中心中不同命名空间使用不同提供者实例,在文件配置中不同文件使用不同提供者实例。
/// 
/// 一个配置类,支持从不同持久化提供者读取,可根据需要选择配置持久化策略。
/// 例如,小系统采用ini/xml/json文件配置,分布式系统采用配置中心。
/// 
/// 可通过实现IConfigMapping接口来自定义映射配置到模型实例。
/// </remarks>
public interface IConfigProvider
{
    /// <summary>名称</summary>
    String Name { get; set; }

    /// <summary>所有键</summary>
    ICollection<String> Keys { get; }

    /// <summary>获取 或 设置 配置值</summary>
    /// <param name="key">配置名,支持冒号分隔的多级名称</param>
    /// <returns></returns>
    String this[String key] { get; set; }

    /// <summary>查找配置项。可得到子级和配置</summary>
    /// <param name="key">配置名</param>
    /// <returns></returns>
    IConfigSection GetSection(String key);

    /// <summary>返回获取配置的委托</summary>
    GetConfigCallback GetConfig { get; }

    /// <summary>加载配置到模型</summary>
    /// <typeparam name="T">模型。可通过实现IConfigMapping接口来自定义映射配置到模型实例</typeparam>
    /// <param name="path">路径。配置树位置,配置中心等多对象混合使用时</param>
    /// <returns></returns>
    T Load<T>(String path = null) where T : new();

    /// <summary>保存模型实例</summary>
    /// <typeparam name="T">模型</typeparam>
    /// <param name="model">模型实例</param>
    /// <param name="path">路径。配置树位置,配置中心等多对象混合使用时</param>
    Boolean Save<T>(T model, String path = null);

    /// <summary>绑定模型,使能热更新,配置存储数据改变时同步修改模型属性</summary>
    /// <typeparam name="T">模型。可通过实现IConfigMapping接口来自定义映射配置到模型实例</typeparam>
    /// <param name="model">模型实例</param>
    /// <param name="autoReload">是否自动更新。默认true</param>
    /// <param name="path">命名空间。配置树位置,配置中心等多对象混合使用时</param>
    void Bind<T>(T model, Boolean autoReload = true, String path = null);

    /// <summary>绑定模型,使能热更新,配置存储数据改变时同步修改模型属性</summary>
    /// <typeparam name="T">模型。可通过实现IConfigMapping接口来自定义映射配置到模型实例</typeparam>
    /// <param name="model">模型实例</param>
    /// <param name="path">命名空间。配置树位置,配置中心等多对象混合使用时</param>
    /// <param name="onChange">配置改变时执行的委托</param>
    void Bind<T>(T model, String path, Action<IConfigSection> onChange);
}


IConfigProvider最常见用法是根据索引器访问数据,例如provider["Title"]。

高级用法是Load和Bind,两者都是把配置数据填充到目标对象的属性上。Bind绑定多了自动刷新,在配置数据改变时,能够及时更新数据到配置对象的属性上。


Json配置

以Json格式存储配置数据,不支持Json注释,支持无限层嵌套。

配置内容格式如下:

{
    "#Debug": "全局调试。XTrace.Debug",
    "Debug": "true",
    "#LogLevel": "日志等级。只输出大于等于该级别的日志,All/Debug/Info/Warn/Error/Fatal,默认Info",
    "LogLevel": "Fatal",
    "#LogPath": "文件日志目录",
    "LogPath": "xxx",
    "#NetworkLog": "网络日志。本地子网日志广播255.255.255.255:514",
    "NetworkLog": "255.255.255.255:514",
    "#LogFileFormat": "日志文件格式。默认{0:yyyy_MM_dd}.log",
    "LogFileFormat": "{0:yyyy_MM_dd}.log",
    "#TempPath": "临时目录",
    "TempPath": "yyy",
    "#PluginPath": "插件目录",
    "PluginPath": "Plugins",
    "#PluginServer": "插件服务器。将从该网页上根据关键字分析链接并下载插件",
    "PluginServer": "http://x.newlifex.com/",
    "#Sys": "系统配置",
    "Sys": { "#Name": "用于标识系统的英文名", "Name": "NewLife.Cube", "#Version": "系统版本", "Version": "", "#DisplayName": "用户可见的名称", "DisplayName": "魔方平台", "#Company": "公司", "Company": "新生命开发团队", "#Instance": "应用实例。单应用多实例部署时用于唯一标识实例节点", "Instance": "0", "#Develop": "开发者模式", "Develop": "true", "#Enable": "启用", "Enable": "true", "#InstallTime": "安装时间", "InstallTime": "2021-04-18 18:06:12" }
}

常见用法如下:

var _provider = new JsonConfigProvider { FileName = "Config/core1.json" };
var set = new ConfigModel
{
    Debug = true,
    LogLevel = LogLevel.Fatal,
    LogPath = "xxx",
    NetworkLog = "255.255.255.255:514",
    TempPath = "yyy",

    Sys = new SysConfig
    {
        Name = "NewLife.Cube",
        DisplayName = "魔方平台",
        Company = "新生命开发团队",
    },
};

_provider.Save(set);

var prv = _provider;
Assert.NotNull(prv);
Assert.Equal(set.Debug.ToString().ToLower(), prv["Debug"]);
Assert.Equal(set.LogLevel + "", prv["LogLevel"]);
Assert.Equal(set.LogPath, prv["LogPath"]);
Assert.Equal(set.NetworkLog, prv["NetworkLog"]);
Assert.Equal(set.LogFileFormat, prv["LogFileFormat"]);
Assert.Equal(set.TempPath, prv["TempPath"]);
Assert.Equal(set.PluginPath, prv["PluginPath"]);
Assert.Equal(set.PluginServer, prv["PluginServer"]);

Assert.Equal("全局调试。XTrace.Debug", prv.GetSection("Debug").Comment);
Assert.Equal("系统配置", prv.GetSection("Sys").Comment);
Assert.Equal("用于标识系统的英文名", prv.GetSection("Sys:Name").Comment);

var sys = set.Sys;
Assert.Equal(sys.Name, prv["Sys:Name"]);
Assert.Equal(sys.DisplayName, prv["Sys:DisplayName"]);
Assert.Equal(sys.Company, prv["Sys:Company"]);

var prv2 = new JsonConfigProvider { FileName = (_provider as FileConfigProvider).FileName };
var set2 = prv2.Load<ConfigModel>();

Assert.NotNull(set2);
Assert.Equal(set.Debug, set2.Debug);
Assert.Equal(set.LogLevel, set2.LogLevel);
Assert.Equal(set.LogPath, set2.LogPath);
Assert.Equal(set.NetworkLog, set2.NetworkLog);
Assert.Equal(set.LogFileFormat, set2.LogFileFormat);
Assert.Equal(set.TempPath, set2.TempPath);
Assert.Equal(set.PluginPath, set2.PluginPath);
Assert.Equal(set.PluginServer, set2.PluginServer);

Assert.Equal("全局调试。XTrace.Debug", prv2.GetSection("Debug").Comment);
Assert.Equal("系统配置", prv2.GetSection("Sys").Comment);
Assert.Equal("用于标识系统的英文名", prv2.GetSection("Sys:Name").Comment);

var sys2 = set2.Sys;
Assert.NotNull(sys2);
Assert.Equal(sys.Name, sys2.Name);
Assert.Equal(sys.DisplayName, sys2.DisplayName);
Assert.Equal(sys.Company, sys2.Company);

Xml配置

以Xml格式存储配置数据,支持注释,支持无限层嵌套。

配置内容格式如下:

<?xml version="1.0" encoding="utf-8"?>
<Root>
  <!--全局调试。XTrace.Debug-->
  <Debug>true</Debug>
  <!--日志等级。只输出大于等于该级别的日志,All/Debug/Info/Warn/Error/Fatal,默认Info-->
  <LogLevel>Fatal</LogLevel>
  <!--文件日志目录-->
  <LogPath>xxx</LogPath>
  <!--网络日志。本地子网日志广播255.255.255.255:514-->
  <NetworkLog>255.255.255.255:514</NetworkLog>
  <!--日志文件格式。默认{0:yyyy_MM_dd}.log-->
  <LogFileFormat>{0:yyyy_MM_dd}.log</LogFileFormat>
  <!--临时目录-->
  <TempPath>yyy</TempPath>
  <!--插件目录-->
  <PluginPath>Plugins</PluginPath>
  <!--插件服务器。将从该网页上根据关键字分析链接并下载插件-->
  <PluginServer>http://x.newlifex.com/</PluginServer>
  <!--系统配置-->
  <Sys>
    <!--用于标识系统的英文名-->
    <Name>NewLife.Cube</Name>
    <!--系统版本-->
    <Version></Version>
    <!--用户可见的名称-->
    <DisplayName>魔方平台</DisplayName>
    <!--公司-->
    <Company>新生命开发团队</Company>
    <!--应用实例。单应用多实例部署时用于唯一标识实例节点-->
    <Instance>0</Instance>
    <!--开发者模式-->
    <Develop>true</Develop>
    <!--启用-->
    <Enable>true</Enable>
    <!--安装时间-->
    <InstallTime>2021-04-18 18:06:12</InstallTime>
  </Sys>
</Root>

常见用法如下:

var _provider = new XmlConfigProvider { FileName = "Config/core1.xml" };
var set = new ConfigModel
{
    Debug = true,
    LogLevel = LogLevel.Fatal,
    LogPath = "xxx",
    NetworkLog = "255.255.255.255:514",
    TempPath = "yyy",

    Sys = new SysConfig
    {
        Name = "NewLife.Cube",
        DisplayName = "魔方平台",
        Company = "新生命开发团队",
    },
};

_provider.Save(set);

var prv = _provider;
Assert.NotNull(prv);
Assert.Equal(set.Debug.ToString().ToLower(), prv["Debug"]);
Assert.Equal(set.LogLevel + "", prv["LogLevel"]);
Assert.Equal(set.LogPath, prv["LogPath"]);
Assert.Equal(set.NetworkLog, prv["NetworkLog"]);
Assert.Equal(set.LogFileFormat, prv["LogFileFormat"]);
Assert.Equal(set.TempPath, prv["TempPath"]);
Assert.Equal(set.PluginPath, prv["PluginPath"]);
Assert.Equal(set.PluginServer, prv["PluginServer"]);

Assert.Equal("全局调试。XTrace.Debug", prv.GetSection("Debug").Comment);
Assert.Equal("系统配置", prv.GetSection("Sys").Comment);
Assert.Equal("用于标识系统的英文名", prv.GetSection("Sys:Name").Comment);

var sys = set.Sys;
Assert.Equal(sys.Name, prv["Sys:Name"]);
Assert.Equal(sys.DisplayName, prv["Sys:DisplayName"]);
Assert.Equal(sys.Company, prv["Sys:Company"]);

var prv2 = new XmlConfigProvider { FileName = (_provider as FileConfigProvider).FileName };
var set2 = prv2.Load<ConfigModel>();

Assert.NotNull(set2);
Assert.Equal(set.Debug, set2.Debug);
Assert.Equal(set.LogLevel, set2.LogLevel);
Assert.Equal(set.LogPath, set2.LogPath);
Assert.Equal(set.NetworkLog, set2.NetworkLog);
Assert.Equal(set.LogFileFormat, set2.LogFileFormat);
Assert.Equal(set.TempPath, set2.TempPath);
Assert.Equal(set.PluginPath, set2.PluginPath);
Assert.Equal(set.PluginServer, set2.PluginServer);

Assert.Equal("全局调试。XTrace.Debug", prv2.GetSection("Debug").Comment);
Assert.Equal("系统配置", prv2.GetSection("Sys").Comment);
Assert.Equal("用于标识系统的英文名", prv2.GetSection("Sys:Name").Comment);

var sys2 = set2.Sys;
Assert.NotNull(sys2);
Assert.Equal(sys.Name, sys2.Name);
Assert.Equal(sys.DisplayName, sys2.DisplayName);
Assert.Equal(sys.Company, sys2.Company);

Ini配置

以Ini格式存储配置数据,支持注释,支持两层结构。

配置内容格式如下:

; 全局调试。XTrace.Debug
Debug = true
; 日志等级。只输出大于等于该级别的日志,All/Debug/Info/Warn/Error/Fatal,默认Info
LogLevel = Fatal
; 文件日志目录
LogPath = xxx
; 网络日志。本地子网日志广播255.255.255.255:514
NetworkLog = 255.255.255.255:514
; 日志文件格式。默认{0:yyyy_MM_dd}.log
LogFileFormat = {0:yyyy_MM_dd}.log
; 临时目录
TempPath = yyy
; 插件目录
PluginPath = Plugins
; 插件服务器。将从该网页上根据关键字分析链接并下载插件
PluginServer = http://x.newlifex.com/

; 系统配置
[Sys]
; 用于标识系统的英文名
Name = NewLife.Cube
; 系统版本
Version = 
; 用户可见的名称
DisplayName = 魔方平台
; 公司
Company = 新生命开发团队
; 应用实例。单应用多实例部署时用于唯一标识实例节点
Instance = 0
; 开发者模式
Develop = true
; 启用
Enable = true
; 安装时间
InstallTime = 2021-04-18 18:06:12

常见用法如下:

var _provider = new InIConfigProvider { FileName = "Config/core1.ini" };
var set = new ConfigModel
{
    Debug = true,
    LogLevel = LogLevel.Fatal,
    LogPath = "xxx",
    NetworkLog = "255.255.255.255:514",
    TempPath = "yyy",

    Sys = new SysConfig
    {
        Name = "NewLife.Cube",
        DisplayName = "魔方平台",
        Company = "新生命开发团队",
    },
};

_provider.Save(set);

var prv = _provider;
Assert.NotNull(prv);
Assert.Equal(set.Debug.ToString().ToLower(), prv["Debug"]);
Assert.Equal(set.LogLevel + "", prv["LogLevel"]);
Assert.Equal(set.LogPath, prv["LogPath"]);
Assert.Equal(set.NetworkLog, prv["NetworkLog"]);
Assert.Equal(set.LogFileFormat, prv["LogFileFormat"]);
Assert.Equal(set.TempPath, prv["TempPath"]);
Assert.Equal(set.PluginPath, prv["PluginPath"]);
Assert.Equal(set.PluginServer, prv["PluginServer"]);

Assert.Equal("全局调试。XTrace.Debug", prv.GetSection("Debug").Comment);
Assert.Equal("系统配置", prv.GetSection("Sys").Comment);
Assert.Equal("用于标识系统的英文名", prv.GetSection("Sys:Name").Comment);

var sys = set.Sys;
Assert.Equal(sys.Name, prv["Sys:Name"]);
Assert.Equal(sys.DisplayName, prv["Sys:DisplayName"]);
Assert.Equal(sys.Company, prv["Sys:Company"]);

var prv2 = new InIConfigProvider { FileName = (_provider as FileConfigProvider).FileName };
var set2 = prv2.Load<ConfigModel>();

Assert.NotNull(set2);
Assert.Equal(set.Debug, set2.Debug);
Assert.Equal(set.LogLevel, set2.LogLevel);
Assert.Equal(set.LogPath, set2.LogPath);
Assert.Equal(set.NetworkLog, set2.NetworkLog);
Assert.Equal(set.LogFileFormat, set2.LogFileFormat);
Assert.Equal(set.TempPath, set2.TempPath);
Assert.Equal(set.PluginPath, set2.PluginPath);
Assert.Equal(set.PluginServer, set2.PluginServer);

Assert.Equal("全局调试。XTrace.Debug", prv2.GetSection("Debug").Comment);
Assert.Equal("系统配置", prv2.GetSection("Sys").Comment);
Assert.Equal("用于标识系统的英文名", prv2.GetSection("Sys:Name").Comment);

var sys2 = set2.Sys;
Assert.NotNull(sys2);
Assert.Equal(sys.Name, sys2.Name);
Assert.Equal(sys.DisplayName, sys2.DisplayName);
Assert.Equal(sys.Company, sys2.Company);

Http配置(配置中心)

通过调用Http接口拉取配置信息。

集成星尘配置中心:

[Fact]
public void TestStardust()
{
    var provider = new HttpConfigProvider
    {
        Server = "http://star.newlifex.com:6600",
        //Server = "http://localhost:6600",
        AppId = "StarWeb"
    };

    var str = provider["test1"];
    Assert.NotEmpty(str);

    var keys = provider.Keys.ToArray();
    Assert.NotNull(keys);

    var model = provider.Load<Model2>();
    Assert.NotNull(model);
    Assert.NotEmpty(model.Test);
    Assert.Equal(str, model.Test);
    Assert.NotEmpty(model.Shop);
    Assert.NotEmpty(model.Title);
    Assert.Equal("NewLife开发团队", model.Title);

    var model2 = new Model2();
    provider.Bind(model2);
    Assert.Equal(str, model2.Test);
    Assert.NotEmpty(model.Shop);
    Assert.Equal("NewLife开发团队", model.Title);

    provider.LoadAll();
}

private class Model2
{
    [DataMember(Name = "test1")]
    public String Test { get; set; }

    [DataMember(Name = "conn_Shop")]
    public String Shop { get; set; }

    public String Title { get; set; }
}

Apollo阿波罗配置

集成阿波罗配置中心

public void TestApollo()
{
    var provider = new ApolloConfigProvider
    {
        Server = _server,
        AppId = "testapi"
    };
    provider.SetApollo("application");
    //provider.LoadAll();

    var url = provider["appapiurl"];
    Assert.NotEmpty(url);

    var keys = provider.Keys.ToArray();
    Assert.NotNull(keys);

    var model = provider.Load<Model>();
    Assert.NotNull(model);
    Assert.NotEmpty(model.AppApiUrl);
    Assert.Equal(url, model.AppApiUrl);
    Assert.True(model.Radius > 0);
    Assert.NotEmpty(model.MySqlServer);

    var model2 = new Model();
    provider.Bind(model2);
    Assert.Equal(url, model2.AppApiUrl);
    Assert.True(model2.Radius > 0);
    Assert.NotEmpty(model2.MySqlServer);
}

private class Model
{
    public Int32 Radius { get; set; }

    public String MySqlServer { get; set; }

    public String AppApiUrl { get; set; }
}