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

配置提供程序

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

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

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

  1. public class BookOptions
  2. {
  3. public const string Book = "Book";
  4. public string Name { get; set; }
  5. public BookmarkOptions Bookmark { get; set; }
  6. public List<string> Authors { get; set; }
  7. }
  8. public class BookmarkOptions
  9. {
  10. public string Remarks { get; set; }
  11. }

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

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

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

文件配置提供程序

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

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

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

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

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

JSON配置提供程序

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

Install-Package Microsoft.Extensions.Configuration.Json

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

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

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

  1. {
  2. "Book": {
  3. "Name": "appsettings.json book name",
  4. "Authors": [
  5. "appsettings.json author name A",
  6. "appsettings.json author name B"
  7. ],
  8. "Bookmark": {
  9. "Remarks": "appsettings.json bookmark remarks"
  10. }
  11. }
  12. }

XML配置提供程序

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

Install-Package Microsoft.Extensions.Configuration.Xml

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

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

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

  1. <?xml version="1.0" encoding="utf-8" ?>
  2. <configuration>
  3. <Book>
  4. <Name>appsettings.xml book name</Name>
  5. <Authors name="0">appsettings.xml author name A</Authors>
  6. <Authors name="1">appsettings.xml author name B</Authors>
  7. <Bookmark>
  8. <Remarks>appsettings.xml bookmark remarks</Remarks>
  9. </Bookmark>
  10. </Book>
  11. </configuration>

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

INI配置提供程序

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

Install-Package Microsoft.Extensions.Configuration.Ini

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

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

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

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

环境变量配置提供程序

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

Install-Package Microsoft.Extensions.Configuration.EnvironmentVariables

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

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

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

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

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

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

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

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

  1. # 注意,这里的 My_ 是前缀
  2. setx My_Book__Name "Environment variables book name" /M
  3. setx My_Book__Authors__0 "Environment variables book author A" /M
  4. setx My_Book__Authors__1 "Environment variables book author B" /M
  5. 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 中的配置只用于开发环境,并且在该文件中设置的环境变量会覆盖在系统环境中设置的变量。

  1. {
  2. "WebApplication": {
  3. "commandName": "Project",
  4. "dotnetRunMessages": "true",
  5. "launchBrowser": true,
  6. "launchUrl": "swagger",
  7. "applicationUrl": "http://localhost:5000", // 设置环境变量 ASPNETCORE_URLS
  8. "environmentVariables": {
  9. "ASPNETCORE_ENVIRONMENT": "Development",
  10. "My_Book__Name": "launchSettings.json Environment variables book name",
  11. "My_Book__Authors__0": "launchSettings.json Environment variables book author A",
  12. "My_Book__Authors__1": "launchSettings.json Environment variables book author B",
  13. "My_Book__Bookmark__Remarks": "launchSettings.json Environment variables bookmark remarks"
  14. }
  15. }
  16. }

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

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

  1. $ systemd-escape http://localhost:5001
  2. http:--localhost:5001

命令行配置提供程序

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

Install-Package Microsoft.Extensions.Configuration.CommandLine

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

  1. public static IHostBuilder CreateHostBuilder(string[] args) =>
  2. Host.CreateDefaultBuilder(args)
  3. .ConfigureAppConfiguration((context, config) =>
  4. {
  5. config.AddCommandLine(args);
  6. });

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

使用=

  1. 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"

使用/

  1. 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"

使用--

  1. 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

接下来我们来映射一下:

  1. public static IHostBuilder CreateHostBuilder(string[] args) =>
  2. Host.CreateDefaultBuilder(args)
  3. .ConfigureAppConfiguration((context, config) =>
  4. {
  5. var switchMappings = new Dictionary<string, string>
  6. {
  7. ["--bn"] = "Book:Name",
  8. ["-ba0"] = "Book:Authors:0",
  9. ["--ba1"] = "Book:Authors:1",
  10. ["--bmr"] = "Book:Bookmark:Remarks"
  11. };
  12. config.AddCommandLine(args, switchMappings);
  13. });

