配置系统

在专业的软件项目中,一些配置项的值应该是可以修改的,我们不应该把这些值硬编码到代码中,.NET Core中提供了非常强大的配置系统以简化配置相关代码的编写方法。

1、配置系统的基本使用

在传统软件开发中,我们一般把数据库连接字符串等配置项放到配置文件中,比如.NET Framework中的Web.config文件,这样如果需要修改程序连接的数据库,我们只要修改配置文件就可以了.

.Net core中不推荐使用Web.config了。

但是在.Net core中的配置系统支持非常丰富的配置方式,例如文件配置(JSON,XML,INI等),注册表,环境变量,命令行等。

.Net core中读取配置文件中的内容,我们可以通过IConfigurationRoot读取配置信息。

下面来看一下基本的使用。

首先,在项目的跟目录下面添加一个JSON文件,如文件名为config.json,该文件中的内容如下所示:

1
2
3
4
5
6
7
{
"name": "laowang",
"proxy": {
"address": "127.0.0.1",
"port": 1234
}
}

因为程序在运行的时候默认加载exe文件同文件夹下的配置文件,而不是项目中的config.json文件,所以我们需要把config.json文件设置为生成项目的时候自动被复制到生成目录.

因此请在Visual Studio中,在config.json文件上右击,选择【属性】,然后在【属性】窗口中把【复制到输出目录】属性修改为“如果较新则复制”(修改了配置文件中的内容,一定要重新生成,这样才会输出到exe文件所在的目录)。

.NET Core中配置系统的基础开发包是Microsoft.Extensions.Configuration,而读取JSON文件的开发包是Microsoft.Extensions.Configuration.Json,因此请先用NuGet安装这两个包。

1
2
Install-Package Microsoft.Extensions.Configuration
Install-Package Microsoft.Extensions.Configuration.Json

下面读取配置文件中的代码如下所示:

1
2
3
4
5
6
7
8
9
using Microsoft.Extensions.Configuration;

ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddJsonFile("config.json", optional: false, reloadOnChange: true);
IConfigurationRoot config= configurationBuilder.Build();
string name = config["name"]!;
Console.WriteLine($"name={name}");
string address = config.GetSection("proxy:address").Value!;
Console.WriteLine($"$Adress:{address}");

在上面的代码中,首先创建了configurationBuilder对象,然后调用了该对象中的AddJsonFile文件,添加了config.json这个配置文件。optional表示的是当前这个文件是否可选,如果它的值为true,则表示当前配置文件不存在的时候,程序不会报错;如果它的值是false,当配置文件不存在的时候,程序会报错。这里建议将其设置为false,这样当文件名或者文件路径写错的时候,出错了,能够及时的发现错误。reloadOnChange参数表示如果文件修改了,是否重新加载配置。设置为true表示的就是重新加载配置。

下面又调用了configurationBuilder对象中的Build方法,创建了IConfigurationRoot对象,通过该对象读取配置文件中的配置项,如果写在JSON文件中的配置项是分级的,可以通过proxy:address这种冒号分割的方式读取配置项。

补充:

