NetCore 入门 (三) : 配置系统
1. QuickStart
配置系统(Configuration)具有如下特点:
- 提供统一的方式读取配置数据
- 支持多样化的数据源
- 支持配置数据的热更新
1.1 Nuget包
Microsoft.Extensions.Configuration.Abstrations; // 系统接口和基础类型定义
Microsoft.Extensions.Configuration; // 默认实现
Microsoft.Extensions.Configuration.Binder; // 配置绑定包
Microsoft.Extensions.Configuration.EnvironmentVariables; // 环境变量的实现包
Microsoft.Extensions.Configuration.CommandLine; // 命令行的实现包
Microsoft.Extensions.Configuration.Json; // Json文件的实现包
Microsoft.Extensions.Configuration.Xml; // Xml文件的实现包
Microsoft.Extensions.Configuration.Ini; // Ini文件的实现包
1.2 示例1 - 读取键值对配置
var source = new Dictionary<string, string>
{
["age"] = "18",
["gender"] = "男",
["contactInfo:email"] = "foo@126.com",
["contactInfo:phone"] = "1234567890"
};
IConfiguration config = new ConfigurationBuilder()
.AddInMemoryCollection(source)
.Build();
Console.WriteLine("age:" + config["age"]);
Console.WriteLine("gender:" + config["gender"]);
Console.WriteLine("email:" + config["contactInfo:email"]);
Console.WriteLine("phone:" + config["contactInfo:phone"]);
// or
IConfigurationSection section = config.GetSection("contactInfo");
Console.WriteLine("email:" + section["email"]);
Console.WriteLine("phone:" + section["phone"]);
结果输出
age:18
gender:男
email:foo@126.com
phone:1234567890
1.3 示例2 - 读取Json文件
项目根目录下有json文件appsettinss.json
:
{
"gender": "男",
"age": 18,
"contactInfo": {
"email": "foo@126.com",
"phone": "1234567890"
}
}
IConfiguration config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
Console.WriteLine("age:" + config["age"]);
Console.WriteLine("gender:" + config["gender"]);
IConfigurationSection section = config.GetSection("contactInfo");
Console.WriteLine("email:" + section["email"]);
Console.WriteLine("phone:" + section["phone"]);
输出结果同上。
1.4 示例3 - 配置绑定
将配置信息直接绑定为POCO对象。
现定义如下配置对象:
public class Profile
{
public string Gender { get; set; }
public int Age { get; set; }
public Contact ContactInfo { get; set; }
}
public class Contact
{
public string Email { get; set; }
public string Phone { get; set; }
}
public void Run()
{
IConfiguration config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
Profile profile = config.Get<Profile>();
Console.WriteLine("age:" + profile.Age);
Console.WriteLine("gender:" + profile.Gender);
Console.WriteLine("email:" + profile.ContactInfo.Email);
Console.WriteLine("phone:" + profile.ContactInfo.Phone);
}
1.5 示例4 - 配置热更新
在配置文件更新后,应用程序接受到变更通知,自动进行配置同步。
IConfiguration config = new ConfigurationBuilder()
.AddJsonFile(
path: "appsettings.json",
optional: true,
reloadOnChange: true)
.Build();
ChangeToken.OnChange(() => config.GetReloadToken(), () =>
{
Console.WriteLine("===========Configuration Changed===============");
Console.WriteLine("age:" + config["age"]);
Console.WriteLine("gender:" + config["gender"]);
IConfigurationSection section = config.GetSection("contactInfo");
Console.WriteLine("email:" + section["email"]);
Console.WriteLine("phone:" + section["phone"]);
});
通过reloadOnChange
参数开启配置热更新功能。在程序启动后,修改bin\Debug\netcoreapp3.1\appsettings.json
文件,将age
变成20。
结果输出
===========Configuration Changed===============
age:20
gender:男
email:foo@126.com
phone:1234567890
2. 模型解析
2.1 核心接口
2.1.1 IConfigurationSource
配置系统支持多种数据源,可以是内存对象、物理文件、数据库或者其他的存储介质。IConfigurationSource
接口就是多样数据源的体现,每支持一种数据源,就需要定义一种IConfigurationSource
实现类型。 但原始数据的读取,要交给与之对应的IConfigurationProvider
接口来实现。
public interface IConfigurationSource
{
IConfigurationProvider Build(IConfigurationBuilder builder);
}
2.1.2 IConfigurationProvider
IConfigurationProvider
的职责:
- Load方法 : 从数据源中加载数据。
- GetReloadToken : 在数据源变更时,及时向外发送通知。用于配置热更新。
- Set 和 TryGet : 读取或更新配置。
public interface IConfigurationProvider
{
IChangeToken GetReloadToken();
// 加载数据
void Load();
void Set(string key, string value);
bool TryGet(string key, out string value);
}
从方法Set
和TryGet
可以推测出:IConfigurationProvider
采用的存储结构为配置字典。
2.1.3 IConfigurationBuilder
IConfigurationBuilder
的职责:
- Add方法: 注册
IConfigurationSource
数据源。 - Build方法: 构建
IConfiguration
对象。
public interface IConfigurationBuilder
{
IList<IConfigurationSource> Sources { get; }
IConfigurationBuilder Add(IConfigurationSource source);
IConfigurationRoot Build();
}
2.2 数据存储
我们来看看对于具有结构化的数据,配置系统是如何存储的。
现有json数据:
{
"gender": "男",
"age": 18,
"contactInfo": {
"email": "foo@126.com",
"phone": "1234567890"
}
}
加载完成后的存储方式如下:
Key | Value |
---|---|
gender | 男 |
age | 18 |
contactInfo:email | foo@126.com |
contactInfo:phone | 1234567890 |
::: tip
对于具有结构化的配置数据,以 键值对 的形式进行存储,他们的Key是以冒号(:)为分隔符的路径。
:::
ConfigurationProvider
我们来看看IConfigurationProvider
接口的默认实现。
public abstract class ConfigurationProvider : IConfigurationProvider
{
private ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken();
protected IDictionary<string, string> Data { get; set; } // 存储数据的配置字典
protected ConfigurationProvider()
{
this.Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
public virtual bool TryGet(string key, out string value)
{
return this.Data.TryGetValue(key, out value);
}
public virtual void Set(string key, string value)
{
this.Data[key] = value;
}
public virtual void Load()
{
}
public IChangeToken GetReloadToken()
{
return this._reloadToken;
}
protected void OnReload()
{
Interlocked.Exchange<ConfigurationReloadToken>(ref this._reloadToken,
new ConfigurationReloadToken()).OnReload();
}
...
}
Data
: 存储数据的配置字典TryGet
和Set
: 针对配置字典的操作GetReloadToken
: 获取IChangeToken
OnReload
: 通知外部:配置数据已更新Load
: 加载原始数据,交给具体的IConfigurationProvider
去实现。
定义新的配置源时,只需继承ConfigurationProvider
,实现方法Load
方法即可。
2.3 ICofiguration
配置系统最终是以一个IConfiguration
对象的形式供我们使用的。IConfiguration
接口定义了一些查询配置数据的方法:
public interface IConfiguration
{
string this[string key] { get; set; }
IEnumerable<IConfigurationSection> GetChildren();
IChangeToken GetReloadToken();
IConfigurationSection GetSection(string key);
}
this[string key]
:以键值对的形式进行查询。GetReloadToken
: 用于配置热更新。GetChildren和GetSection
: 提供层次化的树形结构查询方式。
示例:
Console.WriteLine("age:" + config["age"]);
Console.WriteLine("gender:" + config["gender"]);
Console.WriteLine("email:" + config["contactInfo:email"]);
Console.WriteLine("phone:" + config["contactInfo:phone"]);
IConfigurationSection section = config.GetSection("contactInfo");
Console.WriteLine("email:" + section["email"]);
Console.WriteLine("phone:" + section["phone"]);
虽然配置系统以扁平化的配置字典作为存储结构,但是IConfiguration
的API在逻辑上体现为树形结构。
2.3.1 IConfigurationRoot
IConfigurationRoot
代表配置树的根节点。一棵配置树只有一个根节点。
public interface IConfigurationRoot : IConfiguration
{
IEnumerable<IConfigurationProvider> Providers { get; }
void Reload();
}
Providers
: 注册到配置树中的所有数据源。Reload
: 用于实现配置数据的重新加载。作为配置树的根,也就代表了整颗配置树。如果根节点被重新加载,则意味着所有数据源的数据都会被重新加载。
2.3.2 IConfigurationSection
IConfigurationSection
表示非根配置节点。
public interface IConfigurationSection : IConfiguration
{
string Key { get; }
string Path { get; }
string Value { get; set; }
}
Key
: 代表当前节点的key
.Path
: 代表当前节点在配置树中的路径。Value
: 代表当前节点的值。只有叶子节点才有具体的值,如果没有具体值的话返回null
。
section示例:
IConfiguration config = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
string[] nodeKey = new string[] { "gender", "age", "contactInfo", "contactInfo:email", "contactInfo:phone" };
foreach (var key in nodeKey)
{
var section = config.GetSection(key);
Console.WriteLine($"section[{key}] {{Path={section.Path}, Key={section.Key}, Value={section.Value}}}");
}
输出结果
section[gender] {Path=gender, Key=gender, Value=男}
section[age] {Path=age, Key=age, Value=18}
section[contactInfo] {Path=contactInfo, Key=contactInfo, Value=}
section[contactInfo:email] {Path=contactInfo:email, Key=email, Value=foo@126.com}
section[contactInfo:phone] {Path=contactInfo:phone, Key=phone, Value=1234567890}
2.4 数据读取
我们来看看当IConfiguration
对象读取数据时,系统内部发生了什么。
public class ConfigurationRoot : IConfigurationRoot, IConfiguration
{
private readonly IList<IConfigurationProvider> _providers;
public string this[string key]
{
get
{
for (int i = this._providers.Count - 1; i >= 0; i--)
{
string result;
if (this._providers[i].TryGet(key, out result))
{
return result;
}
}
return null;
}
}
}
ConfigurationRoot
对象保持着对所有IConfigurationProvider
对象的引用。当读取配置数据时,最终都会从这些IConfigurationProvider
对象中提取。也就是说,配置数据在整个模型中只存储在IConfigurationProvider
对象中。
从代码高亮行可以看出,后注册的数据源将被优先查询。如果查询到匹配结果,则不再查询其他数据源。配置系统采用“后来居上”的原则管理数据源,如果多个配置源中有相同的配置项,则后注册的配置项将覆盖之前注册的数据项。所以,在注册数据源时,要注意数据源的优先级。
:::tip 总结
1、配置数据在整个模型中只存储在IConfigurationProvider
对象中。
2、如果多个配置源中有相同的配置项,则后注册的配置项将覆盖之前注册的数据项。
:::
3. 配置绑定
配置系统提供了将IConfiguration
转换成POCO对象的方法,方便我们以面向对象的方式来使用配置。我们把这样的转换过程称之为配置绑定。
IConfiguration
扩展了如下方法:
public static class ConfigurationBinder
{
public static void Bind(this IConfiguration configuration, string key, object instance);
public static void Bind(this IConfiguration configuration, object instance);
public static void Bind(this IConfiguration configuration, object instance, Action<BinderOptions> configureOptions);
public static T Get<T>(this IConfiguration configuration);
public static T Get<T>(this IConfiguration configuration, Action<BinderOptions> configureOptions);
public static object Get(this IConfiguration configuration, Type type);
public static object Get(this IConfiguration configuration, Type type, Action<BinderOptions> configureOptions);
public static T GetValue<T>(this IConfiguration configuration, string key);
public static T GetValue<T>(this IConfiguration configuration, string key, T defaultValue);
public static object GetValue(this IConfiguration configuration, Type type, string key);
public static object GetValue(this IConfiguration configuration, Type type, string key, object defaultValue);
}
public class BinderOptions
{
public bool BindNonPublicProperties { get; set; }
}
3.1 绑定基本类型
对于配置绑定来说,最简单的是针对 叶子节点 的IConfigurationSection
对象的绑定。而叶子节点的值是一个字符串,所以针对它的配置绑定就转换为如何将这个字符串转换成指定的目标类型。
::: tip 转换规则
配置系统将按照如下规则进行目标类型的转换:
- 如果目标类型是
object
,则直接返回原始值(null
或 字符串)。 - 如果目标类型是
Nullable<T>
,那么在原始值是null
或空字符串时直接返回null
,否则目标类型的TypeConverter
被用来做类型转换。 - 如果目标类型不是
Nullable<T>
,则目标类型的TypeConverter
被用来做类型转换。
:::
3.1.1 基元类型
var source = new Dictionary<string, string>
{
["foo"] = null,
["bar"] = "",
["baz"] = "123"
};
var config = new ConfigurationBuilder()
.AddInMemoryCollection(source)
.Build();
// object
Debug.Assert(config.GetValue<object>("foo") == null);
Debug.Assert("".Equals(config.GetValue<object>("bar")));
Debug.Assert("123".Equals(config.GetValue<object>("baz")));
// 普通类型
Debug.Assert(config.GetValue<int>("foo") == 0);
Debug.Assert(config.GetValue<int>("baz") == 123);
// nullable
Debug.Assert(config.GetValue<int?>("foo") == null);
Debug.Assert(config.GetValue<int?>("bar") == null);
3.1.2 自定义类型
下面演示通过指定TypeConverter
的方式,将字符串转换成目标类型。
目标类型和TypeConverter
定义:
[TypeConverter(typeof(PointTypeConverter))]
public class Point
{
public double X { get; set; }
public double Y { get; set; }
}
public class PointTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
string[] numbers = value.ToString()
.Replace("(", "").Replace(")", "")
.Split(",").Select(item => item.Trim())
.ToArray();
if (numbers.Length != 2)
{
return null;
}
return new Point
{
X = double.Parse(numbers[0]),
Y = double.Parse(numbers[1])
};
}
}
转换验证:
var source = new Dictionary<string, string>
{
["point"] = "(123,456)"
};
var config = new ConfigurationBuilder()
.AddInMemoryCollection(source)
.Build();
var point = config.GetValue<Point>("point");
Debug.Assert(point.X == 123);
Debug.Assert(point.Y == 456);
3.2 绑定复合类型
这里的复合类型可以是包含属性成员的POCO对象,可以是集合,也可以是字典。
项目根目录下有数据文件bindComplex.json
:
{
"foo": {
"gender": "男",
"age": 18,
"contactInfo": {
"email": "foo@126.com",
"phone": "1234567890"
}
},
"bar": {
"gender": "男",
"age": 19,
"contactInfo": {
"email": "foo@126.com",
"phone": "1234567890"
}
},
"baz": {
"gender": "男",
"age": 20,
"contactInfo": {
"email": "foo@126.com",
"phone": "1234567890"
}
}
}
类型定义
public class Profile
{
public string Gender { get; set; }
public int Age { get; set; }
public Contact ContactInfo { get; set; }
}
public class Contact
{
public string Email { get; set; }
public string Phone { get; set; }
}
转换演示
var config = new ConfigurationBuilder()
.AddJsonFile("bindComplex.json")
.Build();
// POCO
var profile = config.GetSection("foo").Get<Profile>();
Debug.Assert(profile.Age == 18);
// 集合对象
var profiles = config.Get<Profile[]>();
Debug.Assert(profiles.Any(item => item.Age == 18));
Debug.Assert(profiles.Any(item => item.Age == 19));
Debug.Assert(profiles.Any(item => item.Age == 20));
// 字典
var dict = config.Get<Dictionary<string, Profile>>();
Debug.Assert(dict.ContainsKey("foo") && dict["foo"].Age == 18);
Debug.Assert(dict.ContainsKey("bar") && dict["bar"].Age == 19);
Debug.Assert(dict.ContainsKey("baz") && dict["baz"].Age == 20);
4. 配置热更新
配置热更新涉及两个方面:
- 第一,对原始的配置源实施监控并在其发生变化之后重新加载配置;
- 第二,配置重新加载之后及时通知应用程序,进而使应用能够及时使用最新的配置。
4.1 监控配置源
监控配置源的功能需要有具体的IConfigurationProvider
对象来实现。目前仅文件类配置源实现了此功能,具体参考物理文件配置源。
4.2 重加载通知
4.2.1 ConfigurationReloadToken
利用ConfigurationReloadToken
对象,通知应用程序:配置源已发生改变,并且已经被IConfigurationProvider
对象重新加载进来。IChangeToken
接口的介绍请参考ChangeToken。
public class ConfigurationReloadToken : IChangeToken
{
public bool ActiveChangeCallbacks { get; }
public bool HasChanged { get; }
public void OnReload();
public IDisposable RegisterChangeCallback(Action<object> callback, object state);
}
OnReload
: 手动触发重加载通知。
4.2.2 ConfigurationRoot
public class ConfigurationRoot : IConfigurationRoot, IConfiguration
{
private readonly IList<IConfigurationProvider> _providers;
private readonly IList<IDisposable> _changeTokenRegistrations;
private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();
public ConfigurationRoot(IList<IConfigurationProvider> providers)
{
if (providers == null)
{
throw new ArgumentNullException("providers");
}
this._providers = providers;
this._changeTokenRegistrations = new List<IDisposable>(providers.Count);
using (IEnumerator<IConfigurationProvider> enumerator = providers.GetEnumerator())
{
while (enumerator.MoveNext())
{
IConfigurationProvider p = enumerator.Current;
p.Load();
// 监控 IConfigurationProvider 的变化
this._changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), delegate()
{
this.RaiseChanged();
}));
}
}
}
public IChangeToken GetReloadToken()
{
return this._changeToken;
}
public void Reload()
{
foreach (IConfigurationProvider configurationProvider in this._providers)
{
configurationProvider.Load();
}
this.RaiseChanged();
}
private void RaiseChanged()
{
Interlocked.Exchange<ConfigurationReloadToken>(ref this._changeToken, new ConfigurationReloadToken()).OnReload();
}
public IConfigurationSection GetSection(string key)
{
return new ConfigurationSection(this, key);
}
...
}
Reload
: 重新加载每个IConfigurationProvider
的数据RaiseChanged
: 通知监听者: 配置数据已更新。GetSection
: 获取子节点IConfigurationSection
我们知道一个配置系统只有一个IConfigurationRoot
,一个IConfigurationRoot
对象就代表了整个配置系统。从ConfigurationRoot
的实现中可以看到,它对所有的IConfigurationProvider
对象添加了监控。当其中某个对象发生变化时,它会调用RaiseChanged
方法向外发送配置数据的变更通知。
::: tip RaiseChanged
由于IChangeToken
对象只能发送一次通知,所以RaiseChanged
方法还负责创建新的ConfigurationReloadToken
对象并给_changeToken
字段赋值。
:::
4.2.3 ConfigurationSection
public class ConfigurationSection : IConfigurationSection, IConfiguration
{
private readonly IConfigurationRoot _root;
private readonly string _path;
public ConfigurationSection(IConfigurationRoot root, string path)
{
this._root = root;
this._path = path;
}
public IConfigurationSection GetSection(string key)
{
return this._root.GetSection(ConfigurationPath.Combine(new string[]
{
this.Path,
key
}));
}
public IChangeToken GetReloadToken() => _root.GetReloadToken();
...
}
GetReloadToken
: 调用IConfigurationRoot
的同名方法来实现。
对于组成同一颗配置树的所有IConfiguration
对象来说,不管是IConfigurationRoot
还是IConfigurationSection
,他们的GetReloadToken
方法返回的是同一个ConfigurationReloadToken
对象。这样就保证了当配置源变更时,不管应用程序使用的是哪个节点,都能够被及时地通知到。