.Net Core中的配置Configuration使用以及源码解析

.Net Core中的配置Configuration使用以及源码解析

在以前的.Net Framework程序中,我们的很多配置都会写到App.config或者Web.config,然后通过系统提供的System.Configuration.ConfigurationManager去获取相应的配置,但是在.Net Core 我们有了新的配置获取方式,并且不只是支持config文件,默认实现了ini,xml,json等一系列文件类型的获取方式,并且他们的获取方式是统一的,它做到了不同的配置源,统一的获取方式。

使用IConfiguration来获取配置信息

新建一个Asp.Net Core MVC 应用,这里你也可以建控制台应用,只是网站应用方便演示,两者没有区别。唯一不同的地方是网站已经为我们创建了ConfigurationBuilder对象,并且注入到了容器中,同时也添加了一些默认的配置。

在项目里添加一个config01.json文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"EnableCache": true,
"Email": {
"From": "from@email.com",
"To": "to@email.com"
},
"Type": {
"Assembly": {
"Namespace": {
"FullName": "CoreConfiguration.Controllers.HomeController"
}
}
}
}

接着在Programm.cs文件中找到CreateWebHostBuilder方法,修改如下:

1
2
3
4
5
6
7
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, configBuilder) =>
{
configBuilder.AddJsonFile("config01.json");
})
.UseStartup<Startup>();

然后可以在任意一个地方通过依赖注入的方式获取到IConfiguration这个对象,通过这个对象就可以获取对应的配置,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class HomeController : Controller
{
IConfiguration _configuration;
public HomeController(IConfiguration configuration)
{
_configuration = configuration;
}

public IActionResult Start()
{
ViewBag.Item1 = _configuration["EnableCache"];
ViewBag.Item2 = _configuration["Email"];

IConfigurationSection emailConfiguration = _configuration.GetSection("Email");
ViewBag.Item3 = emailConfiguration["To"];
ViewBag.Item4 = emailConfiguration["From"];

ViewBag.Item5 = _configuration["Email:To"];// why is ":" ?
ViewBag.Item6 = _configuration["Email:From"];

IConfigurationSection emailToConfiguration = emailConfiguration.GetSection("To");
ViewBag.Item7 = emailToConfiguration.Value;//Same with emailConfiguration["To"]

ViewBag.Item8 = _configuration.GetSection("Type").GetSection("Assembly").GetSection("Namespace")["FullName"];

return View();
}
}

最终得到的结果如下:
IConfiguration使用方式一

在这个使用的例子中,我们可以看到,对于简单的值可以直接以键值对的方式获取,而复杂的值,则会返回null,这似乎不太合理,但这是和它解析文件后存储内容的方式有关,后续会详细解释。

如果我们想要获取这种层级的值有两种方式:1.先通过GetSection方法获取对应的子节点,再去获取。2.直接在传递键(key)的时候,进行组合并且以分号(:)隔开,这样也是可以直接获得的。

另外我们可以看到在调用GetSection返回的是IConfigurationSection对象,而不是IConfiguration,实际上IConfigurationSection是继承于IConfiguration,所以我们一样可以在IConfigurationSection直接获取值。另外我们在HomeController里注入进来的那个配置对象,其实是一个IConfigurationRoot,当然它也是继承于IConfiguration,所以他们三者之间UML图,如下:

IConfiguration,IConfigurationRoot,IConfigurationSection的UML图

对于定义在IConfiguration里的几个成员,自不必说,大家都能理解。而在IConfigurationSection中,Path代表了当前子节点路径,Key代表了当前子节点的名称,而value就是当前子节点代表的值,如下两个例子,会让大家更容易理解几个成员的意思:

1
var section1 = _configuration.GetSection("Type").GetSection("Assembly").GetSection("Namespace");

section1:

  • Key: Namespace
  • Path: Type:Assembly:Namespace
  • Value: null
1
var section2 = _configuration.GetSection("Type").GetSection("Assembly").GetSection("Namespace").GetSection("FullName");

section2:

  • Key: FullName
  • Path: Type:Assembly:Namespace:FullName
  • Value: CoreConfiguration.Controllers.HomeController

而IConfigurationRoot里,只多了一个IConfigurationProvider的枚举,而这个IConfigurationProvider对象,就是实际存储数据的地方,后续会详细解释。

