ASP.NET Core 6框架揭秘实例演示[08]:配置的基本编程模式
.NET的配置支持多样化的数据源,我们可以采用内存的变量、环境变量、命令行参数、以及各种格式的配置文件作为配置的数据来源。在对配置系统进行系统介绍之前,我们通过几个简单的实例演示一下如何将具有不同来源的配置数据构建为一个统一的配置对象,并以相同的方式读取具体配置节的内容。(本篇提供的实例已经汇总到《ASP.NET Core 6框架揭秘-实例演示版》)
[501]以键值对形式读取配置(源代码)
[502]读取结构化配置(源代码)
[503]将结构化配置绑定为对象(源代码)
[504]将配置定义在JSON文件中(源代码)
[505]根据环境动态加载配置文件(源代码)
[506]配置内容的实时同步(源代码)
[501]以键值对形式读取配置
“原子”配置项体现为一个键值对形式,并且键和值通常都是字符串。假设我们需要通过配置来设定日期/时间的显示格式,我们为此定义了如下这个DateTimeFormatOptions类型,它的四个属性体现了针对DateTime类型的四种显示格式(分别为长日期/时间和短日期/时间)。
public class DateTimeFormatOptions { ... public string LongDatePattern { get; set; } public string LongTimePattern { get; set; } public string ShortDatePattern { get; set; } public string ShortTimePattern { get; set; } }
我们为该类型定义了一个参数类型为IConfiguration接口的构造函数,IConfiguration对象提供的索引使我们可以采用键值对的形式读取每个配置节的值,下面的代码正是以索引的方式得到对应配置并对DateTimeFormatOptions对象的四个属性赋值。
public class DateTimeFormatOptions { ... public DateTimeFormatOptions (IConfiguration config) { LongDatePattern = config["LongDatePattern"]; LongTimePattern = config["LongTimePattern"]; ShortDatePattern = config["ShortDatePattern"]; ShortTimePattern = config["ShortTimePattern"]; } }
正如前面所述,IConfiguration对象是由IConfigurationBuilder对象构建的,而原始的配置信息则是通过相应的IConfigurationSource对象提供的,所以创建一个IConfiguration对象的正确编程方式如下:创建一个ConfigurationBuilder(IConfigurationBuilder接口的默认实现类型)对象,并为之注册一个或者多个IConfigurationSource对象,最后利用它来创建我们需要的IConfiguration对象。简单起见,我们采用的IConfigurationSource实现类型为MemoryConfigurationSource,它直接利用一个保存在内存中的字典对象作为最初的配置来源。
using App; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Memory; var source = new Dictionary<string, string> { ["longDatePattern"] = "dddd, MMMM d, yyyy", ["longTimePattern"] = "h:mm:ss tt", ["shortDatePattern"] = "M/d/yyyy", ["shortTimePattern"] = "h:mm tt" }; var config = new ConfigurationBuilder() .Add(new MemoryConfigurationSource { InitialData = source }) .Build(); var options = new DateTimeFormatOptions(config); Console.WriteLine($"LongDatePattern: {options.LongDatePattern}"); Console.WriteLine($"LongTimePattern: {options.LongTimePattern}"); Console.WriteLine($"ShortDatePattern: {options.ShortDatePattern}"); Console.WriteLine($"ShortTimePattern: {options.ShortTimePattern}");
如上面的代码片段所示,我们创建了一个ConfigurationBuilder对象,并在它上面注册了一个基于内存字典的MemoryConfigurationSource对象。我们接下来利用ConfigurationBuilder对象的Build方法构建IConfiguration对象来创建DateTimeFormatOptions对象。为了验证该Options对象是否与原始配置数据一致,我们将它的四个属性打印在控制台上。程序运行之后,控制台上的输出结果如图1所示)。
[502]读取结构化配置
配置大都具有结构化的层次结构,所以IConfiguration对象同样具有这样的结构。我们姑且将保持树形层次化结构的配置称为“配置树”,一个IConfiguration对象正好是对这棵配置树的某个节点的描述,而整棵配置树则可以由根节点对应的IConfiguration对象来表示。下面以实例来演示如何定义并读取具有层次结构的配置数据。我们依然沿用上一个实例的应用场景,但现在不仅需要设置日期/时间的格式,还需要设置其他数据类型的格式,如表示货币的Decimal类型。因此我们定义了一个CurrencyDecimalFormatOptions类,它的Digits和Symbol属性分别表示小数位数与货币符号,CurrencyDecimalFormatOptions对象依然是利用IConfiguration对象创建的。
public class CurrencyDecimalFormatOptions { public int Digits { get; set; } public string Symbol { get; set; } public CurrencyDecimalFormatOptions (IConfiguration config) { Digits = int.Parse(config["Digits"]); Symbol = config["Symbol"]; } }
我们定义了如下的FormatOptions类型将两种配置整合在一起,它的DateTime和CurrencyDecimal属性分别表示针对日期/时间与货币数字的格式设置。FormatOptions依然具有一个参数类型为IConfiguration的构造函数,它的两个属性均在此构造函数中被初始化。值得注意的是,初始化这两个属性采用的是调用这个IConfiguration对象的GetSection方法提取的“子配置节”。
public class FormatOptions { public DateTimeFormatOptions DateTime { get; set; } public CurrencyDecimalFormatOptions CurrencyDecimal { get; set; } public FormatOptions (IConfiguration config) { DateTime = new DateTimeFormatOptions (config.GetSection("DateTime")); CurrencyDecimal = new CurrencyDecimalFormatOptions (config.GetSection("CurrencyDecimal")); } }
FormatOptions类型体现的配置具有图2所示的树形层次结构。在前面演示的实例中,我们使用MemoryConfigurationSource对象来提供原始的配置信息,承载原始配置信息的是一个元素类型为KeyValuePair<string, string>的集合,但是它在物理存储上并不具有树形层次结构,那么它如何提供一个结构化的IConfiguration对象承载的数据?
图2 树形层次结构的配置
对于一棵完整的配置树,具体的配置信息存储叶子节点上,所以MemoryConfigurationSource对象只需要在配置字典中保存叶子节点的数据即可。为了描述配置树的结构,配置字典还需要将对应叶子节点在配置树中的路径作为Key。所以MemoryConfigurationSource可以采用表1列举的配置字典对配置树进行扁平化处理。
表1 配置的物理结构
Key | Value |
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 | $ |
下面的演示程序按照表1列举的结构创建了一个Dictionary<string, string>对象,并将其作为参数调用IConfigurationBuilder接口的AddInMemoryCollection扩展方法,该方法会根据提供的字段对象创建对应的了MemoryConfigurationSource对象并进行注册。在得到IConfiguration对象之后,我们调用其GetSection方法提取出“Format”配置节,并利用它将FormatOptions对象创建出来。
using App; using Microsoft.Extensions.Configuration; 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 configuration = new ConfigurationBuilder() .AddInMemoryCollection(source) .Build(); var options = new FormatOptions(configuration.GetSection("Format")); 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}");
在得到利用读取的配置创建的 FormatOptions对象之后,为了验证该对象与原始配置数据是否一致,我们依然将它的相关属性打印在控制台上。这个程序运行之后在控制台上呈现的输出结果如图3所示。
[503]将结构化配置绑定为对象
在前面的实例中,为了创建三个Options对象,我们不得不以键值对的方式从IConfiguration对象中读取每个配置节的值,如果定义的配置项太多,逐条读取配置项其实是一项非常烦琐的工作。如果承载配置数据的IConfiguration对象与对应的Options类型具有兼容的结构,那么利用配置的自动绑定机制可以将IConfiguration对象直接转换成对应的Options对象。配置绑定相应的API定义在“Microsoft.Extensions.Configuration.Binder”这个NuGet包中,
在添加了上述这个NuGet包引用之后,我们删除了三个Options类型的构造函数,然后将演示程序改写成如下的形式。如代码片段所示,在构建出IConfiguration对象之后,我们其调用GetSection方法提取出“Format”配置节,最终的FormatOptions对象直接调用该配置节的Get<T>方法生成出来。修改后的程序运行之后,同样会得到图5-4所示的输出结果。
... var options = new ConfigurationBuilder() .AddInMemoryCollection(source) .Build() .GetSection("Format") .Get<FormatOptions>(); ...
[504]将配置定义在JSON文件中
前面演示的三个实例都是采用MemoryConfigurationSource类型的配置源,我们下来演示JSON配置文件的使用。我们在项目根目录下创建一个名为“appsettings.json”的配置文件,并在其中定义了如下的配置。我们将该文件的“Copy to Output Directory”属性设置为“Copy always”(如果项目采用的SDK类型为“Microsoft .NET.Sdk”,该应用在Visual Studio中运行时会将编译输出目录作为当前目录。如果项目采用的SDK类型为 “Microsoft .NET.Sdk.Web”,那么项目根目录就是当前执行的目录,此时不需要设置配置文件的 “Copy to Output Directory” 属性。),其目的是为了让该文件在编译的时候自动复制到输出目录。
{ "format": { "dateTime": { "longDatePattern": "dddd, MMMM d, yyyy", "longTimePattern": "h:mm:ss tt", "shortDatePattern": "M/d/yyyy", "shortTimePattern": "h:mm tt" }, "currencyDecimal": { "digits": 2, "symbol": "$" } } }
基于JSON文件的配置源通过JsonConfigurationSource类型来表示。JsonConfigurationSource类型定义在“Microsoft.Extensions.Configuration.Json”这个NuGet包中,所以我们需要为演示程序添加该包的引用。我们不需要手动创建这个JsonConfigurationSource对象,只需要按照如下的方式调用IConfigurationBuilder接口的AddJsonFile扩展方法添加指定的JSON文件即可。执行修改后的程序,我们依然可以得到图3所示的输出结果。
var options = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .Build() .GetSection("format") .Get<FormatOptions>(); ...
[505]根据环境动态加载配置文件
配置内容往往取决于应用当前执行的环境,不同的执行环境(开发、测试、预发和产品等)会采用不同的配置。如果采用基于物理文件的配置,我们可以为不同的环境提供对应的配置文件,具体的做法如下:除了提供一个基础配置文件(如appsettings.json),我们还需要为相应的环境提供对应的差异化配置文件,后者通常采用环境名称作为文件扩展名(如appsettings.production.json)。以目前演示的程序为例,现有的配置文件appsettings.json可以作为基础配置文件,如果某个环境需要采用不同的配置,需要将差异化的配置定义在环境对应的文件中。如图4所示,我们额外添加了两个配置文件(appsettings.staging.json和appsettings.production.json),从文件命名可以看出这两个配置文件分别对应预发环境和产品环境。
图4 针对执行环境的配置文件
我们在JSON文件中定义了针对日期/时间和货币格式的配置,假设预发环境和产品环境需要采用不同的货币格式,那么就需要将差异化的配置定义在针对环境的两个配置文件中。简单起见,我们仅仅将货币的小数位数定义在配置文件中。如下面的代码片段所示,货币小数位数(默认值为2)在预发环境和产品环境中分别被设置为3与4。
appsettings.staging.json:
{ "format": { "currencyDecimal": { "digits": 3 } } }
appsettings.production.json:
{ "format": { "currencyDecimal": { "digits": 4 } } }
为了在演示过程中能够灵活地进行环境切换,可以采用命令行参数(如/env staging)来设置环境。到目前为止,针对某一环境的配置被分布到两个配置文件中,所以在启动文件时就应该根据当前执行环境动态地加载对应的配置文件。如果两个文件涉及同一段配置,就应该首选当前环境对应的那个配置文件。由于配置默认采用“后来居上”的原则,所以应该先加载基础配置文件,再加载针对环境的配置文件。针对执行环境的判断以及针对环境的配置加载体现在如下所示的代码片段中。
using App; using Microsoft.Extensions.Configuration; 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方法将两个配置文件加载进来,两个文件合并后的内容将用于构建最终的IConfiguration对象。我们以命令行的形式启动这个控制台程序,并通过命令行参数指定相应的环境名称。从图5所示的输出结果可以看出,打印出的配置数据(货币的小数位数)确实来源于环境对应的配置文件。
图5 输出与当前环境匹配的配置
[506]配置内容的实时同步
.NET的配置模型提供了针对配置源的监控功能,它能保证一旦原始配置改变之后应用程序能够及时接收到通知,此时我们可以利用预先注册的回调进行配置的同步。前面演示的应用程序采用JSON文件作为配置源,我们希望应用程序能够感知该文件的改变,并在发生改变的时候将新的配置应用到程序之中。为了演示配置的同步,我们对程序做了如下改变。
using App; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Primitives; 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();
如上面的代码片段所示,我们在调用IConfigurationBuilder接口的AddJsonFile扩展方法时将reloadOnChange参数设置为True,进而开启在文件更新的时候自动重新加载的功能。在IConfiguration对象成功构建之后,我们调用它的GetReloadToken方法并利用返回的IChangeToken对象来感知配置源的变化的。一旦配置源发生变化,IConfiguration对象将自动加载新的内容并“自我刷新”。上述程序会在感知到配置源发生变化后自动将新的配置内容打印出来。图6中的输出结果是两次修改货币小数位数导致的。
图6 配置文件更新触发配置的重新加载