然后以命令行命令启动:

  1. 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添加内存配置:

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

主机(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 来查看。

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

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

配置体系

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

IConfigurationSource

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

  1. public interface IConfigurationSource
  2. {
  3. IConfigurationProvider Build(IConfigurationBuilder builder);
  4. }

IConfigurationProvider

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

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

  1. public interface IConfigurationProvider
  2. {
  3. // 获取指定父路径下的直接子节点Key,然后 Concat(earlierKeys) 一同返回
  4. IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath);
  5. // 当该配置提供程序支持更改追踪(change tracking)时,会返回 change token
  6. // 否则,返回 null
  7. IChangeToken GetReloadToken();
  8. // 加载配置
  9. void Load();
  10. // 设置 key:value
  11. void Set(string key, string value);
  12. // 尝试获取指定 key 的 value
  13. bool TryGet(string key, out string value);
  14. }
  15. public abstract class ConfigurationProvider : IConfigurationProvider
  16. {
  17. // 包含了该配置提供程序的所有叶子节点的配置项
  18. protected IDictionary<string, string> Data { get; set; }
  19. protected ConfigurationProvider() { }
  20. // 从 Data 中查找指定父路径下的直接子节点Key,然后 Concat(earlierKeys) 一同返回
  21. public virtual IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath) { }
  22. public IChangeToken GetReloadToken() { }
  23. // 将配置项赋值到 Data 中
  24. public virtual void Load() { }
  25. protected void OnReload() { }
  26. // 设置 Data key:value
  27. public virtual void Set(string key, string value) { }
  28. public override string ToString() { }
  29. // 尝试从 Data 中获取指定 key 的 value
  30. public virtual bool TryGet(string key, out string value) { }
  31. }

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

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

IConfigurationBuilder

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

ConfigurationBuilder实现了IConfigurationBuilder接口:

  1. public class ConfigurationBuilder : IConfigurationBuilder
  2. {
  3. public IList<IConfigurationSource> Sources { get; } = new List<IConfigurationSource>();
  4. public IDictionary<string, object> Properties { get; } = new Dictionary<string, object>();
  5. public IConfigurationBuilder Add(IConfigurationSource source)
  6. {
  7. if (source == null)
  8. {
  9. throw new ArgumentNullException(nameof(source));
  10. }
  11. Sources.Add(source);
  12. return this;
  13. }
  14. public IConfigurationRoot Build()
  15. {
  16. var providers = new List<IConfigurationProvider>();
  17. foreach (IConfigurationSource source in Sources)
  18. {
  19. IConfigurationProvider provider = source.Build(this);
  20. providers.Add(provider);
  21. }
  22. return new ConfigurationRoot(providers);
  23. }
  24. }

IConfiguration

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

GetValue

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

  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration)
  4. {
  5. Configuration = configuration;
  6. }
  7. public IConfiguration Configuration { get; }
  8. public void ConfigureServices(IServiceCollection services)
  9. {
  10. var bookName = Configuration.GetValue<string>("Book:Name", defaultValue: "Unknown");
  11. Console.WriteLine(bookName);
  12. }
  13. }

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

GetSection

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

  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration)
  4. {
  5. Configuration = configuration;
  6. }
  7. public IConfiguration Configuration { get; }
  8. public void ConfigureServices(IServiceCollection services)
  9. {
  10. // 返回的 section 永远不会为 null
  11. IConfigurationSection bookSection = Configuration.GetSection(BookOptions.Book);
  12. IConfigurationSection bookmarkSection = bookSection.GetSection("Bookmark");
  13. // or
  14. //IConfigurationSection bookmarkSection = Configuration.GetSection("Book:Bookmark");
  15. var remarks = bookmarkSection["Remarks"];
  16. Console.WriteLine(remarks);
  17. }
  18. }