讲到这里,大家对配置的获取,应该有了一个基本的认识。但是应该会有一个疑问,比如在例子中,我们能不能直接获取整个Email相关的配置并且映射成一个类对象,而不是像上面一样一个一个的获取?毫无疑问,肯定是可以的。

使用Binder获取整个节点的配置

使用方法可以直接看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public IActionResult Binder()
{
IConfigurationSection emailConfiguration = _configuration.GetSection("Email");
var emailOption = emailConfiguration.Get<EmailOption>();
ViewBag.Item1 = emailOption.From;
ViewBag.Item2 = emailOption.To;

//string str1 = _configuration["EnableCache"];
//bool enableCache = bool.Parse(str1);
bool enableCache = _configuration.GetSection("EnableCache").Get<bool>();
ViewBag.Item3 = enableCache;

return View();
}

public class EmailOption
{
public string From { get; set; }

public string To { get; set; }
}

最后输出得到的值和第一个例子别无二致:

IConfiguration配合Options的使用方式

在例子里我们调用了扩展方法:public static T Get(this IConfiguration configuration);使用它,必须添加Microsoft.Extensions.Configuration.Binder这个nuget包,如果是网站程序,这个包默认已经引入进来。可以看到,这个方法是基于IConfiguration的,所以IConfigurationSection和IConfigurationRoot都可以使用它。

多数据源的支持

在我们的项目当中,可能有不止一个配置文件,甚至有不同的类型的配置文件,那么我们只要依次添加这些配置就行了。假如我们的项目中还有如下两个配置文件:

config02.ini

1
2
3
4
5
LogType=Log4Net

[Email]
Body=BodyIni
Subject=Subject

config03.xml

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8" ?>
<config>
<RetryCount>10</RetryCount>
<Email>
<Body>BodyXml</Body>
<BodyType>Html</BodyType>
</Email>
</config>

同时我们也需要一些基于内存数据的配置,那么在Programm.cs修改如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((hostingContext, config) =>
{
                  config.AddInMemoryCollection(new Dictionary<string, string>() {                        { "LogLevel","Debug"},                        { "Email:UseSSL","true"}                  });
                  config.AddJsonFile("config01.json");
config.AddIniFile("config02.ini");
config.AddXmlFile("config03.xml");

})
.UseStartup<Startup>();
}

添加如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public IActionResult MultiSource()
{
IConfigurationSection emailConfiguration = _configuration.GetSection("Email");
MultiEmailOption multiEmailOption = emailConfiguration.Get<MultiEmailOption>();

//InMemory
ViewBag.Item1 = _configuration["LogLevel"];
ViewBag.Item2 = emailConfiguration["UseSSL"];

//Xml
ViewBag.Item3 = _configuration["RetryCount"];
ViewBag.Item4 = emailConfiguration["BodyType"];

//Ini
ViewBag.Item5 = _configuration["LogType"];
ViewBag.Item6 = emailConfiguration["Subject"];

ViewBag.Item7 = Newtonsoft.Json.JsonConvert.SerializeObject(multiEmailOption);
return View();
}

public class MultiEmailOption : EmailOption
{
public string Body { get; set; }

public bool UseSSL { get; set; }

public string Subject { get; set; }

public string BodyType { get; set; }
}

最后输出的结果如下:
IConfiguration配置配合内存配置使用方式

我们可以看到来源于不同的Source的配置,最终可以被整合成到同一个对象,这得益于所有不同的文件最终都会被解析成相同结构的数据源。

另外,最终取到的Email.Body的值为BodyXml,虽然在ini文件中也定义了该属性的值,但是框架会按添加数据源的逆序依次匹配数据源里的数据。利用这一特性可以实现不同环境的不同配置。

可监控的数据源

以前的.Net Framework程序,不论是桌面还是网站,修改配置源以后都必须重启程序才能生效。但是在.Net Core中,在添加参数时,指定reloadOnChange参数就可以在不重启应用的情况下获取新的配置,但是启用这个特性有性能损耗,如非必要,不用添加此参数以启用监控。对Json类型的配置文件,在添加时,如此调用就可以启用监控:

1
config.AddJsonFile("config04.json",true,true);

四大配置对象

在上面介绍了如何在.Net Core 中使用配置,接下来将详细介绍,这些都是如何实现的,先来看一张四大配置对象的UML图

