[ASP.NET Core 3框架揭秘] 配置[7]:多样化的配置源[中篇]

物理文件是我们最常用到的原始配置载体,而最佳的配置文件格式主要有三种,它们分别是JSON、XML和INI,对应的配置源类型分别是JsonConfigurationSource、XmlConfigurationSource和IniConfigurationSource,它们具有如下一个相同的基类FileConfigurationSource。

一、FileConfigurationSource

FileConfigurationSource总是利用一个IFileProvider对象来读取配置文件,我们可以利用FileProvider属性来设置这个对象。配置文件的路径通过Path属性表示,一般来说这是一个针对IFileProvider对象根目录的相对路径。在读取配置文件的时候,这个路径将会作为参数调用IFileProvider对象的GetFileInfo方法得到描述配置文件的IFileInfo对象,该对象的CreateReadStream方法最终会被调用来读取文件内容。

public abstract class FileConfigurationSource : IConfigurationSource
{
    public IFileProvider FileProvider { get; set; }
    public string Path { get; set; }
    public bool Optional { get; set; }
    public int ReloadDelay { get; set; }
    public bool ReloadOnChange { get; set; }
    public Action<FileLoadExceptionContext> OnLoadException { get; set; }

    public abstract IConfigurationProvider Build(IConfigurationBuilder builder);
    public void EnsureDefaults(IConfigurationBuilder builder);
    public void ResolveFileProvider();
}

ResolveFileProvider方法

如果FileProvider属性并没有被显式赋值,而我们指定的配置文件路径是一个绝对路径(比如“c:\app\appsettings.json”),那么一个针对配置文件所在目录(“c:\app”)的PhysicalFileProvider将会自动创建出来作为FileProvider的属性值,而Path属性将被设置成配置文件名。如果指定的仅仅是一个相对路径,FileProvider属性将不会被自动初始化。这个逻辑实现在ResolveFileProvider方法中,并体现在如下的测试程序中。

class Program
{
    static void Main()
    {
        var source = new FakeConfigurationSource
        {
            Path = @"C:\App\appsettings.json"
        };
        Debug.Assert(source.FileProvider == null);

        source.ResolveFileProvider();
        var fileProvider = (PhysicalFileProvider)source.FileProvider;
        Debug.Assert(fileProvider.Root == @"C:\App\");
        Debug.Assert(source.Path == "appsettings.json");
    }

    private class FakeConfigurationSource : FileConfigurationSource
    {
        public override IConfigurationProvider Build(IConfigurationBuilder builder) => throw new NotImplementedException();
    }
}

EnsureDefaults方法

除了ResolveFileProvider方法,FileConfigurationSource还定义了另一个名为EnsureDefaults的方法,该方法会确保FileConfigurationSource总是具有一个用于加载配置文件的IFileProvider对象。具体来说,该方法最终会调用IConfigurationBuilder接口具有如下定义的扩展方法GetFileProvider来获取默认的IFileProvider对象。

public static class FileConfigurationExtensions
{    
    public static IFileProvider GetFileProvider(this IConfigurationBuilder builder)
    {
        if (builder.Properties.TryGetValue("FileProvider", out object provider))
        {
            return builder.Properties["FileProvider"] as IFileProvider;
        }
        return new PhysicalFileProvider(AppContext.BaseDirectory ?? string.Empty);
    }
}

从上面给出的代码片段可以看出,这个扩展方法 实际上是将IConfigurationBuilder对象的Properties属性表示的字典作为了存放IFileProvider对象的容器(对应的Key为“FileProvider”)。如果这个容器中存在一个IFileProvider对象,那么它将作为方法的返回值。反之,该方法会根据当前应用的基础目录(默认为当前应用程序域的基础目录,也就是当前执行的.exe文件所在的目录)作为根目录创建一个PhysicalFileProvider对象。

SetFileProvider和SetBasePath方法

既然默认情况下EnsureDefaults方法会从IConfigurationBuilder对象的属性字典中提取IFileProvider对象,那么我们可以在这个属性字典中存放一个默认的IFileProvider对象供所有注册在它上面的FileConfigurationSource对象共享。实际上IConfigurationBuilder接口提供了如下两个SetFileProvider和SetBasePath扩展方法实现了这个功能。

public static class FileConfigurationExtensions
{    
    public static IConfigurationBuilder SetFileProvider( this IConfigurationBuilder builder, IFileProvider fileProvider)
    {
        builder.Properties["FileProvider"] = fileProvider;
        return builder;
    }