GetChildren

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

  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration)
  4. {
  5. Configuration = configuration;
  6. }
  7. public IConfiguration Configuration { get; }
  8. public void ConfigureServices(IServiceCollection services)
  9. {
  10. // children 包含了 Name、Bookmark、Authors
  11. var children = Configuration.GetSection(BookOptions.Book).GetChildren();
  12. foreach (var child in children)
  13. {
  14. Console.WriteLine($"Key: {child.Key}\tValue: {child.Value}");
  15. }
  16. }
  17. }

Exists

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

  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration)
  4. {
  5. Configuration = configuration;
  6. }
  7. public IConfiguration Configuration { get; }
  8. public void ConfigureServices(IServiceCollection services)
  9. {
  10. IConfigurationSection bookSection = Configuration.GetSection(BookOptions.Book);
  11. if (bookSection.Exists())
  12. {
  13. var notExistSection = bookSection.GetSection("NotExist");
  14. if (!notExistSection.Exists())
  15. {
  16. Console.WriteLine("Book:NotExist");
  17. }
  18. }
  19. }
  20. }

这里分析一下Exists的源码:

  1. public static class ConfigurationExtensions
  2. {
  3. public static bool Exists(this IConfigurationSection section)
  4. {
  5. if (section == null)
  6. {
  7. return false;
  8. }
  9. return section.Value != null || section.GetChildren().Any();
  10. }
  11. }

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

Get

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

  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration)
  4. {
  5. Configuration = configuration;
  6. }
  7. public IConfiguration Configuration { get; }
  8. public void ConfigureServices(IServiceCollection services)
  9. {
  10. var book = Configuration.GetSection(BookOptions.Book).Get<BookOptions>();
  11. Console.WriteLine($"Book Name: {book.Name}" +
  12. $"{Environment.NewLine}Bookmark Remarks:{book.Bookmark.Remarks}" +
  13. $"{Environment.NewLine}Book Authors: {string.Join(" & ", book.Authors)}");
  14. }
  15. }

Bind

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

  1. public class Startup
  2. {
  3. public Startup(IConfiguration configuration)
  4. {
  5. Configuration = configuration;
  6. }
  7. public IConfiguration Configuration { get; }
  8. public void ConfigureServices(IServiceCollection services)
  9. {
  10. var book = new BookOptions();
  11. Configuration.GetSection(BookOptions.Book).Bind(book);
  12. Console.WriteLine($"Book Name: {book.Name}" +
  13. $"{Environment.NewLine}Bookmark Remarks:{book.Bookmark.Remarks}" +
  14. $"{Environment.NewLine}Book Authors: {string.Join(" & ", book.Authors)}");
  15. }
  16. }

IConfigurationRoot

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

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

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

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

  1. public class ConfigurationRoot : IConfigurationRoot, IDisposable
  2. {
  3. private readonly IList<IConfigurationProvider> _providers;
  4. public ConfigurationRoot(IList<IConfigurationProvider> providers)
  5. {
  6. // 该构造函数内代码有删减
  7. _providers = providers;
  8. foreach (IConfigurationProvider p in providers)
  9. {
  10. p.Load();
  11. }
  12. }
  13. public void Reload()
  14. {
  15. foreach (IConfigurationProvider provider in _providers)
  16. {
  17. provider.Load();
  18. }
  19. // 此处删减了部分代码
  20. }
  21. }

IConfigurationSection