IConfigurationBuilder,IConfigurationSource,IConfigurationProvider,IConfigurationRootU这4大对象的UML图

  • IConfigurationBuilder:整个配置系统的核心,包含多个IConfigurationSource,利用它们产生多个IConfigurationProvider,最终是为了得到一个IConfigurationRoot对象,并将它注入到容器中。
  • IConfigurationProvider:实际包含配置信息的类,内部包含一个字符串字典,它由IConfigurationSource产生。
  • IConfigurationSource:包含了一个配置源的信息,以文件为例,包含了文件名和文件路径等,并不包含实际的配置信息。如果是基于数据库的数据源,它会包含数据库连接信息,SQL等。它的目的是为了产生一个IConfigurationProvider。
  • IConfigurationRoot:在获取配置信息时,直接操作的对象,内部包含一个IConfigurationProvider列表。

在实际的使用过程中,我们首先都是调用各类数据源类库提供的扩展方法往IConfigurationBuilder添加数据源IConfigurationSource,之后IConfigurationBuilder会依次调用IConfigurationSource的Build方法产生对应的IConfigurationProvider,并将他们传入到IConfigurationRoot中,最终我们会拿到IConfigurationRoot进行使用。下面来依次看一看这几个类的核心实现(以下的代码都不是全部实现,只拿出了一部分)。

ConfigurationBuilder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ConfigurationBuilder : IConfigurationBuilder
{
public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();


public IConfigurationBuilder Add(IConfigurationSource source)
{
if (source == null)
{
throw new ArgumentNullException(nameof(source));
}

Sources.Add(source);
return this;
}


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);
}
}

ConfigurationProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class ConfigurationProvider : IConfigurationProvider
{
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;

public virtual void Load()
{ }
}

ConfigurationProvider系统的基础实现是一个抽象类,具体的需要由对应类型的Provider去实现,其实也只是需要实现Load方法,去填充那个字符串字典。那应该如何实现这个字典呢,这个时候我们需要使用ConfigurationPath这个静态类来帮助我们生成字典里的Key。具体来说就是各层级之间用分号(:)隔开,例如:A:B:C。这个分号是以静态只读的形式定义在ConfigurationPath中,最后我们可以看看我们之前定义的config01.json文件最终生成的字典结构,如下:
IConfiguration在内部的存储数据的方式

ConfigurationRoot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class ConfigurationRoot : IConfigurationRoot
{
private IList<IConfigurationProvider> _providers;public ConfigurationRoot(IList<IConfigurationProvider> providers)
{
if (providers == null)
{
throw new ArgumentNullException(nameof(providers));
}

_providers = providers;
foreach (var p in providers)
{
p.Load();
}
}

public IEnumerable<IConfigurationProvider> Providers => _providers;

public string this[string key]
{
get
{
foreach (var provider in _providers.Reverse())
{
string value;

if (provider.TryGet(key, out value))
{
return value;
}
}

return null;
}

set
{
if (!_providers.Any())
{
throw new InvalidOperationException("Error");
}

foreach (var provider in _providers)
{
provider.Set(key, value);
}
}
}
public IConfigurationSection GetSection(string key)
=> new ConfigurationSection(this, key);

}

在这里我们可以看到,在构建的时候会依次调用传递过来的Provider的load方法去加载数据,而在取的数据的时候,会逆序依次调用Provider的TryGet方法获取数据,如果成功就直接返回,这就是为什么后添加的数据源会覆盖之前添加的数据源。

同时在创建ConfigurationSection时候会把ConfigurationRoot传递进去,而ConfigurationSection取数据的时候也是调用ConfigurationRoot的Get方法,实际ConfigurationSection也并不包含任何实际的配置数据。

最后,对于可监控的文件源,是基于FileWatch来实现的,核心代码:

1
2
3
4
5
6
7
8
9
if (Source.ReloadOnChange && Source.FileProvider != null)
{
ChangeToken.OnChange(
() => Source.FileProvider.Watch(Source.Path),
() => {
Thread.Sleep(Source.ReloadDelay);
Load(reload: true);
});
}

示例中用到的Demo Code和UML图,以及相关的源码都在 https://github.com/zhurongbo111/AspNetCoreDemo/tree/master/01-Configuration