    public static IConfigurationBuilder SetBasePath( this IConfigurationBuilder builder, string basePath)
        =>builder.SetFileProvider(new PhysicalFileProvider(basePath));
}

可缺省配置文件

FileConfigurationSource的Optional表示当前配置源是否可以缺省。如果该属性被设置成False,即使指定的配置文件不存在也不会抛出异常。可缺省的配置文件在支持多环境的场景中具有广泛的应用。正如前面实例演示的一样,我们可以按照如下的方式加载两个配置文件,基础配置文件appsettings.json一般包含相对全面的配置,针对某个环境的差异化配置则定义在appsettings.{environment}.json文件中。前者是必需的,后者则是可以缺省的,这保证了应用程序在缺少基于当前环境的差异化配置文件的情况下依然可以使用定义在基础配置文件中的默认配置。

var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile(path: "appsettings.json", optional: false)
    .AddJsonFile(path: $"appsettings.{environment}.json", optional: true)
    .Build();

配置数据的实时同步

FileConfigurationSource借助IFileProvider对象提供的文件系统监控功能实现了配置文件在更新后的自动实时加载功能,这个特性通过ReloadOnChange属性来开启或者关闭。默认情况下这个特性是关闭的,我们需要通过将这个属性设置为True来显式地开启该特性。如果开启了配置文件的重新加载功能,一旦配置文件发生变化,IFileProvider对象会在第一时间将通知发送给对应的FileConfigurationProvider对象,后者会调用Load方法重新加载配置文件。考虑到有可能针对配置文件的写入此时尚未结束,FileConfigurationSource采用了 “延时加载” 的方式来解决这个问题,具体的延时通过ReloadDelay属性来控制。该属性的单位是毫秒,默认设置的延时为250毫秒。

异常处理

考虑到针对配置文件的加载不可能百分之百成功,所以FileConfigurationSource提供了相应的异常处理机制。具体来说,我们可以通过FileConfigurationSource对象的OnLoadException属性注册一个Action<FileLoadExceptionContext>类型的委托作为异常处理器。作为参数的FileLoadExceptionContext 对象代表FileConfigurationProvider在加载配置文件出错的情况下为异常处理器提供的执行上下文。

public class FileLoadExceptionContext
{
    public Exception Exception { get; set; }
    public FileConfigurationProvider Provider { get; set; }
    public bool Ignore { get; set; }
}

如上面的代码片段所示,我们可以从FileLoadExceptionContext上下文中获取抛出的异常和当前FileConfigurationProvider对象。如果异常处理结束之后上下文对象的Ignore属性被设置为True,FileConfigurationProvider对象会认为目前的异常(可能是原来抛出的异常,也可能是异常处理器设置的异常)是可以被忽略的,此时程序会继续执行,否则异常还是会抛出来。顺便强调一下,最终抛出来的是原来的异常,所以我们不可以通过修改上下文的Exception属性来达到抛出另一个异常的目的。

就像我们可以为注册到IConfigurationBuilder对象上的所有FileConfigurationSource注册一个共享的IFileProvider对象一样,我们也可以调用IConfigurationBuilder接口的SetFileLoadExceptionHandler扩展方法注册一个共享的异常处理器,该方法依然是利用IConfiguration
Builder对象的属性字典来存放这个作为异常处理器的委托对象。注册的这个异常处理器通过对应的扩展方法GetFileLoadExceptionHandler来获取。

public static class FileConfigurationExtensions
{
    public static IConfigurationBuilder SetFileLoadExceptionHandler(this IConfigurationBuilder builder, Action<FileLoadExceptionContext> handler)
    {
        builder.Properties["FileLoadExceptionHandler"] = handler;
        return builder;
    }

    public static Action<FileLoadExceptionContext> GetFileLoadExceptionHandler(this IConfigurationBuilder builder)
        => builder.Properties.TryGetValue("FileLoadExceptionHandler", out object handler) ? handler as Action<FileLoadExceptionContext> : null;
}

前面我们提到FileConfigurationSource的EnsureDefaults方法,这个方法除了在IFileProvider对象没有被初始化的情况下调用IConfigurationBuilder的GetFileProvider扩展方法提供一个默认的IFileProvider对象之外,它还会在异常处理器没有初始化的情况下调用上面这个GetFileLoad
ExceptionHandler扩展方法提供一个默认的异常处理器。

二、FileConfigurationProvider

对于配置系统默认提供的针对三种文件格式化的FileConfigurationSource类型来说,它们提供的IConfigurationProvider实现都派生于如下这个抽象基类FileConfigurationProvider。对于我们自定义的FileConfigurationSource,但我们也倾向于将这个抽象类作为对应IConfiguration
Provider实现类型的基类。

public abstract class FileConfigurationProvider : ConfigurationProvider
{
    public FileConfigurationSource Source { get; }
    public FileConfigurationProvider(FileConfigurationSource source);

