配置系统 在专业的软件项目中,一些配置项的值应该是可以修改的,我们不应该把这些值硬编码到代码中,.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" ));
以上就是读取配置文件的基本操作。
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(); 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(); 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(){ string str="数据库链接字符串1" ; string str='数据库连接字符串2' }
例如:在上面的定义的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 configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddJsonFile("appsetting.json" , optional: false , reloadOnChange: true ); IConfigurationRoot config=configurationBuilder.Build(); ServiceCollection services = new ServiceCollection(); services.AddOptions().Configure<DBSettings>(c => config.GetSection("DB" ).Bind(c)); services.AddOptions().Configure<SmtpSettings>(s => config.GetSection("Smtp" ).Bind(s)); services.AddScoped<ConfigDemo>(); using (var sp = services.BuildServiceProvider()){ 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 using Microsoft.Extensions.Configuration;using Microsoft.Extensions.DependencyInjection;using 选项方式;ConfigurationBuilder cb = new ConfigurationBuilder(); cb.AddJsonFile("AppSetting.json" ,false ,true ); IConfigurationRoot config = cb.Build(); ServiceCollection services = new ServiceCollection(); services.AddOptions().Configure<DBSettings>(c => config.GetSection("DB" ).Bind(c)); services.AddOptions().Configure<SmtpSettings>(c => config.GetSection("Smtp" ).Bind(c)); services.AddScoped<GetAndOutputConfig>(); using (var sp = services.BuildServiceProvider()){ var GAO = sp.GetRequiredService<GetAndOutputConfig>(); GAO.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" } }