CodeSnippet.Cn
代码片段
Csharp
架构设计
.NetCore
西班牙语
kubernetes
MySql
Redis
Algorithm
Ubuntu
Linux
Other
.NetMvc
VisualStudio
Git
pm
Python
WPF
java
Plug-In
分布式
CSS
微服务架构
JavaScript
DataStructure
Shared
理解ASP.NET Core - 配置(Configuration)
0
.NetCore
小笨蛋
发布于:2022年09月13日
更新于:2022年12月26日
138
#custom-toc-container
### 配置提供程序 在.NET中,配置是通过多种配置提供程序来提供的,包括以下几种: - 文件配置提供程序 - 环境变量配置提供程序 - 命令行配置提供程序 - Azure应用配置提供程序 - Azure Key Vault 配置提供程序 - Key-per-file配置提供程序 - 内存配置提供程序 - 应用机密(机密管理器) - 自定义配置提供程序 为了方便大家后续了解配置,这里先简单提一下选项(Options),它是用于以强类型的方式对程序配置信息进行访问的一种方式。接下来的示例中,我会添加一个简单的配置Book,结构如下: ```csharp public class BookOptions { public const string Book = "Book"; public string Name { get; set; } public BookmarkOptions Bookmark { get; set; } public List
Authors { get; set; } } public class BookmarkOptions { public string Remarks { get; set; } } ``` 然后我们在Startup.ConfigureServices中使用IConfiguration进行配置的读取,并显示在控制台中,如下: ```csharp public void ConfigureServices(IServiceCollection services) { var book = Configuration.GetSection(BookOptions.Book).Get
(); 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文件即可: ```csharp 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 中添加如下配置: ```csharp { "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文件即可: ```csharp public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, config) => { config.AddXmlFile("appsettings.xml", optional: true, reloadOnChange: true); }); ``` 你可以在 appsettings.xml 中添加如下配置: ```csharp
appsettings.xml book name
appsettings.xml author name A
appsettings.xml author name B
appsettings.xml bookmark remarks
在 .NET 6 中,我们就不用手动添加 name 属性来指定索引了,它会自动进行索引编号。 ##### INI配置提供程序 通过IniConfigurationProvider在运行时从Ini文件中加载配置。 > Install-Package Microsoft.Extensions.Configuration.Ini 同样的,只需调用AddIniFile扩展方法添加Ini文件即可: ```csharp public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, config) => { config.AddIniFile("appsettings.ini", optional: true, reloadOnChange: true); }); ``` 你可以在 appsettings.ini 中添加如下配置 ```csharp [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扩展方法添加环境变量即可: ```csharp 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去设置环境变量(记得以管理员身份运行哦): ```csharp # 注意,这里的 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 中的配置只用于开发环境,并且在该文件中设置的环境变量会覆盖在系统环境中设置的变量。** ```csharp { "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工具: ```csharp $ systemd-escape http://localhost:5001 http:--localhost:5001 ``` #### 命令行配置提供程序 通过CommandLineConfigurationProvider在运行时从命令行参数键值对中加载配置。 > Install-Package Microsoft.Extensions.Configuration.CommandLine 通过调用AddCommandLine扩展方法,并传入参数args: ```csharp public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, config) => { config.AddCommandLine(args); }); ``` 有三种设置命令行参数的方式: 使用=: ```csharp 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" ``` 使用/: ```csharp 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" ``` 使用--: ```csharp 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 接下来我们来映射一下: ```csharp public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, config) => { var switchMappings = new Dictionary
{ ["--bn"] = "Book:Name", ["-ba0"] = "Book:Authors:0", ["--ba1"] = "Book:Authors:1", ["--bmr"] = "Book:Bookmark:Remarks" }; config.AddCommandLine(args, switchMappings); }); ``` 然后以命令行命令启动: ```csharp 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添加内存配置: ```csharp public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, config) => { config.AddInMemoryCollection(new Dictionary
{ ["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 来查看。 如果想要添加额外配置文件,但是仍然想要环境变量或命令行参数优先,则可以类似这样做: ```csharp 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实例: ```csharp public interface IConfigurationSource { IConfigurationProvider Build(IConfigurationBuilder builder); } ``` #### IConfigurationProvider IConfigurationProvider负责实现配置的设置、读取、重载等功能,并以键值对形式提供配置。 **所有配置提供程序**均建议继承于抽象类ConfigurationProvider,该类实现了接口IConfigurationProvider ```csharp public interface IConfigurationProvider { // 获取指定父路径下的直接子节点Key,然后 Concat(earlierKeys) 一同返回 IEnumerable
GetChildKeys(IEnumerable
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
Data { get; set; } protected ConfigurationProvider() { } // 从 Data 中查找指定父路径下的直接子节点Key,然后 Concat(earlierKeys) 一同返回 public virtual IEnumerable
GetChildKeys(IEnumerable
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) { } } ``` ```csharp Data包含了该配置提供程序的所有叶子节点的配置项。拿上方的Book示例来说,该Data包含“Book:Name”、“Book:Authors:0”、“Book:Authors:1”和“Book:Bookmark:Remarks”这4个Key。 ``` 另外,你可能还会见到一个名为ChainedConfigurationProvider的配置提供程序,它可以将一个已存在的IConfiguration实例,作为配置提供程序添加到另一个IConfiguration中。例如HostConfiguration流转到AppConfiguration就使用了这个。 #### IConfigurationBuilder ```csharp public interface IConfigurationBuilder { // 存放用于该 Builder 的 Sources 列表中各个元素的共享字典 IDictionary
Properties { get; } // 已注册的 IConfigurationSource 列表 IList
Sources { get; } // 将 IConfigurationSource 添加到 Sources 中 IConfigurationBuilder Add(IConfigurationSource source); // 通过 Sources 构建配置提供程序实例,并创建 IConfigurationRoot 实例 IConfigurationRoot Build(); } ``` 类ConfigurationBuilder实现了IConfigurationBuilder接口: ```csharp public class ConfigurationBuilder : IConfigurationBuilder { public IList
Sources { get; } = new List
(); public IDictionary
Properties { get; } = new Dictionary
(); 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
(); foreach (IConfigurationSource source in Sources) { IConfigurationProvider provider = source.Build(this); providers.Add(provider); } return new ConfigurationRoot(providers); } } ``` #### IConfiguration ```csharp public interface IConfiguration { // 获取或设置指定配置 key 的 value string this[string key] { get; set; } // 获取当前配置节点的 直接 子节点列表 IEnumerable
GetChildren(); // 获取监控配置发生更改的 token IChangeToken GetReloadToken(); // 获取指定Key的配置子节点 IConfigurationSection GetSection(string key); } ``` ##### GetValue 通过IConfiguration的扩展方法ConfigurationBinder.GetValue,可以以类似字典的方式,读取某个Key对应的Value。 ```csharp public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public void ConfigureServices(IServiceCollection services) { var bookName = Configuration.GetValue
("Book:Name", defaultValue: "Unknown"); Console.WriteLine(bookName); } } ``` 该扩展的实质(默认实现)是在底层通过调用IConfigurationProvider.TryGet方法,读取ConfigurationProvider.Data字典中的键值对。所以,只能通过该扩展方法读取叶子节点的配置值。 ##### GetSection 通过IConfiguration.GetSection方法,可以获取到指定Key的配置子节点: ```csharp 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方法,可以获取到当前配置节点的直接子节点列表 ```csharp 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}\tValue: {child.Value}"); } } } ``` ##### Exists 前面提到了,Configuration.GetSection永远不会返回null,那么我们如何判断该 Section 是否真的存在呢?这就要用到扩展方法ConfigurationExtensions.Exists了: ```csharp 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的源码: ```csharp 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方法,可以将配置以强类型的方式绑定到选项对象上: ```csharp 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
(); 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 方法,可以将配置以强类型的方式绑定到已存在的选项对象上: ```csharp 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格式的文档都会有一个根节点(如上方示例中的
),还可以包含多个子节点(如上方示例中的
、
等)。 ```csharp public interface IConfigurationRoot : IConfiguration { // 存放了当前应用程序的所有配置提供程序 IEnumerable
Providers { get; } // 强制从配置提供程序中重载配置 void Reload(); } ``` 类ConfigurationRoot实现了IConfigurationRoot接口,下面就着重看一下Reload方法的实现: > Startup构造函数中注入的IConfiguration其实就是ConfigurationRoot的实例。 ```csharp public class ConfigurationRoot : IConfigurationRoot, IDisposable { private readonly IList
_providers; public ConfigurationRoot(IList
providers) { // 该构造函数内代码有删减 _providers = providers; foreach (IConfigurationProvider p in providers) { p.Load(); } } public void Reload() { foreach (IConfigurationProvider provider in _providers) { provider.Load(); } // 此处删减了部分代码 } } ``` #### IConfigurationSection IConfigurationSection表示配置的子节点。 ```csharp 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格式的配置。该表有Key和Value两个字段,Key对应例子中的“Book”,而Value则是“Book”对应值的Json字符串。 首先,装一下Nuget包: > Install-Package Microsoft.EntityFrameworkCore.InMemory 然后定义自己的DbContext——AppDbContext: ```csharp public class AppDbContext : DbContext { public AppDbContext(DbContextOptions options) : base(options) { } public virtual DbSet
JsonConfigurations { get; set; } } public class JsonConfiguration { [Key] public string Key { get; set; } public string Value { get; set; } } ``` 接下来,通过EFConfigurationSource来构建EFConfigurationProvider实例: ```csharp public class EFConfigurationSource : IConfigurationSource { private readonly Action
_optionsAction; public EFConfigurationSource(Action
optionsAction) { _optionsAction = optionsAction; } public IConfigurationProvider Build(IConfigurationBuilder builder) { return new EFConfigurationProvider(_optionsAction); } } ``` 接着,就是EFConfigurationProvider的实现了,逻辑类似于Json文件配置提供程序,只不过配置来源于EF而不是Json文件: ```csharp public class EFConfigurationProvider : ConfigurationProvider { public EFConfigurationProvider(Action
optionsAction) { OptionsAction = optionsAction; } Action
OptionsAction { get; } public override void Load() { var builder = new DbContextOptionsBuilder
(); 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
{ "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
_data = new SortedDictionary
(StringComparer.OrdinalIgnoreCase); private readonly Stack
_context = new(); private string _currentPath; public static IDictionary
Parse(DbSet
inputs) => new EFJsonConfigurationParser().ParseJsonConfigurations(inputs); private IDictionary
ParseJsonConfigurations(DbSet
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的格式将该配置提供程序的添加封装为扩展方法: ```csharp public static class EntityFrameworkExtensions { public static IConfigurationBuilder AddEFConfiguration( this IConfigurationBuilder builder, Action
optionsAction) { return builder.Add(new EFConfigurationSource(optionsAction)); } } ``` 这时,我们就可以使用扩展方法添加EFConfigurationProvider了: ```csharp public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, config) => { config.AddEFConfiguration(options => options.UseInMemoryDatabase("configdb")); }) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup
(); }); ``` 最后,你可以试着读取一下Book配置了,看看是不是如咱们所期望的那样,读取到EF中的配置呢?这里,我就不再演示了。 ### 其他 #### 查看所有配置项 通过扩展方法ConfigurationExtensions.AsEnumerable,来查看所有配置项: ```csharp public static void Main(string[] args) { var host = CreateHostBuilder(args).Build(); var config = host.Services.GetRequiredService
(); foreach (var c in config.AsEnumerable()) { Console.WriteLine(c.Key + " = " + c.Value); } host.Run(); } ``` #### 通过委托配置选项 除了可以通过配置提供程序来提供配置外,也可以通过委托来提供配置: ```csharp public void ConfigureServices(IServiceCollection services) { services.Configure
(book => { book.Name = "delegate book name"; book.Authors = new List
{ "delegate book author A", "delegate book author A" }; book.Bookmark = new BookmarkOptions { Remarks = "delegate bookmark reamarks" }; }); } ``` ### 注意事项 **配置Key** - 不区分大小写。例如Name和name被视为等效的。 - 配置提供程序有很多种,如果在多个提供程序中添加了某个配置项,那么,只有在最后一个提供程序中配置的才会生效。 - 分层键: - 在环境变量中,由于冒号(:)无法适用于所有平台,所以要使用全平台均支持的双下划线(__),它会在程序中自动转换为冒号(:) - 在其他类型的配置中,一般均使用冒号(:)分隔符即可 - ConfigurationPath类提供了一些辅助方法。 **配置Value** - 均被保存为字符串
这里⇓感觉得写点什么,要不显得有点空,但还没想好写什么...
返回顶部
About
京ICP备13038605号
© 代码片段 2024