【.NET Core框架】配置(Configuration)
简介
.netcore中的配置模块可以将你的配置文件自动读取成一个树状结构(逻辑上是树状,实际上是扁平化的),这样你就可以方便快捷的获取配置数据了。
可使用多种类型数据源(json、内存、xml、ini、command、env...),还可以自定义配置源;
支持多环境版本、如果多次添加相同的配置,后添加的会覆盖之前添加的;
热加载,修改配置文件后可不重启项目,重新将文件加载到内存;
树状结构(逻辑上)
当配置模块完成配置构建后会返回一个树状结构,这个树状结构我们称之为IConfiguration(IConfigurationRoot和IConfigurationSection都继承自IConfiguration),它的根节点是IConfigurationRoot、子节点是IConfigurationSection。子节点里封装了三个属性(Key、Path、Value),这种树状结构和注册表的组织形式很像。
涉及到的nuget包
Microsoft.Extensions.Configuration.Abstractions: 抽象定义;
Microsoft.Extensions.Configuration:配置模块顶层实现(包含从内存中提取配置);
Microsoft.Extensions.Configuration.Json:从json中提取配置
Microsoft.Extensions.Configuration.Xml:从xml中提取配置
Microsoft.Extensions.Configuration.EnvironmentVariables:从环境变量中提取配置;
Microsoft.Extensions.Configuration.CommandLine:从命令行中提取配置;
Microsoft.Extensions.Configuration.Ini:从ini文件中提取配置;
Microsoft.Extensions.Configuration.Binder:将配置数据绑定到对象;
添加数据源
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
var env = context.HostingEnvironment;
config.AddJsonFile("connectionstring.json", optional: true, reloadOnChange: true)
.AddJsonFile($"connectionstring.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
});
optional:bool类型,指示该文件是否是可选的。如果该参数为false,但是指定的文件又不存在,则会报错。
reloadOnChange:bool类型,指示该文件发生更改时,是否要重新加载配置。
除了json配置源还可以添加如下类型的配置源:
- xml
config.AddXmlFile("appsettings.xml", optional: true, reloadOnChange: true);
- ini
config.AddIniFile("appsettings.ini", optional: true, reloadOnChange: true);
- 命令行
config.AddCommandLine(args);
有三种设置命令行参数的方式:
使用=:
dotnet run Book:Name="Command line book name"
使用/:
dotnet run /Book:Name "Command line book name"
使用--:
dotnet WebApplication5.dll --Book:Name "Command line book name"
交换映射
该功能是针对命令行配置参数进行key映射的,如你可以将n映射为Name,要求:
交换映射key必须以-或--开头。当使用-开头时,命令行参数书写时也要以-开头,当使用--开头时,命令行参数书写时可以以--或/开头。
交换映射字典中的key不区分大小写,不能包含重复key。如不能同时出现-n和-N,但可以同时出现-n和--n
接下来我们来映射一下:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
var switchMappings = new Dictionary<string, string>
{
["--bn"] = "Book:Name",
["-ba0"] = "Book:Authors:0",
["--ba1"] = "Book:Authors:1",
["--bmr"] = "Book:Bookmark:Remarks"
};
config.AddCommandLine(args, switchMappings);
});
然后以命令行命令启动:
dotnet run --bn "Command line book name" -ba0 "Command line book author A" /ba1 "Command line book author B" --bmr="Command line bookmark remarks"
- 环境变量
// 添加前缀为 My_ 的环境变量
config.AddEnvironmentVariables(prefix: "My_");
在添加环境变量时,通过指定参数prefix,只读取限定前缀的环境变量。不过在读取环境变量时,会将前缀删除。如果不指定参数prefix,那么会读取所有环境变量。
当创建默认通用主机(Host)时,默认就已经添加了前缀为DOTNET_的环境变量,加载应用配置时,也添加了未限定前缀的环境变量。另外,在 ASP.NET Core 中,配置 Web主机时,默认添加了前缀为ASPNETCORE_的环境变量。
需要注意的是,由于环境变量的分层键:并不受所有平台支持,而双下划线()是全平台支持的,所以要使用双下划线()来代替冒号(:),如下:
//Book__Name == Book.Name
set Book__Name "book name"
- 内存
config.AddInMemoryCollection(new Dictionary<string, string>
{
["Book:Name"] = "Memmory book name",
["Book:Authors:0"] = "Memory book author A",
["Book:Authors:1"] = "Memory book author B",
["Book:Bookmark:Remarks"] = "Memory bookmark remarks"
});
.net core框架已经为我们添加了配置文件appsettings.json、appsettings.{EnvironmentName}.json,所以不需要重新添加
读取配置
弱类型读取
Configuration["Logging:Default:t1"] //按层级取值,返回值是字符串类型
Configuration.GetSection("Ips").Value //返回值是字符串类型
强类型读取(绑定)
虽然我们可以从IConfiguration中轻松的取出配置值,但是我们更倾向于将其转换成一个POCO对象,以面向对象的方式来使用配置,我们将这个转换过程称为配置绑定。
绑定相关的部分扩展方法定义在NuGet包“Microsoft.Extensions.Configuration.Binder”中
Settings settings = Configuration.Get<Settings>();//将Configuration转为Settings对象
string[] ips = Configuration.GetSection("Ips").Get<string[]>();//将Ips节点转为数组
LogLevel logLevel = Configuration.GetSection("Logging:LogLevel").Get<LogLevel>();//节点转为对象
int CACHE_TIME = configuration.GetValue<int>("CACHE_TIME", 20);//20是默认值,需要安装 Microsoft.Extensions.Configuration.Binder包
Bind:将Configuration绑定现有对象
Logging logging = new Logging();
Configuration.GetSection("Logging").Bind(logging);
自定义绑定转换器
[TypeConverter(typeof(PointConvertor))]
public class Point
{
public double X { set; get; }
public double Y { set; get; }
}
public class PointConvertor : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
try
{
string str = value.ToString();
var arr = str.Split(',');
var x = double.Parse(arr[0]);
var y = double.Parse(arr[1]);
return new Point()
{
X = x,
Y = y
};
}
catch (Exception ex)
{
Console.WriteLine($"转换出错:{ex?.Message}");
return null;
}
}
}
public class Person
{
public Point pos { set; get; }
}
class Program
{
public static void Main(string[] args)
{
var conf = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string>()
{
["root:person:pos"] = "113.456784,27.234562"
}).Build();
var person = conf.GetSection("root:person").Get<Person>();
}
}
绑定到字典
var conf = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string>()
{
["root:appname"] = "测试应用",
["root:appversion"] = "v1.0.0",
["root:person:小明:name"] = "小明",
["root:person:小明:age"] = "28",
["root:person:小明:id"] = "2",
["root:person:小红:name"] = "小红",
["root:person:小红:age"] = "24",
["root:person:小红:id"] = "3",
["root:student:0:id"] = "4",
["root:student:0:name"] = "张三",
["root:student:0:age"] = "10",
["root:student:1:id"] = "5",
["root:student:1:name"] = "李四",
["root:student:1:age"] = "11"
}).Build();
var dics = conf.GetSection("root:student").Get<IDictionary<string, Person>>();
string json = JsonConvert.SerializeObject(dics);
//{
// "0":{"name":"张三","id":4,"age":10},
// "1":{"name":"李四","id":5,"age":11}
//}
var dics2 = conf.GetSection("root:person").Get<IDictionary<string, Person>>();
string json2 = JsonConvert.SerializeObject(dics2);
//{
// "小明":{"name":"小明","id":2,"age":28},
// "小红":{"name":"小红","id":3,"age":24}
//}
核心对象
- IConfiguration:配置信息最终会转换成一个IConfiguration对象供应用程序使用
- IConfigurationBuilder:IConfigurationBuilder是IConfiguration对象的构建者
- IConfigurationSource:IConfigurationSource代表配置数据的来源,它会注册到IConfigurationBuilder对象上
- IConfigurationProvider:向IConfiguration提供数据,IConfiguration内部包含IConfigurationProvider对象
以上对象的关系如图:
以上接口以及其他一些基础类型均定义在NuGet包“Microsoft.Extensions.Configuration.Abstractions”中。对这些接口的默认实现,则大多定义在“Microsoft.Extensions.Configuration”这个NuGet包中
IConfiguration
一个IConfiguration对象表示配置树的某个配置节点。表示根节点的IConfiguration对象与表示其它配置节点是不同的,配置模型采用不同的接口来表示它们。根节点使用IConfigurationRoot接口表示,其他节点使用IConfigurationSection接口表示,IConfigurationRoot和IConfigurationSection接口都是IConfiguration的继承者。
public interface IConfiguration
{
IEnumerable<IConfigurationSection> GetChildren();
IConfigurationSection GetSection(string key);//key:相对于当前配置节的路径
IChangeToken GetReloadToken();
string this[string key] { get; set; }
}
IConfigurationRoot
Reload方法实现对配置数据的重新加载。IConfigurationRoot对象表示的配置树的根,所以也代表了整棵配置树,如果它被重新加载了,意味着整棵配置树承载的所有配置数据均被重新加载了。
IConfigurationRoot:
public interface IConfigurationRoot : IConfiguration
{
void Reload();
}
ConfigurationRoot:
public class ConfigurationRoot : IConfigurationRoot, IDisposable
{
private readonly IList<IConfigurationProvider> _providers;
private readonly IList<IDisposable> _changeTokenRegistrations;
private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();
/// <param name="providers">The <see cref="IConfigurationProvider"/>s for this configuration.</param>
public ConfigurationRoot(IList<IConfigurationProvider> providers)
{
if (providers == null)
{
throw new ArgumentNullException(nameof(providers));
}
_providers = providers;
_changeTokenRegistrations = new List<IDisposable>(providers.Count);
foreach (IConfigurationProvider p in providers)
{
p.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
}
}
public IEnumerable<IConfigurationProvider> Providers => _providers;
public string this[string key]
{
get
{
for (int i = _providers.Count - 1; i >= 0; i--)
{
IConfigurationProvider provider = _providers[i];
if (provider.TryGet(key, out string value))
{
return value;
}
}
return null;
}
set
{
if (!_providers.Any())
{
throw new InvalidOperationException(SR.Error_NoSources);
}
foreach (IConfigurationProvider provider in _providers)
{
provider.Set(key, value);
}
}
}
public IEnumerable<IConfigurationSection> GetChildren() => this.GetChildrenImplementation(null);
public IChangeToken GetReloadToken() => _changeToken;
public IConfigurationSection GetSection(string key)
=> new ConfigurationSection(this, key);
public void Reload()
{
foreach (IConfigurationProvider provider in _providers)
{
provider.Load();
}
RaiseChanged();
}
private void RaiseChanged()
{
ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
previousToken.OnReload();
}
public void Dispose()
{
// dispose change token registrations
foreach (IDisposable registration in _changeTokenRegistrations)
{
registration.Dispose();
}
// dispose providers
foreach (IConfigurationProvider provider in _providers)
{
(provider as IDisposable)?.Dispose();
}
}
}
IConfigurationSection
Key用来唯一标识多个具有相同父节点的ConfigurationSection对象
Path表示当前配置节点在配置树中的路径
Value表示配置节点承载的配置数据,只有配置树的叶子结点对应的IConfigurationSection对象才具有值
IConfigurationSection:
public interface IConfigurationSection : IConfiguration
{
string Path { get; }
string Key { get; }
string Value { get; set; }
}
ConfigurationSection:
public class ConfigurationSection : IConfigurationSection
{
private readonly IConfigurationRoot _root;
private readonly string _path;
private string _key;
public ConfigurationSection(IConfigurationRoot root, string path)
{
if (root == null)
{
throw new ArgumentNullException(nameof(root));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
_root = root;
_path = path;
}
/// <summary>
/// 获取到此Section的完整路径
/// </summary>
public string Path => _path;
/// <summary>
/// 获取此Section在其父节点中占用的键。
/// </summary>
public string Key
{
get
{
if (_key == null)
{
_key = ConfigurationPath.GetSectionKey(_path);
}
return _key;
}
}
/// <summary>
/// Gets or sets the section value.
/// </summary>
public string Value
{
get
{
return _root[Path];
}
set
{
_root[Path] = value;
}
}
public string this[string key]
{
get
{
return _root[ConfigurationPath.Combine(Path, key)];
}
set
{
_root[ConfigurationPath.Combine(Path, key)] = value;
}
}
public IConfigurationSection GetSection(string key) => _root.GetSection(ConfigurationPath.Combine(Path, key));
public IEnumerable<IConfigurationSection> GetChildren() => _root.GetChildrenImplementation(Path);
public IChangeToken GetReloadToken() => _root.GetReloadToken();
}
IConfigurationProvider
IConfigurationProvider对象的目的在于将配置从原始结构转换成配置字典,配置数据的加载通过调用IConfigurationProvider的Load方法来完成。
IConfigurationProvider的GetChildKeys方法用于获取某个指定配置节点(对应于parentPath参数)的所有子节点的Key。当IConfiguration的GetChildren方法被调用时,注册的所有IConfigurationSource对应的IConfigurationProvider的GetChildKeys方法会被调用。这个方法的第一个参数earlierKeys代表的Key来源于其他IConfigurationProvider,当解析出当前IConfigurationProvider提供的Key后,该方法需要对它们合并到earlierKeys集合中,合并后结果将作为方法的返回值。值得一提的是,返回的Key的集合是经过排序的。
public interface IConfigurationProvider
{
void Load();
void Set(string key, string value);
bool TryGet(string key, out string value);
IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath);
IChangeToken GetReloadToken();
}
ConfigurationProvider:
每种类型的配置源都具有对应的IConfigurationProvider实现,它们一般不会直接实现接口IConfigurationProvider,而会选择继承另一个名为ConfigurationProvider的抽象类。
这个基类把配置源里面的数据都放在了一个字典(IDictionary<string, string> Data)中,这样凡是需要获取配置数据的时候就会遍历这个字典,找到后就返回。
public abstract class ConfigurationProvider : IConfigurationProvider
{
private ConfigurationReloadToken _reloadToken = new ConfigurationReloadToken();
protected ConfigurationProvider()
{
Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
protected IDictionary<string, string> Data { get; set; }
public virtual bool TryGet(string key, out string value)
=> Data.TryGetValue(key, out value);
public virtual void Set(string key, string value)
=> Data[key] = value;
/// <summary>
/// Loads (or reloads) the data for this provider.
/// </summary>
public virtual void Load()
{ }
//返回此提供程序拥有的键的列表。
public virtual IEnumerable<string> GetChildKeys(
IEnumerable<string> earlierKeys,
string parentPath)
{
string prefix = parentPath == null ? string.Empty : parentPath + ConfigurationPath.KeyDelimiter;
return Data
.Where(kv => kv.Key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
.Select(kv => Segment(kv.Key, prefix.Length))
.Concat(earlierKeys)
.OrderBy(k => k, ConfigurationKeyComparer.Instance);
}
private static string Segment(string key, int prefixLength)
{
int indexOf = key.IndexOf(ConfigurationPath.KeyDelimiter, prefixLength, StringComparison.OrdinalIgnoreCase);
return indexOf < 0 ? key.Substring(prefixLength) : key.Substring(prefixLength, indexOf - prefixLength);
}
public IChangeToken GetReloadToken()
{
return _reloadToken;
}
protected void OnReload()
{
ConfigurationReloadToken previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken());
previousToken.OnReload();
}
public override string ToString() => $"{GetType().Name}";
}
IConfigurationSource
IConfiurationSource代表配置源,它为IConfigurationBuilder提供原始的配置数据,每种不同类型的配置源都具有一个对应的IConfigurationSource实现。由于针对原始配置数据的读取实现在相应的IConfigurationProvider中,所以IConfigurationSource所起的作用在于提供相应的IConfigurationProvider。
public interface IConfigurationSource
{
IConfigurationProvider Build(IConfigurationBuilder builder);
}
MemoryConfigurationSource:
//内存中的配置源
public class MemoryConfigurationSource : IConfigurationSource
{
public IEnumerable<KeyValuePair<string, string>> InitialData
{
get;
set;
}
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new MemoryConfigurationProvider(this);
}
}
IConfigurationBuilder
它通过IConfigurationSource创建IConfiguration对象
IConfigurationBuilder:
public interface IConfigurationBuilder
{
IEnumerable<IConfigurationSource> Sources { get; }
Dictionary<string, object> Properties { get; }
IConfigurationBuilder Add(IConfigurationSource source);
IConfigurationRoot Build();
}
ConfigurationBuilder:
这个类负责搜集配置源,将配置源Build后获取到provider,最后将这些provider放进新创建的IConfigurationRoot中
public class ConfigurationBuilder : IConfigurationBuilder
{
public IList<IConfigurationSource> Sources
{
get;
} = new List<IConfigurationSource>();
public IDictionary<string, object> Properties
{
get;
} = new Dictionary<string, object>();
public IConfigurationBuilder Add(IConfigurationSource source)
{
if (source == null)
{
throw new ArgumentNullException("source");
}
Sources.Add(source);
return this;
}
public IConfigurationRoot Build()
{
List<IConfigurationProvider> list = new List<IConfigurationProvider>();
foreach (IConfigurationSource source in Sources)
{
IConfigurationProvider item = source.Build(this);
list.Add(item);
}
return new ConfigurationRoot(list);
}
}
案例
1、遍历数组节点
{"Configs": [
{
"Name": "n1"
}
]
}
private void LoadConfigs(IConfiguration configuration)
{
var configs = configuration.GetSection("Configs");
var configCount = configs.GetChildren().Count();
for (int i = 0; i < configCount; i++)
{
var config = configs.GetSection(i.ToString());
string name = config.GetValue<string>("Name");
}
}
}
2、自定义数据源
数据源是一个加密的文件
ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddCryptoFile(Path.Combine(AppContext.BaseDirectory, "appsettings.json.crypto"), true);
});
ConfigurationBuilderExtension
public static class ConfigurationBuilderExtension
{
public static IConfigurationBuilder AddCryptoFile(this IConfigurationBuilder configurationBuilder, string path, bool reloadOnChange)
{
configurationBuilder.Add(new CryptoFileConfigurationSource(path, reloadOnChange));
return configurationBuilder;
}
}
CryptoFileConfigurationSource
public class CryptoFileConfigurationSource : IConfigurationSource
{
/// <summary>
/// 路径
/// </summary>
public string Path { get; set; }
/// <summary>
/// 是否启用热加载
/// </summary>
public bool ReloadOnChange { get; set; }
public CryptoFileConfigurationSource(string path, bool reloadOnChange)
{
Path = path;
ReloadOnChange = reloadOnChange;
}
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new CryptoFileConfigurationProvider(this);
}
}
CryptoFileConfigurationProvider
public class CryptoFileConfigurationProvider : ConfigurationProvider
{
private static ConcurrentDictionary<string, string> CONFIG_CACHE = new ConcurrentDictionary<string, string>();
private CryptoFileConfigurationSource _source = null;
private string _random = string.Empty;
public CryptoFileConfigurationProvider(CryptoFileConfigurationSource source)
{
this._source = source;
string fileName = IOHelper.GetFileNameNoPath(_source.Path);
string filePath = _source.Path.Replace(fileName, string.Empty);
IFileProvider fileProvider = new PhysicalFileProvider(filePath);
if (_source.ReloadOnChange)
{
ChangeToken.OnChange(
() => fileProvider.Watch(fileName),
() =>
{
Load();
});
}
else
{
Load();
}
}
public override void Load()
{
if (!IOHelper.IsExistFilePath(_source.Path))
{
throw new ArgumentException($"{_source.Path}路径不存在");
}
string cryptoContent = IOHelper.GetFileContent(_source.Path);
//解密cryptoContent,获取json
string json = AESCryptoHelper.Decrypt(cryptoContent);
byte[] bytes = Encoding.UTF8.GetBytes(json);
MemoryStream ms = new MemoryStream(bytes);
this.Data = JsonConfigurationFileParser.Parse(ms);
}
}
JsonConfigurationFileParser
internal class JsonConfigurationFileParser
{
private JsonConfigurationFileParser() { }
private readonly IDictionary<string, string> _data = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
private readonly Stack<string> _context = new Stack<string>();
private string _currentPath;
private JsonTextReader _reader;
public static IDictionary<string, string> Parse(Stream input)
=> new JsonConfigurationFileParser().ParseStream(input);
private IDictionary<string, string> ParseStream(Stream input)
{
_data.Clear();
_reader = new JsonTextReader(new StreamReader(input));
_reader.DateParseHandling = DateParseHandling.None;
var jsonConfig = JObject.Load(_reader);
VisitJObject(jsonConfig);
return _data;
}
private void VisitJObject(JObject jObject)
{
foreach (var property in jObject.Properties())
{
EnterContext(property.Name);
VisitProperty(property);
ExitContext();
}
}
private void VisitProperty(JProperty property)
{
VisitToken(property.Value);
}
private void VisitToken(JToken token)
{
switch (token.Type)
{
case JTokenType.Object:
VisitJObject(token.Value<JObject>());
break;
case JTokenType.Array:
VisitArray(token.Value<JArray>());
break;
case JTokenType.Integer:
case JTokenType.Float:
case JTokenType.String:
case JTokenType.Boolean:
case JTokenType.Bytes:
case JTokenType.Raw:
case JTokenType.Null:
VisitPrimitive(token.Value<JValue>());
break;
default:
throw new Exception("format error");
}
}
private void VisitArray(JArray array)
{
for (int index = 0; index < array.Count; index++)
{
EnterContext(index.ToString());
VisitToken(array[index]);
ExitContext();
}
}
private void VisitPrimitive(JValue data)
{
var key = _currentPath;
if (_data.ContainsKey(key))
{
throw new Exception("format error");
}
_data[key] = data.ToString(CultureInfo.InvariantCulture);
}
private void EnterContext(string context)
{
_context.Push(context);
_currentPath = ConfigurationPath.Combine(_context.Reverse());
}
private void ExitContext()
{
_context.Pop();
_currentPath = ConfigurationPath.Combine(_context.Reverse());
}
}
参考:
https://www.cnblogs.com/artech/p/inside-asp-net-core-05-09.html
https://blog.csdn.net/u010476739/article/details/105856032