IConfigurationSection表示配置的子节点。

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

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

  • 当我们通过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

  1. public class AppDbContext : DbContext
  2. {
  3. public AppDbContext(DbContextOptions options)
  4. : base(options) { }
  5. public virtual DbSet<JsonConfiguration> JsonConfigurations { get; set; }
  6. }
  7. public class JsonConfiguration
  8. {
  9. [Key]
  10. public string Key { get; set; }
  11. public string Value { get; set; }
  12. }

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

  1. public class EFConfigurationSource : IConfigurationSource
  2. {
  3. private readonly Action<DbContextOptionsBuilder> _optionsAction;
  4. public EFConfigurationSource(Action<DbContextOptionsBuilder> optionsAction)
  5. {
  6. _optionsAction = optionsAction;
  7. }
  8. public IConfigurationProvider Build(IConfigurationBuilder builder)
  9. {
  10. return new EFConfigurationProvider(_optionsAction);
  11. }
  12. }

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

  1. public class EFConfigurationProvider : ConfigurationProvider
  2. {
  3. public EFConfigurationProvider(Action<DbContextOptionsBuilder> optionsAction)
  4. {
  5. OptionsAction = optionsAction;
  6. }
  7. Action<DbContextOptionsBuilder> OptionsAction { get; }
  8. public override void Load()
  9. {
  10. var builder = new DbContextOptionsBuilder<AppDbContext>();
  11. OptionsAction(builder);
  12. using var dbContext = new AppDbContext(builder.Options);
  13. dbContext.Database.EnsureCreated();
  14. // 如果没有任何配置则添加默认配置
  15. if (!dbContext.JsonConfigurations.Any())
  16. {
  17. CreateAndSaveDefaultValues(dbContext);
  18. }
  19. // 将配置项转换为键值对(key和value均为字符串类型)
  20. Data = EFJsonConfigurationParser.Parse(dbContext.JsonConfigurations);
  21. }
  22. private static void CreateAndSaveDefaultValues(AppDbContext dbContext)
  23. {
  24. dbContext.JsonConfigurations.AddRange(new[]
  25. {
  26. new JsonConfiguration
  27. {
  28. Key = "Book",
  29. Value = JsonSerializer.Serialize(
  30. new BookOptions()
  31. {
  32. Name = "ef configuration book name",
  33. Authors = new List<string>
  34. {
  35. "ef configuration book author A",
  36. "ef configuration book author B"
  37. },
  38. Bookmark = new BookmarkOptions
  39. {
  40. Remarks = "ef configuration bookmark Remarks"
  41. }
  42. })
  43. }
  44. });
  45. dbContext.SaveChanges();
  46. }
  47. }
  48. internal class EFJsonConfigurationParser
  49. {
  50. private EFJsonConfigurationParser() { }
  51. private readonly IDictionary<string, string> _data = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  52. private readonly Stack<string> _context = new();
  53. private string _currentPath;
  54. public static IDictionary<string, string> Parse(DbSet<JsonConfiguration> inputs)
  55. => new EFJsonConfigurationParser().ParseJsonConfigurations(inputs);
  56. private IDictionary<string, string> ParseJsonConfigurations(DbSet<JsonConfiguration> inputs)
  57. {
  58. _data.Clear();
  59. if(inputs?.Any() != true)
  60. {
  61. return _data;
  62. }
  63. var jsonDocumentOptions = new JsonDocumentOptions
  64. {
  65. CommentHandling = JsonCommentHandling.Skip,
  66. AllowTrailingCommas = true,
  67. };
  68. foreach (var input in inputs)
  69. {
  70. ParseJsonConfiguration(input, jsonDocumentOptions);
  71. }
  72. return _data;
  73. }
  74. private void ParseJsonConfiguration(JsonConfiguration input, JsonDocumentOptions options)
  75. {
  76. if (string.IsNullOrWhiteSpace(input.Key))
  77. throw new FormatException($"The key {input.Key} is invalid.");
  78. var jsonValue = $"{{\"{input.Key}\": {input.Value}}}";
  79. using var doc = JsonDocument.Parse(jsonValue, options);
  80. if (doc.RootElement.ValueKind != JsonValueKind.Object)
  81. throw new FormatException($"Unsupported JSON token '{doc.RootElement.ValueKind}' was found.");
  82. VisitElement(doc.RootElement);
  83. }
  84. private void VisitElement(JsonElement element)
  85. {
  86. foreach (JsonProperty property in element.EnumerateObject())
  87. {
  88. EnterContext(property.Name);
  89. VisitValue(property.Value);
  90. ExitContext();
  91. }
  92. }
  93. private void VisitValue(JsonElement value)
  94. {
  95. switch (value.ValueKind)
  96. {
  97. case JsonValueKind.Object:
  98. VisitElement(value);
  99. break;
  100. case JsonValueKind.Array:
  101. var index = 0;
  102. foreach (var arrayElement in value.EnumerateArray())
  103. {
  104. EnterContext(index.ToString());
  105. VisitValue(arrayElement);
  106. ExitContext();
  107. index++;
  108. }
  109. break;
  110. case JsonValueKind.Number:
  111. case JsonValueKind.String:
  112. case JsonValueKind.True:
  113. case JsonValueKind.False:
  114. case JsonValueKind.Null:
  115. var key = _currentPath;
  116. if (_data.ContainsKey(key))
  117. throw new FormatException($"A duplicate key '{key}' was found.");
  118. _data[key] = value.ToString();
  119. break;
  120. default:
  121. throw new FormatException($"Unsupported JSON token '{value.ValueKind}' was found.");
  122. }
  123. }
  124. private void EnterContext(string context)
  125. {
  126. _context.Push(context);
  127. _currentPath = ConfigurationPath.Combine(_context.Reverse());
  128. }
  129. private void ExitContext()
  130. {
  131. _context.Pop();
  132. _currentPath = ConfigurationPath.Combine(_context.Reverse());
  133. }
  134. }

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

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

  1. public static class EntityFrameworkExtensions
  2. {
  3. public static IConfigurationBuilder AddEFConfiguration(
  4. this IConfigurationBuilder builder,
  5. Action<DbContextOptionsBuilder> optionsAction)
  6. {
  7. return builder.Add(new EFConfigurationSource(optionsAction));
  8. }
  9. }

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

  1. public static IHostBuilder CreateHostBuilder(string[] args) =>
  2. Host.CreateDefaultBuilder(args)
  3. .ConfigureAppConfiguration((context, config) =>
  4. {
  5. config.AddEFConfiguration(options => options.UseInMemoryDatabase("configdb"));
  6. })
  7. .ConfigureWebHostDefaults(webBuilder =>
  8. {
  9. webBuilder.UseStartup<Startup>();
  10. });

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

