AI智能
改变未来

理解ASP.NET Core – 配置(Configuration)

注:本文隶属于《理解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

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

  • optional

    bool

    类型,指示该文件是否是可选的。如果该参数为

    false

    ,但是指定的文件又不存在,则会报错。

  • reloadOnChange

    bool

    类型,指示该文件发生更改时,是否要重新加载配置。

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 nameAuthors:0=appsettings.ini book author AAuthors: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 平台下,可以通过

set

setx

命令进行环境变量配置,不过:

  • set

    命令设置的环境变量是临时的,仅在当前进程有效,这个进程就是当前cmd窗口启动的。也就是说,当你打开一个cmd窗口时,通过

    set

    命令设置了环境变量,然后通过

    dotnet xxx.dll

    启动了你的应用程序,是可以读取到环境变量的,但是在该cmd窗口之外,例如通过VS启动应用程序,是无法读取到该环境变量的。

  • setx

    命令设置的环境变量是持久化的。可选的添加

    /M

    开关,表示将该环境变量配置到系统环境中(需要管理员权限),否则,将添加到用户环境中。

我更喜欢通过

setx

去设置环境变量(记得以管理员身份运行哦):

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

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

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

当没有向

AddEnvironmentVariables

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

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

在 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:5001http:--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..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);});

配置体系

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

IConfigurationSource

IConfigurationProvider

等接口。

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// 否则,返回 nullIChangeToken GetReloadToken();// 加载配置void Load();// 设置 key:valuevoid Set(string key, string value);// 尝试获取指定 key 的 valuebool 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:valuepublic virtual void Set(string key, string value) { }public override string ToString() { }// 尝试从 Data 中获取指定 key 的 valuepublic 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 的 valuestring this[string key] { get; set; }// 获取当前配置节点的 直接 子节点列表IEnumerable<IConfigurationSection> GetChildren();// 获取监控配置发生更改的 tokenIChangeToken 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 永远不会为 nullIConfigurationSection 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、Authorsvar children = Configuration.GetSection(BookOptions.Book).GetChildren();foreach (var child in children){Console.WriteLine($"Key: {child.Key}\\tValue: {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{// 该子节点在其父节点中所表示的 keystring Key { get; }// 该子节点在配置中的全路径(从根节点开始,到当前节点的路径)string Path { get; }// 该子节点的 value。如果该子节点下存在孩子节点,则其始终为 nullstring 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格式的配置。该表有

Key

Value

两个字段,

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

  • 不区分大小写。例如
    Name

    name

    被视为等效的。

  • 配置提供程序有很多种,如果在多个提供程序中添加了某个配置项,那么,只有在最后一个提供程序中配置的才会生效。
  • 分层键:在环境变量中,由于冒号(
    :

    )无法适用于所有平台,所以要使用全平台均支持的双下划线(

    __

    ),它会在程序中自动转换为冒号(

    :

  • 在其他类型的配置中,一般均使用冒号(
    :

    )分隔符即可

  • ConfigurationPath

    类提供了一些辅助方法。

  • 配置Value

    • 均被保存为字符串
    赞(0) 打赏
    未经允许不得转载:爱站程序员基地 » 理解ASP.NET Core – 配置(Configuration)