提到“配置”二字,我想绝大部分.NET开发人员脑海中会立即浮现出两个特殊文件的身影,那就是我们再熟悉不过的app.config和web.config,多年以来我们已经习惯了将结构化的配置定义在这两个XML格式的文件之中。到了.NET Core的时代,很多我们习以为常的东西都发生了改变,其中就包括定义配置的方式。总的来说,新的配置系统显得更加轻量级,并且具有更好的扩展性,其最大的特点就是支持多样化的数据源。我们可以采用内存的变量作为配置的数据源,也可以将配置定义在持久化的文件甚至数据库中。在对配置系统进行系统介绍之前,我们先从编程的角度来体验一下全新的配置读取方式。
就编程层面来讲,.NET Core的配置系统由如下图所示的三个核心对象构成。读取出来的配置信息最终会转换成一个IConfiguration对象供应用程序使用。IConfigurationBuilder是IConfiguration对象的构建者,而IConfigurationSource则代表配置数据最原始的来源。
在读取配置的时候,我们根据配置的定义方式(数据源)创建相应的IConfigurationSource对象,并将其注册到IConfigurationBuilder对象上。提供配置的最初来源可能不止一个,我们可以注册多个相同或者不同类型的IConfigurationSource对象到同一个IConfigurationBuilder对象上。IConfigurationBuilder对象正是利用注册的这些IConfigurationSource对象提供的数据构建出我们在程序中使用的IConfiguration对象。
这里介绍的IConfiguration、IConfigurationSource和IConfigurationBuilder接口以及其他一些基础类型均定义在NuGet包“Microsoft.Extensions.Configuration.Abstractions”中。对这些接口的默认实现,则大多定义在“Microsoft.Extensions.Configuration”这个NuGet包中。
虽然大部分情况下的配置从整体来说都具有结构化层次关系,但是“原子”配置项都以体现为最简单的“键值对”形式,并且键和值通常都是字符串。接下来我们会通过一个简单的实例来演示如何以键值对的形式来读取配置。
假设我们的应用程序需要通过配置来设定日期/时间的显示格式,为此我们将相关的配置信息定义在如下所示的这个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; }
}
我们希望通过配置的形式来控制由DateTimeFormatOptions的四个属性所体现的显示格式,所以我们为它定义了一个构造函数。如下面的代码片段所示,该构造函数具有一个IConfiguration接口类型的参数。键值对是配置的基本表现形式,所以IConfiguration对象提供了索引使我们可以根据配置项的Key得到配置项的值,下面的代码正是以索引的方式得到对应配置信息的。
public class DateTimeFormatOptions
{
...
public DateTimeFormatOptions (IConfiguration config)
{
LongDatePattern = config["LongDatePattern"];
LongTimePattern = config["LongTimePattern"];
ShortDatePattern = config["ShortDatePattern"];
ShortTimePattern = config ["ShortTimePattern"];
}
}
要创建一个体现当前配置的DateTimeFormatOptions对象,我们必须提供这个承载相关配置信息的IConfiguration对象。正如我们前面所说,IConfiguration对象是由IConfigurationBuilder对象创建的,而原始的配置信息则是通过相应的IConfigurationSource对象来提供,所以创建一个IConfiguration对象的正确编程方式是:创建一个ConfigurationBuilder(IConfigurationBuilder接口的默认实现类型)对象并为之注册一个或者多个IConfigurationSource对象,最后利用它来创建我们需要的IConfiguration对象。
我们通过如下的程序来读取配置并将其转换成一个DateTimeFormatOptions对象。简单起见,我们采用的IConfigurationSource实现类型为MemoryConfigurationSource,它直接利用一个保存在内存中的字典对象作为最初的配置来源。如下面的代码片段所示,我们在为MemoryConfigurationSource提供的字典对象中设置了四种类型的日期/时间显示格式。
public class Program
{
public static void Main()
{
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对象是否与原始的配置一致,我们将它的四个属性打印在控制台上。程序运行之后,控制台上将会产生如下所示的输出结果。
真实项目中涉及的配置大都具有结构化的层次结构,所以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方法根据指定的名称(“DateTime”和“CurrencyDecimal”)获得这两个子配置节。
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类型体现的配置具有如图6-3所示的树形层次结构。在我们前面演示的实例中,我们使用一个MemoryConfigurationSource对象来提供原始的配置信息。由于承载原始配置信息的是一个元素类型为KeyValuePair<string, string>的集合,它在物理存储上并不具有树形化的层次结构,那么它如何能够提供一个结构化的IConfiguration对象承载的数据呢?
解决方案其实很简单,对于一棵完整的配置树,具体的配置信息最终是通过叶子节点来承载的,所以MemoryConfigurationSource只需要在配置字典中保存叶子节点的数据即可。除此之外,为了描述配置树的结构,配置字典需要将对应叶子节点在配置树中的路径作为Key。所以MemoryConfigurationSource可以采用下表6-1所示的配置字典对配置树进行“扁平化”,作为Key的路径采用冒号(“:”)作为分隔符。
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 | $ |
如下面的代码片段所示,我们按照表6-1所示的结构创建了一个Dictionary<string, string>对象,并利用它创建出MemoryConfigurationSource对象。在利用ConfigurationBuilder得到IConfiguration对象之后,我们调用其GetSection方法得到名称为“Format”的配置节,并利用后者创建一个FormatOptions。
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 configuration = new ConfigurationBuilder()
.Add(new MemoryConfigurationSource { InitialData = 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对象之后,为了验证该对象与原始配置数据是否一致,我们依然将它的相关属性打印在控制台上。这个程序运行之后会在控制台上呈现如下所示的输出结果。
[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]:多样化的配置源[中篇]