    public override void Load();    
    public abstract void Load(Stream stream);    
}

当我们创建一个FileConfigurationProvider对象的时候需要提供对应的FileConfigurationSource对象,它会赋值给Source属性。如果指定的FileConfigurationSource对象开启了配置文件更新监控和自动加载功能(其属性OnLoadException返回True),FileConfigurationProvider对象会利用FileConfigurationSource对象提供的IFileProvider对象对配置文件实施监控,并通过注册回调的方式在配置文件更新的时候调用Load方法重新加载配置。

由于FileConfigurationSource对象提供了IFileProvider对象,所以FileConfigurationProvider对象可以调用其CreateReadStream方法获取读取配置文件内容的流对象,因此我们可以利用这个Stream对象来完成配置的加载。根据基于Stream加载配置的功能体现在抽象方法Load上,所以FileConfigurationProvider对象的派生类都需要重写这个方法。

三、JsonConfigurationSource

JsonConfigurationSource代表针对通过JSON文件的配置源,该类型定义在NuGet包“Microsoft.Extensions.Configuration.Json”中。从如下给出的定义可以看出,JsonConfigurationSource重写的Build方法在提供对应的JsonConfigurationProvider对象之前会调用EnsureDefaults方法,这个方法确保用于读取配置文件的IFileProvider对象和处理配置文件加载异常的处理器被初始化。JsonConfigurationProvider对象派生于抽象类FileConfigurationProvider,它利用重写的Load方法读取配置文件的内容并将其转换成配置字典。

public class JsonConfigurationSource : FileConfigurationSource
{
    public override IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        EnsureDefaults(builder);
        return new JsonConfigurationProvider(this);
    }
}

public class JsonConfigurationProvider : FileConfigurationProvider
{
    public JsonConfigurationProvider(JsonConfigurationSource source);
    public override void Load(Stream stream);
}

IConfigurationBuilder接口具有如下几个名为AddJsonFile扩展方法来注册JsonConfigurationSource。如果调用第一个AddJsonFile方法重载,我们可以利用指定的Action<JsonConfigurationSource>对象对创建的JsonConfigurationSource进行初始化。至于其他AddJsonFile方法重载,实际上就是通过相应的参数初始化JsonConfigurationSource对象的Path、Optional和ReloadOnChange属性罢了。

public static class JsonConfigurationExtensions
{
    public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, Action<JsonConfigurationSource> configureSource);
    public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, string path);
    public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, string path, bool optional);
    public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange);
    public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange);
}

当使用JSON文件来定义配置的时候,我们会发现不论对于何种数据结构(复杂对象、集合、数组和字典),我们都能通过JSON格式以一种简单而自然的方式来定义它们。同样以前面定义的Profile类型为例,我们可以利用如下所示的三个JSON文件分别定义一个完整的Profile对象、一个Profile对象的集合以及一个Key和Value类型分别为字符串和Profile的字典。

Profile对象:

{
    "profile": {
        "gender" : "Male",
        "age" : "18",
        "contactInfo"    : {
            "email" : "foobar@outlook.com",
            "phoneNo": "123456789"
        }
    }
}

Profile集合或者数组:

{
  "profiles": [
    {
      "gender": "Male",
      "age": "18",
      "contactInfo": {
        "email": "foo@outlook.com",
        "phoneNo": "123"
      }
    },
    {
      "gender": "Male",
      "age": "25",
      "contactInfo": {
        "email": "bar@outlook.com",
        "phoneNo": "456"
      }
    },
    {
      "gender": "Female",
      "age": "40",
      "contactInfo": {
        "email": "baz@outlook.com",
        "phoneNo": "789"
      }
    }
  ]
}

Profile字典(Dictionary<string, Profile>):

{
  "profiles": {
    "foo": {
      "gender": "Male",
      "age": "18",
      "contactInfo": {
        "email": "foo@outlook.com",
        "phoneNo": "123"
      }
    },
    "bar": {
      "gender": "Male",
      "age": "25",
      "contactInfo": {
        "email": "bar@outlook.com",
        "phoneNo": "456"
      }
    },
    "baz": {
      "gender": "Female",
      "age": "40",
      "contactInfo": {
        "email": "baz@outlook.com",
        "phoneNo": "789"
      }
    }
  }
}

