说到配置,绝大部分系统都会有配置,不需要配置的系统是非常少的,想想以前做.net 开发时,我们常常将配置放到web.config中,然后使用ConfigurationManager去读取。
初次接触到.net core 的同学,在项目中看到有一个appsettings.json文件,确实这个appsettings.json文件是做配置用的,所以想当然的把它看做.net 开发中的web.config一样,但是我们要清除,.net core并不依赖appsettings.json文件中的配置。
.net core 提供了一种非常灵活的配置方式,大部分时候,我们只需要关注DI容器中的IConfiguration接口实例对象就可以了,下面具体介绍。
这里介绍的.net core版本是3.1,源码地址:https://github.com/dotnet/extensions/tree/v3.1.12/src/Configuration
一、原理
要介绍原理,先看与配置相关的几个接口及它们的实现类:
IConfigurationBuilder
配置建造者接口,我们使用它去创建配置对象,有一个实现类:ConfigurationBuilder
IConfiguration
表示配置集合的接口,一般的,程序通过从DI获取IConfiguration接口的实例来获取配置
IConfigurationRoot
IConfiguration的子接口,表示配置的根节点,换句话说,IConfigurationBuilder创建的第一个配置对象就是IConfigurationRoot接口对象,它的实现类是:ConfigurationRoot
IConfigurationSection
IConfiguration的子接口,表示配置的一个节点,包含节点名、节点路径、值等等,配置节点分隔默认是冒号(:),它的实现类是:ConfigurationSection
IConfigurationSource
配置来源接口,IConfigurationSource接口的实现类都很简单,主要用于结合Options创建配置提供者IConfigurationProvider,一般的,它的作用可以认为就是接收参数,然后在创建IConfigurationProvider时将参数传进去。
但是在读取来自文件的配置时,推荐继承抽象类:FileConfigurationSource ,其它的就直接实现 IConfigurationSource 就可以了,然后添加到 IConfigurationBuilder 的配置源中去。
IConfigurationProvider
配置信息的具体提供者,这个就是提供配置的获取、更新等等操作的接口,有两个重要的抽象类:ConfigurationProvider 和 FileConfigurationProvider
一般的,如果我们需要集成自己的配置,需要实现这个 IConfigurationSource 接口和 IConfigurationProvider 接口,如果我们的配置和文件有关,建议通过继承 FileConfigurationSource 两个 FileConfigurationProvider 两个抽象类来实现 IConfigurationSource 和 IConfigurationProvider接口,因为这两个抽象类已经提供了一些我们可能需要的功能,比如,它们可以监听文件状态,如果文件内容被修改,则可以重新加载配置。如果配置不来自文件,配置来源可以直接实现 IConfigurationSource 接口,而通过继承 ConfigurationProvider 来实现 IConfigurationProvider 接口。
于是乎,将它们串接起来,流程就是这样的:
1、提供一个实现了 IConfigurationProvider 接口的配置提供类,它需要提供配置的读取以及更新等操作
2、提供一个 IConfigurationSource 接口实现类,它负责创建 IConfigurationProvider 。
3、创建一个 IConfigurationBuilder 配置建造者对象,然后将 IConfigurationSource 添加进配置构造者中,这里我们一般都采用 IConfigurationBuilder 的拓展方法来实现。
4、使用 IConfigurationBuilder 构造一个 IConfigurationRoot ,然后使用这个 IConfigurationRoot 去操作配置。
这是一般流程,而.net core的配置是一个拓展模块,也就是说我们可以在控制台等其他项目中引用,只需要安装包:Microsoft.Extensions.Configuration
为了更好的说明,我们可以先看IConfiguration在WebHost中是怎么集成的,已.net core 3.1为例,它的Program是这样的:
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
} public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
看看Host.CreateDefaultBuilder()方法(源码),源码是这样的:
public static IHostBuilder CreateDefaultBuilder(string[] args)
{
... builder.ConfigureAppConfiguration((hostingContext, config) =>
{
var env = hostingContext.HostingEnvironment; config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); if (env.IsDevelopment() && !string.IsNullOrEmpty(env.ApplicationName))
{
var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
if (appAssembly != null)
{
config.AddUserSecrets(appAssembly, optional: true);
}
} config.AddEnvironmentVariables(); if (args != null)
{
config.AddCommandLine(args);
}
}) ...
}
现在可以看出为什么appsettings.json是默认的配置文件了,ConfigureAppConfiguration方法就是对配置的构造过程,这里默认最多会加载5个配置源(也就是上面config.AddXXXXX()部分,后面具体介绍)。
而ConfigureAppConfiguration的实现就是将传进去的委托保存(源码):
public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate)
{
_configureAppConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
return this;
}
这里保存是将委托放到一个List中,也就是说ConfigureAppConfiguration方法可以多次调用,我们可以添加我们自己的配置了,在Build时就会按顺序来调用(源码):
public IHost Build()
{
...
BuildAppConfiguration();
...
}
而BuildAppConfiguration方法则是最终构造配置的过程(源码):
private void BuildAppConfiguration()
{
var configBuilder = new ConfigurationBuilder()
.SetBasePath(_hostingEnvironment.ContentRootPath)
.AddConfiguration(_hostConfiguration, shouldDisposeConfiguration: true); foreach (var buildAction in _configureAppConfigActions)
{
buildAction(_hostBuilderContext, configBuilder);
}
_appConfiguration = configBuilder.Build();
_hostBuilderContext.Configuration = _appConfiguration;
}
可以看到.net core内部也是直接实例化一个ConfigurationBuilder来构造配置的,而它的Build方法则返回的是一个 IConfigurationRoot 接口对象(源码),剩下的就是使用 IConfigurationRoot 接口对象来读取更新配置了。
public IConfigurationRoot Build()
{
var providers = new List<IConfigurationProvider>();
foreach (var source in Sources)
{
var provider = source.Build(this);
providers.Add(provider);
}
return new ConfigurationRoot(providers);
}
二、内置的配置方式
官方在配置方法,提供了一些默认的配置源,它们都是通过IConfigurationBuilder的拓展方法来集成配置源,这也很好的给我们展示了如何添加自己的配置源。
官方默认提供的配置源有:
Json文件
NuGet安装包:Microsoft.Extensions.Configuration.Json
通过 IConfigurationBuilder的AddJsonFile和AddJsonStream两个拓展方法来集成(源码),有多个重载,各参数的含义如下:
provider:提供json文件的一些信息及功能操作,比如所在结构目录,监听文件状态等等,默认默认值:new PhysicalFileProvider(AppContext.BaseDirectory ?? string.Empty)
path:json文件路径
optional:表示json文件是否是可选的,如果未false,那么当json文件不存在时则会抛出异常
reloadOnChange:表示是否在文件内容修改后重新加载配置,如果未false,表示不重新加载
stream:json文件流
比如有一个json文件(注意文件位置):
{
"Hello": {
"Microsoft": {
"Extensions": "Configuration"
}
}
}
configuration.json
我们读取是这样的:
static void Main(string[] args)
{
ConfigurationBuilder builder = new ConfigurationBuilder();
builder.AddJsonFile("configuration.json");
var configuration = builder.Build();
var collections = configuration.AsEnumerable();
foreach (var item in collections)
{
Console.WriteLine("{0}={1}", item.Key, item.Value);
}
}
结果:
Ini文件
NuGet安装包:Microsoft.Extensions.Configuration.Ini
通过 IConfigurationBuilder的AddIniFile和AddIniStream两个拓展方法来集成(源码),有多个重载,各参数的含义同上Json文件。
比如我们有一个ini文件(注意文件位置):
[SessionName1]
KeyName11=value11
KeyName12=value12[Section2Name]
KeyName21=value21
KeyName22=value22
iniFile.ini
我们读取是这样的:
static void Main(string[] args)
{
ConfigurationBuilder builder = new ConfigurationBuilder();
builder.AddIniFile("iniFile.ini");
var configuration = builder.Build();
var collections = configuration.AsEnumerable();
foreach (var item in collections)
{
Console.WriteLine("{0}={1}", item.Key, item.Value);
}
}
结果:
Xml文件
NuGet安装包:Microsoft.Extensions.Configuration.Xml
通过 IConfigurationBuilder的AddXmlFile和AddXmlStream两个拓展方法来集成(源码),有多个重载,各参数的含义同上Json文件(这也是在告诉我们,如果我们要从其他文件添加,只需要类似这些参数就可以了)。
比如我们有一个xml文件(注意文件位置):
<?xml version="1.0" encoding="utf-8" ?> <node1>
<node2>value2</node2>
<node3>
<node4>value4</node4>
<node5>value5</node5>
</node3>
</node1>
xmlFile.xml
我们读取是这样的:
static void Main(string[] args)
{
ConfigurationBuilder builder = new ConfigurationBuilder();
builder.AddXmlFile("xmlFile.xml");
var configuration = builder.Build();
var collections = configuration.AsEnumerable();
foreach (var item in collections)
{
Console.WriteLine("{0}={1}", item.Key, item.Value);
}
}
结果:
注:配置的键不包含xml中的根路径
命令行参数
NuGet安装包:Microsoft.Extensions.Configuration.CommandLine
熟悉命令行的朋友知道,经常的,在执行一个命令时,可以携带一些参数,有些使用 – 或者 — 符号,有些则没有,同样的,dotnet命令在执行可以也可以携带一些运行时参数,.net core可以将这些参数集成到IConfiguration中。
通过 IConfigurationBuilder的AddCommandLine拓展方法来集成(源码),同样有几个重载,说明如下:
args:命令函参数
switchMappings:映射转化,主要是将-开头和--开头的配置转换成执行的键名
说明一下,参数必须满足一下规则:
1、必须以 -、--、/ 作为前缀,其中 -- 与 / 等价
2、如果是以 - 为前缀的参数,则必须使用 switchMappings 做一层映射,否则将抛出 FormatException ,所以自定义的命令行参数建议采用 -- 作为前缀
3、参数名与参数值之间使用 = 或者空格分隔,建议使用 =
比如:
static void Main(string[] args)
{
//dotnet XXXX.dll -a=a --b=b /c=c --e 1 /d 2 var mapper = new Dictionary<string, string>()
{
{ "-a", "mapper-a" },
{ "--b", "mapper-b" },
{ "--c", "mapper-c" }
}; ConfigurationBuilder builder = new ConfigurationBuilder();
builder.AddCommandLine(args, mapper);
var configuration = builder.Build();
var collections = configuration.AsEnumerable();
foreach (var item in collections)
{
Console.WriteLine("{0}={1}", item.Key, item.Value);
}
}
如果执行命令行:
dotnet XXXX.dll -a=a --b=b /c=c --e 1 /d 2
结果是:
环境变量
NuGet安装包:Microsoft.Extensions.Configuration.EnvironmentVariables
.net core允许我们将本地的环境变量集成到IConfiguration配置中,通过IConfigurationBuilder的AddEnvironmentVariables拓展方法来集成(源码),同时可以指定一个前缀(拓展方法中的prefix参数),表示要加载在配置中的哪些环境变量的前缀,而不是全部环境变量。
例如:
static void Main(string[] args)
{
ConfigurationBuilder builder = new ConfigurationBuilder();
builder.AddEnvironmentVariables("Common");
var configuration = builder.Build();
var collections = configuration.AsEnumerable();
foreach (var item in collections)
{
Console.WriteLine("{0}={1}", item.Key, item.Value);
}
}
结果:
注:IConfiguration中的键值会去掉环境变量名中的前缀,不如这里我的环境变量名是CommonProgramFiles,但是IConfiguration中的配置名是ProgramFiles
IConfiguration配置
有时候,我们已经存在一个IConfiguration配置了,然后我们可以将它集成到另一个IConfiguration中去使用,通过IConfigurationBuilder的AddConfiguration拓展方法来集成(源码)。
这个很简单,只看个例子就可以了:
static void Main(string[] args)
{
//Json
ConfigurationBuilder builder1 = new ConfigurationBuilder();
builder1.AddJsonFile("configuration.json");
var configuration1 = builder1.Build();
//Ini
ConfigurationBuilder builder2 = new ConfigurationBuilder();
builder2.AddIniFile("iniFile.ini");
var configuration2 = builder2.Build();
//Xml
ConfigurationBuilder builder3 = new ConfigurationBuilder();
builder3.AddXmlFile("xmlFile.xml");
var configuration3 = builder3.Build();
//EnvironmentVariables
ConfigurationBuilder builder4 = new ConfigurationBuilder();
builder4.AddEnvironmentVariables("Common");
var configuration4 = builder4.Build(); ConfigurationBuilder builder = new ConfigurationBuilder();
builder.AddConfiguration(configuration1);
builder.AddConfiguration(configuration2);
builder.AddConfiguration(configuration3);
builder.AddConfiguration(configuration4);
var configuration = builder.Build();
var collections = configuration.AsEnumerable();
foreach (var item in collections)
{
Console.WriteLine("{0}={1}", item.Key, item.Value);
}
}
结果:
内存集合
有时候,我们配置源源自某个集合变量,这是我们同样可以将它集成到IConfiguration中去,通过IConfigurationBuilder的AddInMemoryCollection拓展方法来集成(源码)。
这个也很简单,看例子就明白了了:
static void Main(string[] args)
{
Dictionary<string, string> dict = new Dictionary<string, string>();
dict["Key1"] = "Value1";
dict["Key2"] = "Value2";
dict["Key3"] = "Value3";
dict["Key4"] = "Value4"; ConfigurationBuilder builder = new ConfigurationBuilder();
builder.AddInMemoryCollection(dict);
var configuration = builder.Build();
var collections = configuration.AsEnumerable();
foreach (var item in collections)
{
Console.WriteLine("{0}={1}", item.Key, item.Value);
}
}
结果:
文件目录
NuGet安装包:Microsoft.Extensions.Configuration.KeyPerFile
可能考虑到在某些情况下,有些软件将产生的数据保存着独立的文件中,采用文件名作为区分,因此作者提供了一个将这些数据文件内容作为配置的方法,采用文件名作为key,文件内容作为value。它采用AddKeyPerFile拓展方法集成(源码)
需要注意的是,文件名中的双下划线(__)作为配置节点分隔符。
比如,我们有一些文件(在属性中输出类型设置成始终复制):
代码:
static void Main(string[] args)
{
string directory = Path.Combine(Directory.GetCurrentDirectory(), "files");
ConfigurationBuilder builder = new ConfigurationBuilder();
builder.AddKeyPerFile(directory, true);
var configuration = builder.Build();
var collections = configuration.AsEnumerable();
foreach (var item in collections)
{
Console.WriteLine("{0}={1}", item.Key, item.Value);
}
}
结果:
用户私密文件
NuGet安装包:Microsoft.Extensions.Configuration.UserSecrets
通过IConfigurationBuilder的AddUserSecrets拓展方法来集成(源码),它主要用来配置一些私密信息文件,这个很少使用,了解一下就行了。
在集成时,会涉及到一个userSecretsId,其实它的本意是一个独一无二的目录名,一般设置成GUID,我们有两种方式使用它:
方式一:直接使用
builder.AddUserSecrets("userSecretsId");
方式二:通过 UserSecretsIdAttribute 特性
首先给某个程序集添加 UserSecretsIdAttribute 特性,比如我这里就是启动项目设置:
[assembly: Microsoft.Extensions.Configuration.UserSecrets.UserSecretsId("userSecretsId")]
然后使用启动项目的程序集去获取:
builder.AddUserSecrets(typeof(Program).Assembly);
上面说到userSecretsId是目录名,那是哪个目录名呢?根据源码,私密文件路径使用 PathHelper.GetSecretsPathFromSecretsId()方法来获取(源码):
public static string GetSecretsPathFromSecretsId(string userSecretsId)
{
...
const string userSecretsFallbackDir = "DOTNET_USER_SECRETS_FALLBACK_DIR"; // For backwards compat, this checks env vars first before using Env.GetFolderPath
var appData = Environment.GetEnvironmentVariable("APPDATA");
var root = appData // On Windows it goes to %APPDATA%\Microsoft\UserSecrets\
?? Environment.GetEnvironmentVariable("HOME") // On Mac/Linux it goes to ~/.microsoft/usersecrets/
?? Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)
?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
?? Environment.GetEnvironmentVariable(userSecretsFallbackDir); // this fallback is an escape hatch if everything else fails if (string.IsNullOrEmpty(root))
{
throw new InvalidOperationException("Could not determine an appropriate location for storing user secrets. Set the " +
userSecretsFallbackDir +
" environment variable to a folder where user secrets should be stored.");
} return !string.IsNullOrEmpty(appData)
? Path.Combine(root, "Microsoft", "UserSecrets", userSecretsId, SecretsFileName)
: Path.Combine(root, ".microsoft", "usersecrets", userSecretsId, SecretsFileName);
}
在开发过程中,这个私密文件一般是:C:\Users\[USER]\AppData\Roaming\Microsoft\UserSecrets\[userSecretsId]\secrets.json
其实AddUserSecrets是基于Json文件配置的一个实现,换句话说,我们只需要在上面的隐私文件secrets.json中配置数据,就可以集成到IConfiguration中。
比如我在上面的目录下的secrets.json(C:\Users\Administrator\AppData\Roaming\Microsoft\UserSecrets\userSecretsId\secrets.json)内容如下:
{
"Secret": {
"Key1": {
"Key2": "Value2"
},
"Key3": "Value3"
}
}
secret.json
我们这样访问:
[assembly: Microsoft.Extensions.Configuration.UserSecrets.UserSecretsId("userSecretsId")] namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{
var secretPath = Microsoft.Extensions.Configuration.UserSecrets.PathHelper.GetSecretsPathFromSecretsId("userSecretsId");
Console.WriteLine("secretPath目录在:" + secretPath); ConfigurationBuilder builder = new ConfigurationBuilder();
//builder.AddUserSecrets("userSecretsId");
builder.AddUserSecrets(typeof(Program).Assembly);
var configuration = builder.Build();
var collections = configuration.AsEnumerable();
foreach (var item in collections)
{
Console.WriteLine("{0}={1}", item.Key, item.Value);
}
}
}
}
结果:
Azure云
NuGet安装包:Microsoft.Extensions.Configuration.AzureKeyVault
这个和Azure有关,因为我们基本上不用Azure云,所以就不介绍了,感兴趣的可以看看源码,都挺简单,源码地址:https://github.com/dotnet/extensions/tree/v3.1.12/src/Configuration/Config.AzureKeyVault/src
三、IConfiguration使用
一般的,我们要获取IConfiguration或者IConfigurationRoot,都是结合DI容器来使用的,比如我们的Startup:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
} public IConfiguration Configuration { get; } ...
}
1、在结构上,我们可以将IConfiguration看做一个Key-Value的集合,另一方面,因为Key的特殊性,我们又可以将它看做一种树形结构的数据集合。
这里说的Key的特殊性,指的就是Key是有层级关系的,层级采用 ConfigurationPath.KeyDelimiter 字段标识来分隔,默认是冒号(:),不建议修改(采用反射可修改)。
2、当我们配置由改动,需要重新加载时,可以调用 IConfigurationRoot 的 Reload 方法重新加载配置
3、IConfiguration的GetSection方法可以获取某个节点信息,参数key是相对于当前节点的,其中IConfigurationSection中有三个属性:
Key:当前节点名,不包含路径
Path:从根节点到当前节点的路径
Value:当前节点数据
4、IConfiguration的GetConnectionString方法获取的是当前节点下的ConnectionStrings节点下指定名称子节点的数据(源码):
public static string GetConnectionString(this IConfiguration configuration, string name)
{
return configuration?.GetSection("ConnectionStrings")?[name];
}
5、IConfiguration有两个重要的方法:Get和Bind。
Get方法将当前节点及其子节点的数据转换并存放到指定类型的实体对象的属性中,并返回改实体对象。
Bind方法接收一个实体对象,并将当前节点的及其字节点的数据保存到改实体对象的属性中。
举个简单的例子,比如我们在appsettings.json 中有配置 :
{
...
"Data": {
"Value1": 1,
"Value2": 3.14,
"Value3": true,
"Value4": [ 1, 2, 3 ],
"Value5": {
"Value1": 2,
"Value2": 5.20,
"Value3": false,
"Value4": [ 4,5,6,7 ]
}
}
}
然后相对应的创建一个实体类:
public class TestData
{
public int Value1 { get; set; }
public decimal Value2 { get; set; }
public bool Value3 { get; set; }
public int[] Value4 { get; set; }
public TestData Value5 { get; set; }
}
使用时的区别就是:
var section = configuration.GetSection("Data");
var data1= section.Get<TestData>();
var data2 = new TestData();
section.Bind(data2);
这样一来,经过Get方法或者Bind方法,IConfiguration中的数据就放到我们熟悉的实体对象中去了,再也不用去做那些烦躁的字符串类型转换了!
四、总结
这一篇就先到这里吧,.net core的配置还是很简单的,随便看看源码就能掌握。