提到“配置”二字,我想绝大部分.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的路径采用冒号(“:”)作为分隔符。
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}"); } }
四、将结构化配置直接绑定为对象
在真正的项目开发过程中,我们倾向于像我们演示的实例一样将一组相关的配置转换成一个POCO对象,比如演示实例中的DateTimeFormatOptions、CurrencyDecimalOptions和FormatOptions对象。在前面演示的实例中,为了创建这些封装配置的对象,我们都是采用手工读取配置的形式。如果定义的配置项太多的话,逐条读取配置项其实是一项非常繁琐的工作。
如果承载配置数据的IConfiguration对象与对应的POCO类型具有兼容的结构,我们利用配置的自动绑定机制可以将IConfiguration对象直接转换成对应的POCO对象。对于我们演示的这个实例来说,如果采用自动化配置绑定来创建对应的Options对象,那么这些类型中实现手工绑定的构造函数就不再需要了。
在删除所有Options类型的构造函数之后,我们修改Options对象的创建方式。如下面的代码片段所示,在调用IConfigurationBuilder的Build方法创建出对应IConfiguration对象之后,我们调用GetSection方法得到其“format”配置节,而FormatOptions对象不用再通过调用构造函数来创建,而是直接调用该配置节的Get<T>方法,该方法完成了从IConfiguration到POCO对象之间的自动化绑定。
修改后的程序运行之后,我们同样会得到如下图所示的输出结果。
五、将配置定义在文件中
前面演示的三个实例都是采用 MemoryConfigurationSource将一个字典对象作为配置源,接下来我们演示一种更加常见的配置定义方法,那就是将原始配置的内容定义在一个JSON文件中。我们将原本通过一个内存字典对象承载的配置定义在一个JSON文件中,为此我们在项目的根目录下创建一个名为“appsettings.json”的配置文件,并将该文件的“Copy to Output Directory”属性设置为“Copy always”,其目的是促使项目在编译的时候能够将此文件拷贝到输出目录下。我们采用如下的形式定义关于日期/时间和货币的格式配置。
public class Program
{
public static void Main()
{
var options = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")// 注意 此方法在 Microsoft.Extensions.Configuration.Json中 原文中并没有说明
.Build()
.GetSection("format")
.Get<FormatOptions>(); //注意 此方法在 Microsoft.Extensions.Configuration.Binder中 原文中并没有说明
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": 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();
//运行程序 然后修改bin目录下面的 appsettings.json 其中配置 就会在控制台输出最新的配置
//如果 reloadOnChange: 为false 修改配置文件 则不会触发更新配置 此时需要重启程序
}
}
表示JSON文件配置源的JsonConfigurationSource在默认的情况下并不会监控源文件的变化,所以我们需要在调用IConfigurationBuilder的扩展方法AddJsonFile的时候,通过传入的reloadOnChange参数开启这个功能。通过IConfigurationBuilder的Build方法创建的IConfiguration对象具有一个返回类型为IChangeToken的GetReloadToken方法,我们正是利用它返回的IChangeToken来感知配置源的变化。一旦配置源发生变化,IConfiguration对象将自动加载新的内容,所以我们只需要通过注册的回调将同一个IConfiguration对象应用到程序之中就可以。
我们的程序会在感知到配置源变化后自动将新的配置内容打印出来,所以当该程序被启动之后,我们对appsettings.json文件所做的任何修改都会触发应用对该文件的重新加载。下图所示的输出是我们两次修改货币小数位数导致的。
本文转载自
https://www.cnblogs.com/artech/p/inside-asp-net-core-05-01.html
https://www.cnblogs.com/artech/p/inside-asp-net-core-05-02.html
按照原作者代码执行时 某些方法报错 导致并不能正常运行(原作者并没有说明那些方法 来自哪些引用包)因此本人复制了原作者的内容 并标明了 那些可能报错的方法 来自哪些引用包
出现报错时 只需安装相应的包 并引用即可正常运行