其他

查看所有配置项

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

  1. public static void Main(string[] args)
  2. {
  3. var host = CreateHostBuilder(args).Build();
  4. var config = host.Services.GetRequiredService<IConfiguration>();
  5. foreach (var c in config.AsEnumerable())
  6. {
  7. Console.WriteLine(c.Key + " = " + c.Value);
  8. }
  9. host.Run();
  10. }

通过委托配置选项

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

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.Configure<BookOptions>(book =>
  4. {
  5. book.Name = "delegate book name";
  6. book.Authors = new List<string> { "delegate book author A", "delegate book author A" };
  7. book.Bookmark = new BookmarkOptions { Remarks = "delegate bookmark reamarks" };
  8. });
  9. }

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

注意事项

配置Key

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

配置Value

  • 均被保存为字符串

理解ASP.NET Core - 配置(Configuration)的更多相关文章

  1. 理解ASP.NET Core - [01] Startup

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 准备工作:一份ASP.NET Core Web API应用程序 当我们来到一个陌生的环境,第一 ...

  2. 理解ASP.NET Core - [03] Dependency Injection

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 依赖注入 什么是依赖注入 简单说,就是将对象的创建和销毁工作交给DI容器来进行,调用方只需要接 ...

  3. 理解ASP.NET Core - [04] Host

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 本文会涉及部分 Host 相关的源码,并会附上 github 源码地址,不过为了降低篇幅,我会 ...

  4. 理解ASP.NET Core - 选项(Options)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 Options绑定 上期我们已经聊过了配置(IConfiguration),今天我们来聊一聊O ...

  5. 理解ASP.NET Core - 文件服务器(File Server)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 提供静态文件 静态文件默认存放在 Web根目录(Web Root) 中,路径为 项目根目录(C ...

  6. 理解ASP.NET Core - 日志(Logging)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 快速上手 添加日志提供程序 在文章主机(Host)中,讲到Host.CreateDefault ...

  7. 理解ASP.NET Core - 基于JwtBearer的身份认证(Authentication)

    注:本文隶属于<理解ASP.NET Core>系列文章,请查看置顶博客或点击此处查看全文目录 在开始之前,如果你还不了解基于Cookie的身份认证,那么建议你先阅读<基于Cookie ...

  8. ASP.NET Core配置Kestrel 网址Urls

    ASP.NET Core中如何配置Kestrel Urls呢,大家可能都知道使用UseUrls() 方法来配置. 今天给介绍全面的ASP.NET Core 配置 Urls,使用多种方式配置Urls.让 ...

  9. ASP.NET Core 配置 EF 框架服务 - ASP.NET Core 基础教程 - 简单教程,简单编程

    原文:ASP.NET Core 配置 EF 框架服务 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 配置 EF 框架服务 上一章节中我们了解了 Entity ...

随机推荐

  1. ffmpeg 常用知识点收集

    ffmpeg 常用知识点收集 一.基础简介 FFmpeg是一个自由软件,可以运行音频和视频多种格式的录影.转换.流功能,包含了libavcodec ─这是一个用于多个项目中音频和视频的解码器库,以及l ...

  2. 如何将eclipse中项目部署到tomcat

    项目路径: \tmp0\wtpwebapps\test 复制test目录到 D:\software_install\apache-tomcat-8.0.33-windows-x64\apache-to ...

  3. 前端云原生,以 Kubernetes 为基础设施的高可用 SSR(Vue.js) 渲染微服务初探(开源 Demo)

    背景 笔者在逛掘金的时候,有幸看到掘友狼族小狈开源的 genesis - 一个可以支持 SSR 和 CSR 渲染的微服务解决方案.总体来说思想不错,但是基于 Kubernetes 云原生部署方面一直没 ...

  4. RabitMq过期时间TTL

    第一种:给消息设置过期时间 启动一个插件 @Bean public DirectExchange DirectExchange() { return new DirectExchange(" ...

  5. tensorflow 单机多卡 官方cifar10例程

    测试了官方历程,看没有问题,加上时间紧任务重,就不深究了. 官方tutorials:https://www.tensorflow.org/tutorials/images/deep_cnn githu ...

  6. Qt之类反射机制

    在java语言中,可以使用getObject(String)函数,从类型直接构建新的对象. 而在C++中是没有这种机制的,Qt虽然提供了元对象机制,但只可以获取对象的类名,不能反向构建. 所以搜索一下 ...

  7. Mybatis-plus<二>通用CRUD,分页

    Mybatis-plus<二>通用CRUD,分页 与博客Mybatis-plus<一>为同一个Springboot项目. Demo GitHub下载地址:https://git ...

  8. angularjs实现购物清单

    HTML: 1:要定义ng-app,在html上定义ng-app="App"; 2:在body上定义ng-controller="ToDoCtrl" 3: &l ...

  9. Linux上安装服务器监视工具,名为Scout_Realtime。

    如何从浏览器监视Linux服务器和进程指标 在服务器上安装Ruby 1.9.3+ sudo yum -y install rubygems-devel 在Linux系统上安装了Ruby之后,现在可以使 ...

  10. Java中int和short的转化

    例子[1]: 第一种情况: short a = 1; a = a + 1; // 这一步会报错 System.out.print(a); 编译器会报错,原因如下: 第二种情况: short a = 1 ...