IConfigurationRoot中有一个GetConnectionString(string name)方法用于获取连接字符串,它读取“ConnectionStrings”节点下的名为name的值作为连接字符串。**“ConnectionStrings”是.NET Core`要求必须使用这个节点保存数据库连接字符串。**

演示:

修改config.json这个配置文件,如下代码所示:

1
2
3
4
5
6
7
8
9
10
{
"name": "laowang",
"proxy": {
"address": "127.0.0.1",
"port": 1234
},
"ConnectionStrings": {
"str": "Data Source=.;Initial Catalog=TestDB;uid=sa;pwd=123456"
}
}

在上面的配置中,添加了配置节点:ConnectionStrings,在该配置节点下面添加了str这个配置项,它的值就是链接数据库的字符串。

1
2
3
string address = config.GetSection("proxy:address").Value!;
Console.WriteLine($"$Adress:{address}");
Console.WriteLine(config.GetConnectionString("str"));// 这里就可以通过GetConnectionString方法获取名称是str的数据库链接字符串

以上就是读取配置文件的基本操作。

2、采用绑定方式读取配置

在上一小结的代码中,为了能够读取配置文件中的配置项,采用了如下的方式:

1
2
IConfigurationRoot config= configurationBuilder.Build();
string name = config["name"]!;

当然,在.Net中还提供了另外一种相对来讲比较简单的读取配置项的方式,就是可以采用绑定一个类的方式,自动完成配置的读取。

首先安装如下包。

1
Install-Package  Microsoft.Extensions.Configuration.Binder

然后在Program.cs文件中定义一个类(该类的名字可以随意的命名。),定义该类的目的就是将配置文件中的proxy配置节点中的配置项的值赋值给Proxy中的属性

1
2
3
4
5
6
//类名随意定义,属性与配置项中的名称保持一致
class Proxy
{
public string? address { get; set; }
public int port { get; set; }
}

类名在这里可以随意定义,属性的名字要与配置文件中的proxy配置节中配置项名称保持一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using Microsoft.Extensions.Configuration;

ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddJsonFile("config.json", optional: false, reloadOnChange: true);
IConfigurationRoot config= configurationBuilder.Build();
/*string name = config["name"]!;
Console.WriteLine($"name={name}");
string address = config.GetSection("proxy:address").Value!;
Console.WriteLine($"$Adress:{address}");
Console.WriteLine(config.GetConnectionString("str"));*/
Proxy p = config.GetSection("proxy").Get<Proxy>()!;
Console.WriteLine($"IP地址是:{p.address},端口号:{p.port}");
//类名随意定义,属性与配置项中的名称保持一致
class Proxy
{
public string? address { get; set; }
public int port { get; set; }
}

在上面的代码中,通过IConfigurationRoot的对象config中的GetSection方法来获取配置文件中的proxy节点,然后在调用泛型方法Get,将获取到的proxy节点中的配置项的值就会自动填充到Proxy类中的各个属性中。这样就比前面单独的一个一个的获取方便多了。

下面将Prxoy类中这些属性的值打印出来就可以了。

当然,这里我们也可以将配置文件中的所有属性都读取出来,映射到一个完整的对象中。

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
using Microsoft.Extensions.Configuration;

ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddJsonFile("config.json", optional: false, reloadOnChange: true);
IConfigurationRoot config= configurationBuilder.Build();
/*string name = config["name"]!;
Console.WriteLine($"name={name}");
string address = config.GetSection("proxy:address").Value!;
Console.WriteLine($"$Adress:{address}");
Console.WriteLine(config.GetConnectionString("str"));*/
/*Proxy p = config.GetSection("proxy").Get<Proxy>()!;
Console.WriteLine($"IP地址是:{p.address},端口号:{p.port}");*/
Config c = config.Get<Config>()!;
Console.WriteLine(c.name);
Console.WriteLine(c.proxy?.address);
Console.WriteLine(c.proxy?.port);
Console.WriteLine(c.ConnectionStrings!.str);

//类名随意定义,属性与配置项中的名称保持一致
class Config
{
public string? name { get; set; }
public Proxy? proxy { get; set; }
public ConStr? ConnectionStrings { get; set; }
}
class Proxy
{
public string? address { get; set; }
public int port { get; set; }
}
class ConStr
{
public string? str { get; set; }
}

在上面的代码中,我们首先定义了ConStr类(这个类是随意定义的),定义了str这个属性,该属性的名字一定要与配置文件中ConnectionStrings这个配置节中的str配置项保持名称一致。

然后又定义了Config这个类(类名也是随意定义的),在这个类中添加了name属性,也与配置文件中的name配置节名称相同。

然后又定义了proxy属性,该属性的名字也是与配置文件中的proxy配置节点名称相同。类型是Proxy类。

同时也定义了ConnectionStrings属性,与配置文件中的ConnectionStrings配置节 点名称相同。对应的类型就是ConStr这个类。

最后通过config.Get<Config>()中的Get方法,获取配置文件中的配置信息映射到了Config类中的各个属性中。

3、使用选项方式读取配置

使用选项方式读取配置是.Net Core中推荐的方式,因为它不仅和依赖注入机制能够很好的进行结合,而且它可以实现配置修改以后自动的进行刷新,所以更加的方便。

使用选项方式读取配置需要通过NuGet为项目安装Microsoft.Extensions.Options,并且这种方式也是对绑定方式的封装,因此我们仍然需要安装Microsoft.Extensions.Configuration.Binder这个包。同时前面的配置的基础包也需要安装:

1
2
3
4
Install-Package Microsoft.Extensions.Configuration
Install-Package Microsoft.Extensions.Configuration.Json

Install-Package Microsoft.Extensions.DependencyInjection // 这里读取配置会使用到依赖注入,所以需要安装该包

然后在项目中,在创建一个配置文件appsettings.json,(这里是在解决方案中,重新创建了一个控制台的项目,在该项目中添加该配置文件,同时也选择“较新则复制”)该配置文件中的配置信息如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"DB": {
"DbType": "SQLServer",
"ConnectionString": "Data Source=.;Initial Catalog=TestDB;uid=sa;pwd=123456"
},

"Smtp": {
"Server": "smtp.163.com",
"UserName": "laowang",
"Password": "123"
}


}

对应上面的配置创建两个模型类(在Program.cs中创建)

1
2
3
4
5
6
7
8
9
10
11
12

public class DBSettings
{
public string? DbType { get; set; }
public string? ConnectionString { get;set; }
}
public class SmtpSettings
{
public string? Server { get; set; }
public string? UserName { get; set; }
public string? Password { get; set; }
}

当我们读取以上两个配置类的时候,不能直接在构造函数中声明成员变量(这一点与前面所讲解的DI,通过构造函数注入稍微有区别),这里需要使用IOptions<T>、IOptionsMonitor<T>、IOptionsSnapshot<T>等泛型接口类型,通过这些泛型接口,可以帮我们处理容器生命周期、配置刷新等。

它们的区别是:IOptions<T>在配置改变后,我们不能读到新的值,必须重启程序才可以读到新的值;

IOptionsMonitor<T>在配置改变后,我们能读到新的值;IOptionsSnapshot<T>也是在配置改变后,我们能读到新的值,和IOptionsMonitor<T>不同的是,在同一个范围内IOptionsSnapshot<T>会保持一致性。

通俗地说,在一个范围内(例如一个方法内,获取一次请求中),如果有A、B两处代码都读取了某个配置项,在运行A之后且在运行B之前,这个配置项改变了,那么如果我们用IOptionsMonitor<T>读取配置,在A处读到的将会是旧值,而在B处读到的是新值;如果我们用IOptionsSnapshot<T>读取配置,在A处和B处读到的都是旧值,只有再次进入这个范围才会读到新值。

1
2
3
4
5
6
7
8
read(){
// 该方法的作用是将A表中的数据读取出来,然后删除,在将其插入B表中
string str="数据库链接字符串1";
// 读取A表中的某条数据
// 将其从A表中删除
string str='数据库连接字符串2'
// 将读取出来的数据插入B表
}

例如:在上面的定义的read方法中,如果使用IOptionsMonitor,最开始的时候通过链接字符串1,链接了第一个数据库,然后将A表中的某条数据读取出来,然后将其从A表中删除,还没有将读取出来的数据插入B表,这时候,将配置修改成了数据库链接字符串2,这时候,如果是使用的IOptionsMonitor,这时候,这里获取的配置就是“数据库链接字符串2”,这时候,就会把数据插入到新数据的B表中,这样就导致了业务混乱。因为,我们真正的需求还是将A表中读取出来的数据插入到同一个数据库中的B中。但是使用了IOptionsMonitor以后就导致了业务的混乱。但是如果使用的是IOptionsSnapshot,即使后面将配置修改了,获取到的配置还是以前的,这就比较符合我们的业务需求了。

当然,当再次进入到这个范围,例如请发了一次请求,这时候,IOptionsSnapshot获取的就是最新的配置了。

由于IOptions<T>不监听配置的改变,因此它的资源占用比较少,适用于对服务器启动后就不会改变的值进行读取。由于IOptionsMonitor<T>可能会导致同一个请求过程中,配置的改变使读取同一个选项的值不一致,从而导致程序出错,综上所述,IOptionsSnapshot<T>更符合大部分场景的需求.

因此主要讲解IOptionsSnapshot<T>

在项目中创建ConfigDemo.cs类,该类中的代码如下所示:

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
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp
{
public class ConfigDemo
{
private readonly IOptionsSnapshot<DBSettings> optDbSettings;
private readonly IOptionsSnapshot<SmtpSettings> optSmtpSettings;
public ConfigDemo(IOptionsSnapshot<DBSettings> optDbSettings, IOptionsSnapshot<SmtpSettings> optSmtpSettings)
{
this.optDbSettings = optDbSettings;
this.optSmtpSettings = optSmtpSettings;
}
public void Run()
{
var db = optDbSettings.Value;
Console.WriteLine($"数据是:{db.DbType},链接字符串是:{db.ConnectionString}");
var smtp = optSmtpSettings.Value;
Console.WriteLine($"Smtp:{smtp.Server},{smtp.UserName},{smtp.Password}");
}
}
}

在上面的代码中,我们需要通过 IOptionsSnapshot<T>这个接口来获取配置的信息。当然,这里也是通过ConfigDemo这个构造函数完成该泛型接口的注入。

Run这个函数中,通过 IOptionsSnapshot这个泛型接口中的value属性来获取DbSettings,SmtpSettings这两个模型对象的值。

下面返回到Program.cs文件中进行代码的修改

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
using ConsoleApp;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
//创建ConfigurationBuilder对象,通过该对象中的`AddJSonFile`方法来加载配置文件appsetting.json
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
// 这里加载配置文件,同时这里将optional属性设置false,,这样当文件名或者文件路径写错的时候,出错了,能够及时的发现错误
// 将reloadOnChange属性的值设置为true,这样当配置修改了以后,可以重新加载配置。
configurationBuilder.AddJsonFile("appsetting.json", optional: false, reloadOnChange: true);
// 通过configurationBuilder对象中的Build方法创建IConfigurationRoot对象,后面就可以通过该对象中的GetSection方法来读取配置文件中对应的配置节点。
IConfigurationRoot config=configurationBuilder.Build();
// 创建DI容器
ServiceCollection services = new ServiceCollection();
//AddOptions()方法的作用是把optons这些选项注册到DI中,也就是说将ConfigDemo类中的IOptionsSnapshot<T>泛型接口完成注入
// Bind() :把DBSettings配置对象绑定到config这个IConfigurationRoot这个跟节点中,从而获取获取配置中的DB这个配置节点。
services.AddOptions().Configure<DBSettings>(c => config.GetSection("DB").Bind(c));
services.AddOptions().Configure<SmtpSettings>(s => config.GetSection("Smtp").Bind(s));
services.AddScoped<ConfigDemo>();// 这里也需要将ConfigDemo这个类添加到容器中完成注入。
using (var sp = services.BuildServiceProvider())
{
// 通过GetRequiredService方法获取容器中所注入的ConfigDemo对象,从而完成对Run方法的调用。
var demo= sp.GetRequiredService<ConfigDemo>();
demo.Run();

}
public class DBSettings
{
public string? DbType { get; set; }
public string? ConnectionString { get;set; }
}
public class SmtpSettings
{
public string? Server { get; set; }
public string? UserName { get; set; }
public string? Password { get; set; }
}

测试一下上面的程序。

这里,我们要测试一下当配置文件发生了改变以后,程序能够自动加载最新的配置。

1
2
3
4
5
6
7
8
9
10
11
12
using (var sp = services.BuildServiceProvider())
{
while (true)
{
var demo = sp.GetRequiredService<ConfigDemo>();
demo.Run();
Console.WriteLine("点击任意键继续....");
Console.ReadKey();

}

}

在上面的代码中,我们写了一个死循环,这样当按下任意键的时候,又可以重新读取配置了。

下面修改bin\Debug\net7.0目录下的appsetting.json文件中的配置项,注意:这里一定修改的是bin目录下的appsetting.json配置文件,当项目运行起来的时候,读取的就是该目录下的appsetting.json

但是问题是,当我们修改了appsetting.json中的某个配置项以后,发现控制台中的内容没有变化。

原因是:我们在ConfigDemo类中使用的是IOptionsSnapshot<T>这个泛型接口,该接口会在同一个范围内保持一致。

而当前死循环中就是在当前同一个范围内,并不是一个新的范围。所以每次读取到的配置都是旧的配置。

为了能够读取到新的配置,需要每次循环开启一个新的范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using (var sp = services.BuildServiceProvider())
{
while (true)
{
using (var scope = sp.CreateScope())
{
var spScope= scope.ServiceProvider;
var demo = spScope.GetRequiredService<ConfigDemo>();
demo.Run();

}

Console.WriteLine("点击任意键继续....");
Console.ReadKey();

}

}

这里我们每次循环都调用了CreateScope方法创建出了一个新的范围。

然后再次进行配置的修改,发现在控制台中展示的就是最新的修改后的配置项。

这也就说明了IOptionsSnapshot会在下一次范围内生效,也就是重新进入一个范围后读取的就是新的值。

今日小结

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
53
54
55
56
57
58
59
60
61
62
63
64
65
//推荐使用该方式
//有三种接口可选IOptions<T>、IOptionsMonitor<T>、IOptionsSnapshot<T>
//一般选择第三种(看配置系统.md)
//创建一个新的类来负责打印配置信息(GetAndOutputConfig)

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using 选项方式;

ConfigurationBuilder cb = new ConfigurationBuilder();
cb.AddJsonFile("AppSetting.json",false,true);
/* 1、加载配置文件:
* 首先,我们创建了一个ConfigurationBuilder cb对象,使用cb.AddJsonFile方法加载配置文件
* 就像打开一本食谱书
*/
IConfigurationRoot config = cb.Build();

//创建DI容器,自动创建对象的第一步
ServiceCollection services = new ServiceCollection();
/* 2、创建DI容器:
* 然后,创建了DI容器。这个容器就像一个厨房,里面装满了各种工具和调料
*/


//AddOptions()方法的作用是把optons这些选项注册到DI中,
//也就是说将ConfigDemo类中的IOptionsSnapshot<T>泛型接口完成注入
//上翻译成下
//AddOptions() 方法将选项模式 (Options Pattern) 的支持注册到依赖注入 (DI) 容器中,
//使得应用程序可以使用 IOptionsSnapshot<T> 泛型接口来获取配置项,并将这些配置项注入到 ConfigDemo 类中。
//上再翻译成下
//要想让GetAndOutputConfigC#文件自动通过DI容器创建对象,则需要将你所想要创建的对象注册在DI容器中
//通过services.AddOptions().Configure<DBSettings>这种方法
services.AddOptions().Configure<DBSettings>(c => config.GetSection("DB").Bind(c));
services.AddOptions().Configure<SmtpSettings>(c => config.GetSection("Smtp").Bind(c));
/* 3、绑定配置到配置类:
* 告诉厨房我要喝咖啡(DBSettings),从食谱书中找到咖啡食谱
* 告诉厨房我要喝奶茶(SmtpSettings),从食谱书中找到奶茶食谱
*/

services.AddScoped<GetAndOutputConfig>();
/* 4、注册服务
* 把GetAndOutputConfig类注册到厨房里
* 需要制作咖啡时,让厨房提供一个GetAndOutputConfig对象
*/

using (var sp = services.BuildServiceProvider())
{
var GAO = sp.GetRequiredService<GetAndOutputConfig>();
GAO.outputConfig();
}
/* 5、运行程序:
* 最后,我们从厨房里要了一个GetAndOutputConfig对象,执行了它的outputConfig方法,
* 这个方法会取咖啡配方和奶茶配方,然后打印出来。
*/
public class DBSettings
{
public string? DbType { get; set; }
public string? ConnectionString { get; set; }
}
public class SmtpSettings
{
public string? Server { get; set; }
public string? UserName { get; set; }
public string? Password { get; set; }
}
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
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace 选项方式
{
public class GetAndOutputConfig
{
//通过构造函数注入对象
private readonly IOptionsSnapshot<DBSettings>? _OptDBSrttings;
private readonly IOptionsSnapshot<SmtpSettings>? _OptSmtpSettings;
public GetAndOutputConfig(IOptionsSnapshot<DBSettings>? _OptDBSrttings, IOptionsSnapshot<SmtpSettings>? _OptSmtpSettings)
{
this._OptDBSrttings = _OptDBSrttings;
this._OptSmtpSettings = _OptSmtpSettings;
}
public void outputConfig()
{
var db = _OptDBSrttings.Value;
Console.WriteLine($"数据是:{db.DbType},链接字符串是:{db.ConnectionString}");
var smtp = _OptSmtpSettings.Value;
Console.WriteLine($"Smtp:{smtp.Server},{smtp.UserName},{smtp.Password}");
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
{
"DB": {
"DbType": "SQLServer",
"ConnectionString": "Data Source=.;Initial Catalog=TestDB;uid=sa;pwd=123456"
},

"Smtp": {
"Server": "smtp.163.com",
"UserName": "laowang",
"Password": "123"
}
}