• 理解ASP.NET Core


    注:本文隶属于《理解ASP.NET Core》系列文章,请查看置顶博客或点击此处查看全文目录

    配置提供程序

    在.NET中,配置是通过多种配置提供程序来提供的,包括以下几种:

    • 文件配置提供程序
    • 环境变量配置提供程序
    • 命令行配置提供程序
    • Azure应用配置提供程序
    • Azure Key Vault 配置提供程序
    • Key-per-file配置提供程序
    • 内存配置提供程序
    • 应用机密(机密管理器)
    • 自定义配置提供程序

    为了方便大家后续了解配置,这里先简单提一下选项(Options),它是用于以强类型的方式对程序配置信息进行访问的一种方式。接下来的示例中,我会添加一个简单的配置Book,结构如下:

    public class BookOptions
    {
        public const string Book = "Book";
    
        public string Name { get; set; }
    
        public BookmarkOptions Bookmark { get; set; }
    
        public List<string> Authors { get; set; }
    }
    
    public class BookmarkOptions
    {
        public string Remarks { get; set; }
    }
    

    然后我们在Startup.ConfigureServices中使用IConfiguration进行配置的读取,并显示在控制台中,如下:

    public void ConfigureServices(IServiceCollection services)
    {
        var book = Configuration.GetSection(BookOptions.Book).Get<BookOptions>();
        Console.WriteLine($"Book Name: {book.Name}" +
            $"{Environment.NewLine}Bookmark Remarks:{book.Bookmark.Remarks}" +
            $"{Environment.NewLine}Book Authors: {string.Join(" & ", book.Authors)}");
    }
    

    接下来,就挑几个常用的配置提供程序来详细讲解一下。

    文件配置提供程序

    顾名思义,就是从文件中加载配置。文件细分为

    • JSON配置提供程序(JsonConfigurationProvider)
    • XML配置提供程序(XmlConfigurationProvider)
    • INI配置提供程序(IniConfigurationProvider)

    以上这些配置提供程序,均继承于抽象类FileConfigurationProvider

    另外,所有文件配置提供程序都支持提供两个配置参数:

    • optionalbool类型,指示该文件是否是可选的。如果该参数为false,但是指定的文件又不存在,则会报错。
    • reloadOnChangebool类型,指示该文件发生更改时,是否要重新加载配置。

    JSON配置提供程序

    通过JsonConfigurationProvider在运行时从Json文件中加载配置。

    Install-Package Microsoft.Extensions.Configuration.Json

    使用方式非常简单,只需要调用AddJsonFile扩展方法添加用于保存配置的Json文件即可:

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((context, config) =>
            {
                // 清空所有配置提供程序
                config.Sources.Clear();
    
                var env = context.HostingEnvironment;
    
                // 添加 appsettings.json 和 appsettings.{env.EnvironmentName}.json 两个json文件
                config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                      .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
            });
    

    你可以在 appsetting.json 中添加如下配置:

    {
      "Book": {
        "Name": "appsettings.json book name",
        "Authors": [
          "appsettings.json author name A",
          "appsettings.json author name B"
        ],
        "Bookmark": {
          "Remarks": "appsettings.json bookmark remarks"
        }
      }
    }
    

    XML配置提供程序

    通过XmlConfigurationProvider在运行时从Xml文件中加载配置。

    Install-Package Microsoft.Extensions.Configuration.Xml

    同样的,只需调用AddXmlFile扩展方法添加Xml文件即可:

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((context, config) =>
            {
                config.AddXmlFile("appsettings.xml", optional: true, reloadOnChange: true);
            });
    

    你可以在 appsettings.xml 中添加如下配置:

    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
      <Book>
        <Name>appsettings.xml book name</Name>
        <Authors name="0">appsettings.xml author name A</Authors>
        <Authors name="1">appsettings.xml author name B</Authors>
         <Bookmark>
          <Remarks>appsettings.xml bookmark remarks</Remarks>
        </Bookmark>
      </Book>
    </configuration>
    

    在 .NET 6 中,我们就不用手动添加 name 属性来指定索引了,它会自动进行索引编号。

    INI配置提供程序

    通过IniConfigurationProvider在运行时从Ini文件中加载配置。

    Install-Package Microsoft.Extensions.Configuration.Ini

    同样的,只需调用AddIniFile扩展方法添加Ini文件即可:

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((context, config) =>
            {
                config.AddIniFile("appsettings.ini", optional: true, reloadOnChange: true);
            });
    

    你可以在 appsettings.ini 中添加如下配置

    [Book]
    Name=appsettings.ini book name
    Authors:0=appsettings.ini book author A
    Authors:1=appsettings.ini book author B
    
    [Book:Bookmark]
    Remarks=appsettings.ini bookmark remarks
    

    环境变量配置提供程序

    通过EnvironmentVariablesConfigurationProvider在运行时从环境变量中加载配置。

    Install-Package Microsoft.Extensions.Configuration.EnvironmentVariables

    同样的,只需调用AddEnvironmentVariables扩展方法添加环境变量即可:

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((context, config) =>
            {
                // 添加前缀为 My_ 的环境变量
                config.AddEnvironmentVariables(prefix: "My_");
            });
    

    在添加环境变量时,通过指定参数prefix,只读取限定前缀的环境变量。不过在读取环境变量时,会将前缀删除。如果不指定参数prefix,那么会读取所有环境变量。

    当创建默认通用主机(Host)时,默认就已经添加了前缀为DOTNET_的环境变量,加载应用配置时,也添加了未限定前缀的环境变量。另外,在 ASP.NET Core 中,配置 Web主机时,默认添加了前缀为ASPNETCORE_的环境变量。

    需要注意的是,由于环境变量的分层键:并不受所有平台支持,而双下划线(__)是全平台支持的,所以要使用双下划线(__)来代替冒号(:)。

    在 Windows 平台下,可以通过setsetx命令进行环境变量配置,不过:

    • set命令设置的环境变量是临时的,仅在当前进程有效,这个进程就是当前cmd窗口启动的。也就是说,当你打开一个cmd窗口时,通过set命令设置了环境变量,然后通过dotnet xxx.dll启动了你的应用程序,是可以读取到环境变量的,但是在该cmd窗口之外,例如通过VS启动应用程序,是无法读取到该环境变量的。
    • setx命令设置的环境变量是持久化的。可选的添加/M开关,表示将该环境变量配置到系统环境中(需要管理员权限),否则,将添加到用户环境中。

    我更喜欢通过setx去设置环境变量(记得以管理员身份运行哦):

    # 注意,这里的 My_ 是前缀
    setx My_Book__Name "Environment variables book name" /M
    setx My_Book__Authors__0 "Environment variables book author A" /M
    setx My_Book__Authors__1 "Environment variables book author B" /M
    setx My_Book__Bookmark__Remarks "Environment variables bookmark remakrs" /M
    

    配置完环境变量后,一定要记得重启VS或cmd窗口,否则是无法读取到最新的环境变量值的

    连接字符串前缀的特殊处理

    当没有向AddEnvironmentVariables传入前缀时,默认也会针对含有以下前缀的环境变量进行特殊处理:

    前缀 环境变量Key 配置Key 配置提供程序
    MYSQLCONNSTR_ MYSQLCONNSTR_{KEY} ConnectionStrings:{KEY} MySQL
    SQLCONNSTR_ SQLCONNSTR_{KEY} ConnectionStrings:{KEY} SQL Server
    SQLAZURECONNSTR_ SQLAZURECONNSTR_{KEY} ConnectionStrings:{KEY} Azure SQL
    CUSTOMCONNSTR_ CUSTOMCONNSTR_{KEY} ConnectionStrings:{KEY} 自定义配置提供程序

    在 launchSettings.json 中配置环境变量

    在 ASP.NET Core 模板项目中,会生成一个 launchSettings.json 文件,我们也可以在该文件中配置环境变量。

    需要注意的是,launchSettings.json 中的配置只用于开发环境,并且在该文件中设置的环境变量会覆盖在系统环境中设置的变量。

    {
      "WebApplication": {
        "commandName": "Project",
        "dotnetRunMessages": "true",
        "launchBrowser": true,
        "launchUrl": "swagger",
        "applicationUrl": "http://localhost:5000",      // 设置环境变量 ASPNETCORE_URLS
        "environmentVariables": {
          "ASPNETCORE_ENVIRONMENT": "Development",
          "My_Book__Name": "launchSettings.json Environment variables book name",
          "My_Book__Authors__0": "launchSettings.json Environment variables book author A",
          "My_Book__Authors__1": "launchSettings.json Environment variables book author B",
          "My_Book__Bookmark__Remarks": "launchSettings.json Environment variables bookmark remarks"
        }
      }
    }
    

    虽然说在 launchSettings.json 中配置环境变量时可以使用冒号(:)作为分层键,但是我在测试过程中,发现当同时配置了系统环境变量时,程序读取到的环境变量值会发生错乱(一部分是系统环境变量,一部分是该文件中的环境变量)。所以建议大家还是使用双下划线(__)作为分层键。

    在Linux平台,当设置的环境变量为URL时,需要设置为转义后的URL。可以使用systemd-escaple工具:

    $ systemd-escape http://localhost:5001
    http:--localhost:5001
    

    命令行配置提供程序

    通过CommandLineConfigurationProvider在运行时从命令行参数键值对中加载配置。

    Install-Package Microsoft.Extensions.Configuration.CommandLine

    通过调用AddCommandLine扩展方法,并传入参数args

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((context, config) =>
            {
                config.AddCommandLine(args);
            });
    

    有三种设置命令行参数的方式:

    使用=

    dotnet run Book:Name="Command line book name" Book:Authors:0="Command line book author A" Book:Authors:1="Command line book author B" Book:Bookmark:Remarks="Command line bookmark remarks"
    

    使用/

    dotnet run /Book:Name "Command line book name" /Book:Authors:0 "Command line book author A" /Book:Authors:1 "Command line book author B"  /Book:Bookmark:Remarks "Command line bookmark remarks"
    

    使用--

    dotnet WebApplication5.dll --Book:Name "Command line book name" --Book:Authors:0 "Command line book author A" --Book:Authors:1 "Command line book author B" --Book:Bookmark:Remarks "Command line bookmark remarks"
    

    交换映射

    该功能是针对命令行配置参数进行key映射的,如你可以将n映射为Name,要求:

    • 交换映射key必须以---开头。当使用-开头时,命令行参数书写时也要以-开头,当使用--开头时,命令行参数书写时可以以--/开头。
    • 交换映射字典中的key不区分大小写,不能包含重复key。如不能同时出现-n-N,但可以同时出现-n--n

    接下来我们来映射一下:

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((context, config) =>
            {
                var switchMappings = new Dictionary<string, string>
                {
                    ["--bn"] = "Book:Name",
                    ["-ba0"] = "Book:Authors:0",
                    ["--ba1"] = "Book:Authors:1",
                    ["--bmr"] = "Book:Bookmark:Remarks"
                };
                config.AddCommandLine(args, switchMappings);
            });
    

    然后以命令行命令启动:

    dotnet run --bn "Command line book name" -ba0 "Command line book author A" /ba1 "Command line book author B" --bmr="Command line bookmark remarks"
    

    内存配置提供程序

    通过MemoryConfigurationProvider在运行时从内存中的集合中加载配置。

    Install-Package Microsoft.Extensions.Configuration

    通过调用AddInMemoryCollection添加内存配置:

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((context, config) =>
            {
                config.AddInMemoryCollection(new Dictionary<string, string>
                {
                    ["Book:Name"] = "Memmory book name",
                    ["Book:Authors:0"] = "Memory book author A",
                    ["Book:Authors:1"] = "Memory book author B",
                    ["Book:Bookmark:Remarks"] = "Memory bookmark remarks"
                });
            });
    

    主机(Host)中的默认配置优先级

    约定:越后添加的配置提供程序优先级越高,优先级高的配置值会覆盖优先级低的配置值

    在 主机(Host)中,我们介绍了Host的启动流程,根据默认的配置提供程序的添加顺序,默认的优先级从低到高为(我顺便将WebHost默认配置的也加进来了):

    1. 内存配置提供程序 环境变量配置提供程序(prefix: DOTNET_)
    2. 环境变量配置提供程序(prefix: ASPNETCORE_)
    3. JSON配置提供程序(appsettings.json)
    4. JSON配置提供程序(appsettings.{Environment}.json)
    5. 机密管理器(仅Windows)
    6. 环境变量配置提供程序(未限定前缀)
    7. 命令行配置提供程序

    完整的配置提供程序列表可以通过 IConfigurationRoot.Providers 来查看。

    如果想要添加额外配置文件,但是仍然想要环境变量或命令行参数优先,则可以类似这样做:

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((context, config) =>
            {
                config.AddJsonFile("my.json", optional: true, reloadOnChange: true);
                
                config.AddEnvironmentVariables();
                config.AddCommandLine(args);
            });
    

    配置体系

    上面我们已经了解了几种常用的配置提供程序,这是微软已经提供的。如果你看过某个配置提供程序的源码的话,一定见过IConfigurationSourceIConfigurationProvider等接口。

    IConfigurationSource

    IConfigurationSource负责创建IConfigurationProvider实现的实例。它的定义很简单,就一个Build方法,返回IConfigurationProvider实例:

    public interface IConfigurationSource
    {
        IConfigurationProvider Build(IConfigurationBuilder builder);
    }
    

    IConfigurationProvider

    IConfigurationProvider负责实现配置的设置、读取、重载等功能,并以键值对形式提供配置。

    所有配置提供程序均建议继承于抽象类ConfigurationProvider,该类实现了接口IConfigurationProvider

    public interface IConfigurationProvider
    {
        // 获取指定父路径下的直接子节点Key,然后 Concat(earlierKeys) 一同返回
        IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath);
        
        // 当该配置提供程序支持更改追踪(change tracking)时,会返回 change token
        // 否则,返回 null
        IChangeToken GetReloadToken();
    
        // 加载配置
        void Load();
    
        // 设置 key:value
        void Set(string key, string value);
    
        // 尝试获取指定 key 的 value
        bool TryGet(string key, out string value);
    }
    
    public abstract class ConfigurationProvider : IConfigurationProvider
    {
        // 包含了该配置提供程序的所有叶子节点的配置项
        protected IDictionary<string, string> Data { get; set; }
    
        protected ConfigurationProvider() { }
    
        // 从 Data 中查找指定父路径下的直接子节点Key,然后 Concat(earlierKeys) 一同返回
        public virtual IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath) { }
    
        public IChangeToken GetReloadToken() { }
    
        // 将配置项赋值到 Data 中
        public virtual void Load() { }
    
        protected void OnReload() { }
    
        // 设置 Data key:value
        public virtual void Set(string key, string value) { }
    
        public override string ToString() { }
        
        // 尝试从 Data 中获取指定 key 的 value
        public virtual bool TryGet(string key, out string value) { }
    }
    

    Data包含了该配置提供程序的所有叶子节点的配置项。拿上方的Book示例来说,该Data包含“Book:Name”、“Book:Authors:0”、“Book:Authors:1”和“Book:Bookmark:Remarks”这4个Key。

    另外,你可能还会见到一个名为ChainedConfigurationProvider的配置提供程序,它可以将一个已存在的IConfiguration实例,作为配置提供程序添加到另一个IConfiguration中。例如HostConfiguration流转到AppConfiguration就使用了这个。

    IConfigurationBuilder

    public interface IConfigurationBuilder
    {
        // 存放用于该 Builder 的 Sources 列表中各个元素的共享字典
        IDictionary<string, object> Properties { get; }
    
        // 已注册的 IConfigurationSource 列表
        IList<IConfigurationSource> Sources { get; }
    
        // 将 IConfigurationSource 添加到 Sources 中
        IConfigurationBuilder Add(IConfigurationSource source);
    
        // 通过 Sources 构建配置提供程序实例,并创建 IConfigurationRoot 实例
        IConfigurationRoot Build();
    }
    

    ConfigurationBuilder实现了IConfigurationBuilder接口:

    public class ConfigurationBuilder : IConfigurationBuilder
    {
        public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();
    
        public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();
    
        public IConfigurationBuilder Add(IConfigurationSource source)
        {
            if (source == null)
            {
                throw new ArgumentNullException(nameof(source));
            }
    
            Sources.Add(source);
            return this;
        }
    
        public IConfigurationRoot Build()
        {
            var providers = new List<IConfigurationProvider>();
            foreach (IConfigurationSource source in Sources)
            {
                IConfigurationProvider provider = source.Build(this);
                providers.Add(provider);
            }
            return new ConfigurationRoot(providers);
        }
    }
    

    IConfiguration

    public interface IConfiguration
    {
        // 获取或设置指定配置 key 的 value
        string this[string key] { get; set; }
        
        // 获取当前配置节点的 直接 子节点列表
        IEnumerable<IConfigurationSection> GetChildren();
    
        // 获取监控配置发生更改的 token
        IChangeToken GetReloadToken();
        
        // 获取指定Key的配置子节点
        IConfigurationSection GetSection(string key);
    }
    

    GetValue

    通过IConfiguration的扩展方法ConfigurationBinder.GetValue,可以以类似字典的方式,读取某个Key对应的Value。

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
    
        public IConfiguration Configuration { get; }
    
        public void ConfigureServices(IServiceCollection services)
        {
            var bookName = Configuration.GetValue<string>("Book:Name", defaultValue: "Unknown");
            Console.WriteLine(bookName);
        }
    }
    

    该扩展的实质(默认实现)是在底层通过调用IConfigurationProvider.TryGet方法,读取ConfigurationProvider.Data字典中的键值对。所以,只能通过该扩展方法读取叶子节点的配置值。

    GetSection

    通过IConfiguration.GetSection方法,可以获取到指定Key的配置子节点:

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
    
        public IConfiguration Configuration { get; }
    
        public void ConfigureServices(IServiceCollection services)
        {
            // 返回的 section 永远不会为 null
            IConfigurationSection bookSection = Configuration.GetSection(BookOptions.Book);
    
            IConfigurationSection bookmarkSection = bookSection.GetSection("Bookmark");
            // or
            //IConfigurationSection bookmarkSection = Configuration.GetSection("Book:Bookmark");
    
            var remarks = bookmarkSection["Remarks"];
            Console.WriteLine(remarks);
        }
    }
    

    GetChildren

    通过IConfiguration.GetChildren方法,可以获取到当前配置节点的直接子节点列表

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
    
        public IConfiguration Configuration { get; }
    
        public void ConfigureServices(IServiceCollection services)
        {
            // children 包含了 Name、Bookmark、Authors
            var children = Configuration.GetSection(BookOptions.Book).GetChildren();
            foreach (var child in children)
            {
                Console.WriteLine($"Key: {child.Key}	Value: {child.Value}");
            }
        }
    }
    

    Exists

    前面提到了,Configuration.GetSection永远不会返回null,那么我们如何判断该 Section 是否真的存在呢?这就要用到扩展方法ConfigurationExtensions.Exists了:

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
    
        public IConfiguration Configuration { get; }
    
        public void ConfigureServices(IServiceCollection services)
        {
            IConfigurationSection bookSection = Configuration.GetSection(BookOptions.Book);
            if (bookSection.Exists())
            {
                var notExistSection = bookSection.GetSection("NotExist");
                if (!notExistSection.Exists())
                {
                    Console.WriteLine("Book:NotExist");
                }
            }
        }
    }
    

    这里分析一下Exists的源码:

    public static class ConfigurationExtensions
    {
        public static bool Exists(this IConfigurationSection section)
        {
            if (section == null)
            {
                return false;
            }
            return section.Value != null || section.GetChildren().Any();
        }
    }
    

    因此,在这里补充一下:假设存在某个子节点(ConfigurationSection),若该子节点为叶子节点,那么其Value一定不为null,若该子节点非叶子节点,则该子节点的子节点一定不为空

    Get

    通过ConfigurationBinder.Get方法,可以将配置以强类型的方式绑定到选项对象上:

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
    
        public IConfiguration Configuration { get; }
    
        public void ConfigureServices(IServiceCollection services)
        {
            var book = Configuration.GetSection(BookOptions.Book).Get<BookOptions>();
            Console.WriteLine($"Book Name: {book.Name}" +
            $"{Environment.NewLine}Bookmark Remarks:{book.Bookmark.Remarks}" +
            $"{Environment.NewLine}Book Authors: {string.Join(" & ", book.Authors)}");
        }
    }
    

    Bind

    与上方Get方法类似,通过ConfigurationBinder.Bind 方法,可以将配置以强类型的方式绑定到已存在的选项对象上:

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
    
        public IConfiguration Configuration { get; }
    
        public void ConfigureServices(IServiceCollection services)
        {
            var book = new BookOptions();
            Configuration.GetSection(BookOptions.Book).Bind(book);
            Console.WriteLine($"Book Name: {book.Name}" +
            $"{Environment.NewLine}Bookmark Remarks:{book.Bookmark.Remarks}" +
            $"{Environment.NewLine}Book Authors: {string.Join(" & ", book.Authors)}");
        }
    }
    

    IConfigurationRoot

    IConfigurationRoot表示配置的,相应的,下面要提到的IConfigurationSection则表示配置的子节点。举个例子,XML格式的文档都会有一个根节点(如上方示例中的<configuration>),还可以包含多个子节点(如上方示例中的<Book><Name>等)。

    public interface IConfigurationRoot : IConfiguration
    {
        // 存放了当前应用程序的所有配置提供程序
        IEnumerable<IConfigurationProvider> Providers { get; }
    
        // 强制从配置提供程序中重载配置
        void Reload();
    }
    

    ConfigurationRoot实现了IConfigurationRoot接口,下面就着重看一下Reload方法的实现:

    Startup构造函数中注入的IConfiguration其实就是ConfigurationRoot的实例。

    public class ConfigurationRoot : IConfigurationRoot, IDisposable
    {
        private readonly IList<IConfigurationProvider> _providers;
        
        public ConfigurationRoot(IList<IConfigurationProvider> providers)
        {
            // 该构造函数内代码有删减
        
            _providers = providers;
            foreach (IConfigurationProvider p in providers)
            {
                p.Load();
            }
        }
        
        public void Reload()
        {
            foreach (IConfigurationProvider provider in _providers)
            {
                provider.Load();
            }
            
            // 此处删减了部分代码
        }
    }
    

    IConfigurationSection

    IConfigurationSection表示配置的子节点。

    public interface IConfigurationSection : IConfiguration
    {
        // 该子节点在其父节点中所表示的 key
        string Key { get; }
    
        // 该子节点在配置中的全路径(从根节点开始,到当前节点的路径)
        string Path { get; }
    
        // 该子节点的 value。如果该子节点下存在孩子节点,则其始终为 null
        string Value { get; set; }
    }
    

    借用上方的数据举个例子,假设配置提供程序为内存:

    • 当我们通过Configuration.GetSection("Book:Name")获取到子节点时,Key为“Name”,Path为“Book:Name”,Value则为“Memmory book name”
    • 当我们通过Configuration.GetSection("Book:Bookmark")获取到子节点时,Key为“Bookmark”,Path为“Book:Name”,Value则为null

    实现自定义配置提供程序

    既然我们已经理解了.NET中的配置体系,那我们完全可以自己动手实践一下了,现在就来实现一个自定义的配置提供程序来玩玩。

    日常使用的配置中心客户端,如Apollo等,都是通过实现自定义配置提供程序来提供配置的。

    咱们不搞那么复杂,就基于ORM框架EF Core来实现一个自定义配置提供程序,具体逻辑是这样的:数据库中有一个JsonConfiguration数据集,专门用来存放Json格式的配置。该表有KeyValue两个字段,Key对应例子中的“Book”,而Value则是“Book”对应值的Json字符串。

    首先,装一下Nuget包:

    Install-Package Microsoft.EntityFrameworkCore.InMemory

    然后定义自己的DbContext——AppDbContext

    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions options) 
            : base(options) { }
    
        public virtual DbSet<JsonConfiguration> JsonConfigurations { get; set; }
    }
    
    public class JsonConfiguration
    {
        [Key]
        public string Key { get; set; }
    
        public string Value { get; set; }
    }
    

    接下来,通过EFConfigurationSource来构建EFConfigurationProvider实例:

    public class EFConfigurationSource : IConfigurationSource
    {
        private readonly Action<DbContextOptionsBuilder> _optionsAction;
    
        public EFConfigurationSource(Action<DbContextOptionsBuilder> optionsAction)
        {
            _optionsAction = optionsAction;
        }
    
        public IConfigurationProvider Build(IConfigurationBuilder builder)
        {
            return new EFConfigurationProvider(_optionsAction);
        }
    }
    

    接着,就是EFConfigurationProvider的实现了,逻辑类似于Json文件配置提供程序,只不过配置来源于EF而不是Json文件:

    public class EFConfigurationProvider : ConfigurationProvider
    {
        public EFConfigurationProvider(Action<DbContextOptionsBuilder> optionsAction)
        {
            OptionsAction = optionsAction;
        }
    
        Action<DbContextOptionsBuilder> OptionsAction { get; }
    
        public override void Load()
        {
            var builder = new DbContextOptionsBuilder<AppDbContext>();
    
            OptionsAction(builder);
    
            using var dbContext = new AppDbContext(builder.Options);
    
            dbContext.Database.EnsureCreated();
    
            // 如果没有任何配置则添加默认配置
            if (!dbContext.JsonConfigurations.Any())
            {
                CreateAndSaveDefaultValues(dbContext);
            }
    
            // 将配置项转换为键值对(key和value均为字符串类型)
            Data = EFJsonConfigurationParser.Parse(dbContext.JsonConfigurations);
        }
    
        private static void CreateAndSaveDefaultValues(AppDbContext dbContext)
        {
            dbContext.JsonConfigurations.AddRange(new[]
            {
                new JsonConfiguration
                {
                    Key = "Book",
                    Value = JsonSerializer.Serialize(
                    new BookOptions()
                    {
                        Name = "ef configuration book name",
                        Authors = new List<string>
                        {
                            "ef configuration book author A",
                            "ef configuration book author B"
                        },
                        Bookmark = new BookmarkOptions
                        {
                            Remarks = "ef configuration bookmark Remarks"
                        }
                    })
                }
            });
    
            dbContext.SaveChanges();
        }
    }
    
    internal class EFJsonConfigurationParser
    {
        private EFJsonConfigurationParser() { }
    
        private readonly IDictionary<string, string> _data = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        private readonly Stack<string> _context = new();
        private string _currentPath;
    
        public static IDictionary<string, string> Parse(DbSet<JsonConfiguration> inputs)
            => new EFJsonConfigurationParser().ParseJsonConfigurations(inputs);
    
        private IDictionary<string, string> ParseJsonConfigurations(DbSet<JsonConfiguration> inputs)
        {
            _data.Clear();
    
            if(inputs?.Any() != true)
            {
                return _data;
            }
    
            var jsonDocumentOptions = new JsonDocumentOptions
            {
                CommentHandling = JsonCommentHandling.Skip,
                AllowTrailingCommas = true,
            };
    
            foreach (var input in inputs)
            {
                ParseJsonConfiguration(input, jsonDocumentOptions);
            }
    
            return _data;
        }
    
        private void ParseJsonConfiguration(JsonConfiguration input, JsonDocumentOptions options)
        {
            if (string.IsNullOrWhiteSpace(input.Key))
                throw new FormatException($"The key {input.Key} is invalid.");
    
            var jsonValue = $"{{"{input.Key}": {input.Value}}}";
            using var doc = JsonDocument.Parse(jsonValue, options);
    
            if (doc.RootElement.ValueKind != JsonValueKind.Object)
                throw new FormatException($"Unsupported JSON token '{doc.RootElement.ValueKind}' was found.");
    
            VisitElement(doc.RootElement);
        }
    
        private void VisitElement(JsonElement element)
        {
            foreach (JsonProperty property in element.EnumerateObject())
            {
                EnterContext(property.Name);
                VisitValue(property.Value);
                ExitContext();
            }
        }
    
        private void VisitValue(JsonElement value)
        {
            switch (value.ValueKind)
            {
                case JsonValueKind.Object:
                    VisitElement(value);
                    break;
    
                case JsonValueKind.Array:
                    var index = 0;
                    foreach (var arrayElement in value.EnumerateArray())
                    {
                        EnterContext(index.ToString());
                        VisitValue(arrayElement);
                        ExitContext();
                        index++;
                    }
                    break;
    
                case JsonValueKind.Number:
                case JsonValueKind.String:
                case JsonValueKind.True:
                case JsonValueKind.False:
                case JsonValueKind.Null:
                    var key = _currentPath;
                    if (_data.ContainsKey(key))
                        throw new FormatException($"A duplicate key '{key}' was found.");
    
                    _data[key] = value.ToString();
                    break;
    
                default:
                    throw new FormatException($"Unsupported JSON token '{value.ValueKind}' was found.");
            }
        }
    
        private void EnterContext(string context)
        {
            _context.Push(context);
            _currentPath = ConfigurationPath.Combine(_context.Reverse());
        }
    
        private void ExitContext()
        {
            _context.Pop();
            _currentPath = ConfigurationPath.Combine(_context.Reverse());
        }
    }
    

    其中,EFJsonConfigurationParser是我借鉴JsonConfigurationFileParser而实现的,这也是学习优秀设计的一种方式!

    接着,我们按照AddXXX的格式将该配置提供程序的添加封装为扩展方法:

    public static class EntityFrameworkExtensions
    {
        public static IConfigurationBuilder AddEFConfiguration(
            this IConfigurationBuilder builder,
            Action<DbContextOptionsBuilder> optionsAction)
        {
            return builder.Add(new EFConfigurationSource(optionsAction));
        }
    }
    

    这时,我们就可以使用扩展方法添加EFConfigurationProvider了:

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((context, config) => 
            {
                config.AddEFConfiguration(options => options.UseInMemoryDatabase("configdb"));
            })
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
    

    最后,你可以试着读取一下Book配置了,看看是不是如咱们所期望的那样,读取到EF中的配置呢?这里,我就不再演示了。

    其他

    查看所有配置项

    通过扩展方法ConfigurationExtensions.AsEnumerable,来查看所有配置项:

    public static void Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();
    
        var config = host.Services.GetRequiredService<IConfiguration>();
    
        foreach (var c in config.AsEnumerable())
        {
            Console.WriteLine(c.Key + " = " + c.Value);
        }
        host.Run();
    }
    

    通过委托配置选项

    除了可以通过配置提供程序来提供配置外,也可以通过委托来提供配置:

    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<BookOptions>(book =>
        {
            book.Name = "delegate book name";
            book.Authors = new List<string> { "delegate book author A", "delegate book author A" };
            book.Bookmark = new BookmarkOptions { Remarks = "delegate bookmark reamarks" };
        });
    }
    

    关于选项的更多理解,将在后续章节进行详细讲解。

    注意事项

    配置Key

    • 不区分大小写。例如Namename被视为等效的。
    • 配置提供程序有很多种,如果在多个提供程序中添加了某个配置项,那么,只有在最后一个提供程序中配置的才会生效。
    • 分层键:
      • 在环境变量中,由于冒号(:)无法适用于所有平台,所以要使用全平台均支持的双下划线(__),它会在程序中自动转换为冒号(:
      • 在其他类型的配置中,一般均使用冒号(:)分隔符即可
    • ConfigurationPath类提供了一些辅助方法。

    配置Value

    • 均被保存为字符串
  • 相关阅读:
    Subversion 1.5.5 与 Apache 2.2 结合
    IE6 式样表 Bug
    Dreamweaver 禁止自动换行
    错误:Internet Explorer cannot open the Internet site
    Html Agility Pack – XPath 基本语法
    不要安装 CodeSmith 5.2
    控件包含代码块,因此无法修改控件集合
    单例模式
    第一篇博文
    包与导入的概念
  • 原文地址:https://www.cnblogs.com/xiaoxiaotank/p/15367747.html
Copyright © 2020-2023  润新知