四、XmlConfiguationSource

XML也是一种常用的配置定义形式,它对数据的表达能力甚至强于JSON,几乎所有类型的数据结构都可以通过XML表示出来。当我们通过一个XML元素表示一个复杂对象的时候,对象的数据成员定义成当前XML元素的子元素。如果数据成员是一个简单数据类型,我们还可以选择将其定义成当前XML元素的属性(Attribute)。针对一个Profile对象,我们可以采用如下两种不同的形式来定义。

<Profile>
    <Gender>Male</Gender>
    <Age>18</Age>
    <ContactInfo>
        <EmailAddress>foobar@outlook.com</EmailAddress>
        <PhoneNo>123456789</PhoneNo>
   </ContactInfo>
</Profile>

或者

<Profile Gender="Male" Age="18">
  <ContactInfo EmailAddress ="foobar@outlook.com" PhoneNo="123456789"/>
</Profile>

虽然XML对数据结构的表达能力总体要强于JSON,但是作为配置模型的数据来源却有自己的局限性,比如它们对集合的表现形式有点不尽如人意。举个简单的例子,对于一个元素类型为Profile的集合,我们可以采用具有如下结构的XML来表现。

<Profiles>
    <Profile Gender="Male" Age="18">
        <ContactInfo EmailAddress ="foo@outlook.com" PhoneNo="123"/>
    </Profile>
    <Profile Gender="Male" Age="25">
        <ContactInfo EmailAddress ="bar@outlook.com" PhoneNo="456"/>
    </Profile>
    <Profile Gender="Male" Age="36">
        <ContactInfo EmailAddress ="baz@outlook.com" PhoneNo="789"/>
    </Profile>
</Profiles>

但是这段XML却不能正确地转换成配置字典,原因很简单,因为字典的Key必须是唯一的,这必然要求最终构成配置树的每个节点必须具有不同的路径。上面这段XML很明显不满足这个基本的要求,因为表示一个Profile对象的三个XML元素(<Profile>...</Profile>)是“同质”的,对于由它们表示的三个Profile对象来说,分别表示性别、年龄、电子邮箱地址和电话号码的四个叶子节点的路径是完全一样的,所以根本无法作为配置字典的Key。通过前面针对配置绑定的介绍我们知道,如果需要通过配置字典来表示一个Profile对象的集合,我们需要按照如下的方式为每个集合元素加上相应的索引(“foo”、“bar”和“baz”)。

foo:Gender
foo:Age
foo:ContactInfo:EmailAddress
foo:ContactInfo:PhoneNo

bar:Gender
bar:Age
bar:ContactInfo:EmailAddress
bar:ContactInfo:PhoneNo

baz:Gender
baz:Age
baz:ContactInfo:EmailAddress
baz:ContactInfo:PhoneNo

按照这样的结构,如果我们需要以XML的方式来表示一个Profile对象的集合,就不得不采用如下的结构。但是这样的定义方式从语义的角度来讲是不合理的,因为同一个集合的所有元素就应该是“同质”的,同质的XML元素采用不同的名称有点说不过去。根据配置绑定的规则,这样的结构同样可以表示一个由三个元素组成的Dictionary<string, Profile>对象,Key分别是“Foo”、“Bar”和“Baz”。如果用这样的XML来表示一个字典对象,语义上就完全没有问题了。

<Profiles>
  <Foo Gender="Male" Age="18">
    <ContactInfo EmailAddress ="foobar@outlook.com" PhoneNo="123"/>
  </Foo>
  <Bar Gender="Male" Age="25">
    <ContactInfo EmailAddress ="foobar@outlook.com" PhoneNo="123"/>
  </Bar>
  <Baz Gender="Male" Age="18">
    <ContactInfo EmailAddress ="baz@outlook.com" PhoneNo="789"/>
  </Baz>
</Profiles>

针对XML文件的配置源类型为XmlConfigurationSource,该类型定义在“Microsoft.Extensions.Configuration.Xml”这个NuGet包中。如下面的代码片段所示,XmlConfigurationSource通过重写的Build方法创建出对应的XmlConfigurationProvider对象。作为抽象类型FileConfigurationProvider的继承者,XmlConfigurationProvider通过重写的Load方法完成了针对XML文件的读取和配置字典的初始化。

public class XmlConfigurationSource : FileConfigurationSource
{
    public override IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        EnsureDefaults(builder);
        return new XmlConfigurationProvider(this);
    }
}

public class XmlConfigurationProvider : FileConfigurationProvider
{   
    public XmlConfigurationProvider(XmlConfigurationSource source);   
    public override void Load(Stream stream);
}

