[接上篇]提到“配置”二字,我想绝大部分.NET开发人员脑海中会立即浮现出两个特殊文件的身影,那就是我们再熟悉不过的app.config和web.config,多年以来我们已经习惯了将结构化的配置定义在这两个XML格式的文件之中。到了.NET Core的时代,很多我们习以为常的东西都发生了改变,其中就包括定义配置的方式。总的来说,新的配置系统显得更加轻量级,并且具有更好的扩展性,其最大的特点就是支持多样化的数据源。我们可以采用内存的变量作为配置的数据源,也可以将配置定义在持久化的文件甚至数据库中。在对配置系统进行系统介绍之前,我们先从编程的角度来体验一下全新的配置读取方式。
在真正的项目开发过程中,我们倾向于像我们演示的实例一样将一组相关的配置转换成一个POCO对象,比如演示实例中的DateTimeFormatOptions、CurrencyDecimalOptions和FormatOptions对象。在前面演示的实例中,为了创建这些封装配置的对象,我们都是采用手工读取配置的形式。如果定义的配置项太多的话,逐条读取配置项其实是一项非常繁琐的工作。
如果承载配置数据的IConfiguration对象与对应的POCO类型具有兼容的结构,我们利用配置的自动绑定机制可以将IConfiguration对象直接转换成对应的POCO对象。对于我们演示的这个实例来说,如果采用自动化配置绑定来创建对应的Options对象,那么这些类型中实现手工绑定的构造函数就不再需要了。
在删除所有Options类型的构造函数之后,我们修改Options对象的创建方式。如下面的代码片段所示,在调用IConfigurationBuilder的Build方法创建出对应IConfiguration对象之后,我们调用GetSection方法得到其“format”配置节,而FormatOptions对象不用再通过调用构造函数来创建,而是直接调用该配置节的Get<T>方法,该方法完成了从IConfiguration到POCO对象之间的自动化绑定。
public class Program
{
public static void Main()
{
var source = new Dictionary<string, string>
{
["format:dateTime:longDatePattern"] = "dddd, MMMM d, yyyy",
["format:dateTime:longTimePattern"] = "h:mm:ss tt",
["format:dateTime:shortDatePattern"] = "M/d/yyyy",
["format:dateTime:shortTimePattern"] = "h:mm tt",
["format:currencyDecimal:digits"] = "2",
["format:currencyDecimal:symbol"] = "$",
};
var options = new ConfigurationBuilder()
.Add(new MemoryConfigurationSource { InitialData = source })
.Build()
.GetSection("format")
.Get<FormatOptions>();
var dateTime = options.DateTime;
var currencyDecimal = options.CurrencyDecimal;
Console.WriteLine("DateTime:");
Console.WriteLine($"\tLongDatePattern: {dateTime.LongDatePattern}");
Console.WriteLine($"\tLongTimePattern: {dateTime.LongTimePattern}");
Console.WriteLine($"\tShortDatePattern: {dateTime.ShortDatePattern}");
Console.WriteLine($"\tShortTimePattern: {dateTime.ShortTimePattern}");
Console.WriteLine("CurrencyDecimal:");
Console.WriteLine($"\tDigits:{currencyDecimal.Digits}");
Console.WriteLine($"\tSymbol:{currencyDecimal.Symbol}");
}
}
修改后的程序运行之后,我们同样会得到如下图所示的输出结果。
前面演示的三个实例都是采用 MemoryConfigurationSource将一个字典对象作为配置源,接下来我们演示一种更加常见的配置定义方法,那就是将原始配置的内容定义在一个JSON文件中。我们将原本通过一个内存字典对象承载的配置定义在一个JSON文件中,为此我们在项目的根目录下创建一个名为“appsettings.json”的配置文件,并将该文件的“Copy to Output Directory”属性设置为“Copy always”,其目的是促使项目在编译的时候能够将此文件拷贝到输出目录下。我们采用如下的形式定义关于日期/时间和货币的格式配置。
{
"format": {
"dateTime": {
"longDatePattern" : "dddd, MMMM d, yyyy",
"longTimePattern" : "h:mm:ss tt",
"shortDatePattern" : "M/d/yyyy",
"shortTimePattern": "h:mm tt"
},
"currencyDecimal": {
"digits": 2,
"symbol": "$"
}
}
}
由于配置源发生了改变,原来的MemoryConfigurationSource需要替换成JsonConfigurationSource,不过我们不需要手工创建这个JsonConfigurationSource对象,只需要调用IConfigurationBuilder接口的扩展方法AddJsonFile添加指定的JSON文件即可。执行修改后的程序,我们依然会得到如上图所示的输出结果。
public class Program
{
public static void Main()
{
var options = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build()
.GetSection("format")
.Get<FormatOptions>();
var dateTime = options.DateTime;
var currencyDecimal = options.CurrencyDecimal;
Console.WriteLine("DateTime:");
Console.WriteLine($"\tLongDatePattern: {dateTime.LongDatePattern}");
Console.WriteLine($"\tLongTimePattern: {dateTime.LongTimePattern}");
Console.WriteLine($"\tShortDatePattern: {dateTime.ShortDatePattern}");
Console.WriteLine($"\tShortTimePattern: {dateTime.ShortTimePattern}");
Console.WriteLine("CurrencyDecimal:");
Console.WriteLine($"\tDigits:{currencyDecimal.Digits}");
Console.WriteLine($"\tSymbol:{currencyDecimal.Symbol}");
}
}
真实项目开发过程中使用的配置往往决定于应用当前执行的环境,也就是说不同的执行环境(开发、测试、预发和产品等)会采用不同的配置。如果采用基于物理文件的配置,我们可以为不同的环境提供对应的配置文件,具体的做法是:除了提供一个“基础配置文件”(比如“appsettings.json”)之外,我们还需为相应的环境提供对应的“差异化”配置文件,后者通常采用环境名称作为文件扩展名(比如“appsettings.production.json”)。
以我们目前演示的这个程序为例,现有的这个配置文件appsettings.json可以作为基础配置文件,如果某个环境需要采用不同的配置,我们可以将差异化的配置定义在对应的文件中。如下图所示,我们额外添加了两个配置文件(appsettings.staging.json和appsettings.production.json),从文件命名我们不难看出它们分别对应的是预发和产品环境。
我们在JSON文件中定义了针对日期/时间和货币格式的配置,假设预发环境和产品环境需要采用不同的货币格式,那么我们需要将差异化的配置定义在针对环境的两个配置文件中就可以了。简单起见,我们仅仅将货币的小数位数定义在配置文件中。如下面的代码片段所示,货币小数位数(默认值为2)在预发和产品环境分别被设置为3和4。
appsettings.staging.json:
{
"format": {
"currencyDecimal": {
"digits": 3
}
}
}
appsettings.production.json:
{
"format": {
"currencyDecimal": {
"digits": 4
}
}
}
一般来说,我们会采用环境变量来决定应用的执行环境,但是为了在演示过程中能够灵活地进行环境切换,我们采用命令行参数(比如“/env staging”)的形式来设置环境。到目前为止,针对某一环境的配置被分布到两个配置文件中,那么我们在启动文件的时候就应该根据当前执行环境动态地加载对应的配置文件。如果两个文件涉及到同一段配置,应该首选当前环境对应的那个配置文件。由于配置默认采用“后来居上”的原则,所以应该先加载基础配置文件,再加载针对环境的配置文件。针对执行环境的判断以及针对环境的配置加载体现在如下所示的代码片段中。
class Program
{
static void Main(string[] args)
{
var index = Array.IndexOf(args, "/env");
var environment = index > -1
? args[index + 1]
: "Development";
var options = new ConfigurationBuilder()
.AddJsonFile("appsettings.json",false)
.AddJsonFile($"appsettings.{environment}.json",true)
.Build()
.GetSection("format")
.Get<FormatOptions>();
...
}
}
如上面的代码片段所示,在利用传入的命令行参数确定了当前执行环境之后,我们先后两次调用了IConfigurationBuilder对象的AddJsonFile方法将两个配置文件加载进来,那么两个文件合并后的内容将用于构建Build方法创建的IConfiguration对象。接下来我们以命令行的形式启动这个控制台程序,并通过命令行参数指定相应的环境名称。从如图6-6所示的输出结果可以看出打印出来的配置数据(货币的小数位数)确实来源于环境对应的配置文件。(S605)
很多情况下应用程序的配置只会在启动的时候从相应的配置源中读取,并在整个应用的生命周期中保持不变,一旦我们需要重修更新配置,我们不得不重新启动应用程序。.NET Core的配置模型提供了针对配置源的监控功能,它能保证一旦原始的配置改变之后应用程序能够及时接收到通知,此时我们可以利用预先注册的回调进行配置的同步。
我们演示的应用程序采用JSON文件作为配置源,所以我们希望应用程序能够感知到该文件的改变,并在文件发生改变的时候自动加载新的配置比将其重新应用到程序之中。为了演示配置的同步,我们对程序做了如下的改变。
class Program
{
static void Main()
{
var config = new ConfigurationBuilder()
.AddJsonFile(path: "appsettings.json",optional:true,reloadOnChange: true)
.Build();
ChangeToken.OnChange(() => config.GetReloadToken(), () =>
{
var options = config.GetSection("format").Get<FormatOptions>();
var dateTime = options.DateTime;
var currencyDecimal = options.CurrencyDecimal;
Console.WriteLine("DateTime:");
Console.WriteLine($"\tLongDatePattern: {dateTime.LongDatePattern}");
Console.WriteLine($"\tLongTimePattern: {dateTime.LongTimePattern}");
Console.WriteLine($"\tShortDatePattern: {dateTime.ShortDatePattern}");
Console.WriteLine($"\tShortTimePattern: {dateTime.ShortTimePattern}");
Console.WriteLine("CurrencyDecimal:");
Console.WriteLine($"\tDigits:{currencyDecimal.Digits}");
Console.WriteLine($"\tSymbol:{currencyDecimal.Symbol}\n\n");
});
Console.Read();
}
}
表示JSON文件配置源的JsonConfigurationSource在默认的情况下并不会监控源文件的变化,所以我们需要在调用IConfigurationBuilder的扩展方法AddJsonFile的时候,通过传入的reloadOnChange参数开启这个功能。通过IConfigurationBuilder的Build方法创建的IConfiguration对象具有一个返回类型为IChangeToken的GetReloadToken方法,我们正是利用它返回的IChangeToken来感知配置源的变化。一旦配置源发生变化,IConfiguration对象将自动加载新的内容,所以我们只需要通过注册的回调将同一个IConfiguration对象应用到程序之中就可以。
我们的程序会在感知到配置源变化后自动将新的配置内容打印出来,所以当该程序被启动之后,我们对appsettings.json文件所做的任何修改都会触发应用对该文件的重新加载。下图所示的输出是我们两次修改货币小数位数导致的。
[ASP.NET Core 3框架揭秘] 配置[1]:读取配置数据[上篇]
[ASP.NET Core 3框架揭秘] 配置[2]:读取配置数据[下篇]
[ASP.NET Core 3框架揭秘] 配置[3]:配置模型总体设计
[ASP.NET Core 3框架揭秘] 配置[4]:将配置绑定为对象
[ASP.NET Core 3框架揭秘] 配置[5]:配置数据与数据源的实时同步
[ASP.NET Core 3框架揭秘] 配置[6]:多样化的配置源[上篇]
[ASP.NET Core 3框架揭秘] 配置[7]:多样化的配置源[中篇]