JsonConfigurationSource的注册可以通过调用针对IConfigurationBuilder对象的扩展方法AddJsonFile来完成。与之类似,IConfigurationBuilder接口同样具有如下一系列名为AddXmlFile的扩展方法,这些方法会帮助我们注册根据指定XML文件创建的XmlConfigurationSource对象。

public static class XmlConfigurationExtensions
{
    public static IConfigurationBuilder AddXmlFile(this IConfigurationBuilder builder, string path);
    public static IConfigurationBuilder AddXmlFile(this IConfigurationBuilder builder, string path, bool optional);
    public static IConfigurationBuilder AddXmlFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange);
    public static IConfigurationBuilder AddXmlFile(this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange);
}

五、IniConfigurationSource

“INI”是“Initialization”的缩写,INI文件又被称为初始化文件,它是Windows系统普遍使用的配置文件,同时也被一些Linux和Unix系统所支持。INI文件直接以键值对的形式定义配置项,如下所示的代码片段体现了INI文件的基本格式。总的来说,INI文件以单纯的“{Key}={Value}”的形式定义配置项,{Value}可以定义在可选的双引号中(如果值的前后包括空白字符,必须使用双引号,否则会被忽略)。

[Section]
key1=value1
key2 = " value2 "
; comment
# comment
/ comment

除了以“{Key}={Value}”的形式定义的原子配置项外,我们还可以采用“[{SectionName}]”的形式定义配置节对它们进行分组。中括号(“[]”)作为下一个的配置节开始的标志和上一个配置节结束的标志,所以采用INI文件定义的配置节并不存在层次化的结构,即没有“子配置节”的概念。除此之外,我们可以在INI中定义相应的注释,注释行前置的字符可以采用“;”、“#”或者“/”。

由于INI文件自身就体现为一个数据字典,所以我们可以采用“路径化”的Key来定义最终绑定为复杂对象、集合或者字典的配置数据。如果采用INI文件来定义一个Profile对象的基本信息,我们就可以采用如下的定义形式。

Gender = "Male"
Age  = "18"
ContactInfo:EmailAddress = "foobar@outlook.com"
ContactInfo:PhoneNo = "123456789"

由于Profile的配置信息具有两个层次(Profile>ContactInfo),我们可以按照如下的形式将EmailAddress和PhoneNo定义在配置节“ContactInfo”中,这个INI文件在语义表达上和上面是完全等效的。

Gender = "Male"
Age  = "18"

[ContactInfo]
EmailAddress = "foobar@outlook.com"
PhoneNo  = "123456789"

针对INI文件类型的配置源类型通过如下所示的IniConfigurationSource来表示,该类型定义在“Microsoft.Extensions.Configuration.Ini”这个NuGet包中。IniConfigurationSource重写的Build方法创建的是一个IniConfigurationProvider对象。作为抽象类FileConfigurationProvider的继承者,IniConfigurationProvider利用重写的Load方法完成INI文件内容的读取和配置字典的初始化。

public class IniConfigurationSource : FileConfigurationSource
{
    public override IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        EnsureDefaults(builder);
        return new IniConfigurationProvider(this);
    }
 }

public class IniConfigurationProvider : FileConfigurationProvider
{
    public IniConfigurationProvider(IniConfigurationSource source);
    public override void Load(Stream stream);
}

既然JsonConfigurationSource和XmlConfigurationSource的注册可以通过调用IConfigurationBuilder接口的扩展方法AddJsonFile和AddXmlFile来完成,“Microsoft.Extensions. Configuration.Ini”这个NuGet包会也会为IniConfigurationSource定义如下所示的AddIniFile扩展方法。

public static class IniConfigurationExtensions
{
    public static IConfigurationBuilder AddIniFile( this IConfigurationBuilder builder, string path);
    public static IConfigurationBuilder AddIniFile( this IConfigurationBuilder builder, string path, bool optional);
    public static IConfigurationBuilder AddIniFile( this IConfigurationBuilder builder, string path, bool optional,  bool reloadOnChange);
    public static IConfigurationBuilder AddIniFile( this IConfigurationBuilder builder, IFileProvider provider, string path,  bool optional, bool reloadOnChange);
}

[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]:多样化的配置源[中篇]
[ASP.NET Core 3框架揭秘] 配置[8]:多样化的配置源[下篇]
[ASP.NET Core 3框架揭秘] 配置[9]:自定义配置源

posted @ 2019-12-20 08:19  Artech  阅读(2203)  评论(4编辑  收藏  举报