|
配置提供程序
在.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($&#34;Book Name: {book.Name}&#34; +
$&#34;{Environment.NewLine}Bookmark Remarks:{book.Bookmark.Remarks}&#34; +
$&#34;{Environment.NewLine}Book Authors: {string.Join(&#34; & &#34;, book.Authors)}&#34;);
}
接下来,就挑几个常用的配置提供程序来详细讲解一下。
文件配置提供程序
顾名思义,就是从文件中加载配置。文件细分为
- 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(&#34;appsettings.json&#34;, optional: true, reloadOnChange: true)
.AddJsonFile($&#34;appsettings.{env.EnvironmentName}.json&#34;, optional: true, reloadOnChange: true);
});
你可以在 appsetting.json 中添加如下配置:
{
&#34;Book&#34;: {
&#34;Name&#34;: &#34;appsettings.json book name&#34;,
&#34;Authors&#34;: [
&#34;appsettings.json author name A&#34;,
&#34;appsettings.json author name B&#34;
],
&#34;Bookmark&#34;: {
&#34;Remarks&#34;: &#34;appsettings.json bookmark remarks&#34;
}
}
}
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(&#34;appsettings.xml&#34;, optional: true, reloadOnChange: true);
});
你可以在 appsettings.xml 中添加如下配置:
<?xml version=&#34;1.0&#34; encoding=&#34;utf-8&#34; ?>
<configuration>
<Book>
<Name>appsettings.xml book name</Name>
<Authors name=&#34;0&#34;>appsettings.xml author name A</Authors>
<Authors name=&#34;1&#34;>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(&#34;appsettings.ini&#34;, 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: &#34;My_&#34;);
});
在添加环境变量时,通过指定参数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 &#34;Environment variables book name&#34; /M
setx My_Book__Authors__0 &#34;Environment variables book author A&#34; /M
setx My_Book__Authors__1 &#34;Environment variables book author B&#34; /M
setx My_Book__Bookmark__Remarks &#34;Environment variables bookmark remakrs&#34; /M
配置完环境变量后,一定要记得重启VS或cmd窗口,否则是无法读取到最新的环境变量值的 连接字符串前缀的特殊处理
当没有向AddEnvironmentVariables传入前缀时,默认也会针对含有以下前缀的环境变量进行特殊处理:

在 launchSettings.json 中配置环境变量
在 http://ASP.NET Core 模板项目中,会生成一个 launchSettings.json 文件,我们也可以在该文件中配置环境变量。
需要注意的是,launchSettings.json 中的配置只用于开发环境,并且在该文件中设置的环境变量会覆盖在系统环境中设置的变量。
{
&#34;WebApplication&#34;: {
&#34;commandName&#34;: &#34;Project&#34;,
&#34;dotnetRunMessages&#34;: &#34;true&#34;,
&#34;launchBrowser&#34;: true,
&#34;launchUrl&#34;: &#34;swagger&#34;,
&#34;applicationUrl&#34;: &#34;http://localhost:5000&#34;, // 设置环境变量 ASPNETCORE_URLS
&#34;environmentVariables&#34;: {
&#34;ASPNETCORE_ENVIRONMENT&#34;: &#34;Development&#34;,
&#34;My_Book__Name&#34;: &#34;launchSettings.json Environment variables book name&#34;,
&#34;My_Book__Authors__0&#34;: &#34;launchSettings.json Environment variables book author A&#34;,
&#34;My_Book__Authors__1&#34;: &#34;launchSettings.json Environment variables book author B&#34;,
&#34;My_Book__Bookmark__Remarks&#34;: &#34;launchSettings.json Environment variables bookmark remarks&#34;
}
}
}
虽然说在 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=&#34;Command line book name&#34; Book:Authors:0=&#34;Command line book author A&#34; Book:Authors:1=&#34;Command line book author B&#34; Book:Bookmark:Remarks=&#34;Command line bookmark remarks&#34;
使用/:
dotnet run /Book:Name &#34;Command line book name&#34; /Book:Authors:0 &#34;Command line book author A&#34; /Book:Authors:1 &#34;Command line book author B&#34; /Book:Bookmark:Remarks &#34;Command line bookmark remarks&#34;
使用--:
dotnet WebApplication5.dll --Book:Name &#34;Command line book name&#34; --Book:Authors:0 &#34;Command line book author A&#34; --Book:Authors:1 &#34;Command line book author B&#34; --Book:Bookmark:Remarks &#34;Command line bookmark remarks&#34;
交换映射
该功能是针对命令行配置参数进行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>
{
[&#34;--bn&#34;] = &#34;Book:Name&#34;,
[&#34;-ba0&#34;] = &#34;Book:Authors:0&#34;,
[&#34;--ba1&#34;] = &#34;Book:Authors:1&#34;,
[&#34;--bmr&#34;] = &#34;Book:Bookmark:Remarks&#34;
};
config.AddCommandLine(args, switchMappings);
});
然后以命令行命令启动:
dotnet run --bn &#34;Command line book name&#34; -ba0 &#34;Command line book author A&#34; /ba1 &#34;Command line book author B&#34; --bmr=&#34;Command line bookmark remarks&#34;
内存配置提供程序
通过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>
{
[&#34;Book:Name&#34;] = &#34;Memmory book name&#34;,
[&#34;Book:Authors:0&#34;] = &#34;Memory book author A&#34;,
[&#34;Book:Authors:1&#34;] = &#34;Memory book author B&#34;,
[&#34;Book:Bookmark:Remarks&#34;] = &#34;Memory bookmark remarks&#34;
});
});
主机(Host)中的默认配置优先级
约定:越后添加的配置提供程序优先级越高,优先级高的配置值会覆盖优先级低的配置值
在 主机(Host)中,我们介绍了Host的启动流程,根据默认的配置提供程序的添加顺序,默认的优先级从低到高为(我顺便将WebHost默认配置的也加进来了):
- 内存配置提供程序 环境变量配置提供程序(prefix: DOTNET_)
- 环境变量配置提供程序(prefix: ASPNETCORE_)
- JSON配置提供程序(appsettings.json)
- JSON配置提供程序(appsettings.{Environment}.json)
- 机密管理器(仅Windows)
- 环境变量配置提供程序(未限定前缀)
- 命令行配置提供程序
完整的配置提供程序列表可以通过 IConfigurationRoot.Providers 来查看。 如果想要添加额外配置文件,但是仍然想要环境变量或命令行参数优先,则可以类似这样做:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
config.AddJsonFile(&#34;my.json&#34;, 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
// 否则,返回 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>(&#34;Book:Name&#34;, defaultValue: &#34;Unknown&#34;);
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(&#34;Bookmark&#34;);
// or
//IConfigurationSection bookmarkSection = Configuration.GetSection(&#34;Book:Bookmark&#34;);
var remarks = bookmarkSection[&#34;Remarks&#34;];
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($&#34;Key: {child.Key}\tValue: {child.Value}&#34;);
}
}
}
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(&#34;NotExist&#34;);
if (!notExistSection.Exists())
{
Console.WriteLine(&#34;Book:NotExist&#34;);
}
}
}
}
这里分析一下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($&#34;Book Name: {book.Name}&#34; +
$&#34;{Environment.NewLine}Bookmark Remarks:{book.Bookmark.Remarks}&#34; +
$&#34;{Environment.NewLine}Book Authors: {string.Join(&#34; & &#34;, book.Authors)}&#34;);
}
}
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($&#34;Book Name: {book.Name}&#34; +
$&#34;{Environment.NewLine}Bookmark Remarks:{book.Bookmark.Remarks}&#34; +
$&#34;{Environment.NewLine}Book Authors: {string.Join(&#34; & &#34;, book.Authors)}&#34;);
}
}
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(&#34;Book:Name&#34;)获取到子节点时,Key为“Name”,Path为“Book:Name”,Value则为“Memmory book name”
- 当我们通过Configuration.GetSection(&#34;Book:Bookmark&#34;)获取到子节点时,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 = &#34;Book&#34;,
Value = JsonSerializer.Serialize(
new BookOptions()
{
Name = &#34;ef configuration book name&#34;,
Authors = new List<string>
{
&#34;ef configuration book author A&#34;,
&#34;ef configuration book author B&#34;
},
Bookmark = new BookmarkOptions
{
Remarks = &#34;ef configuration bookmark Remarks&#34;
}
})
}
});
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($&#34;The key {input.Key} is invalid.&#34;);
var jsonValue = $&#34;{{\&#34;{input.Key}\&#34;: {input.Value}}}&#34;;
using var doc = JsonDocument.Parse(jsonValue, options);
if (doc.RootElement.ValueKind != JsonValueKind.Object)
throw new FormatException($&#34;Unsupported JSON token &#39;{doc.RootElement.ValueKind}&#39; was found.&#34;);
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($&#34;A duplicate key &#39;{key}&#39; was found.&#34;);
_data[key] = value.ToString();
break;
default:
throw new FormatException($&#34;Unsupported JSON token &#39;{value.ValueKind}&#39; was found.&#34;);
}
}
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(&#34;configdb&#34;));
})
.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 + &#34; = &#34; + c.Value);
}
host.Run();
}
通过委托配置选项
除了可以通过配置提供程序来提供配置外,也可以通过委托来提供配置:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<BookOptions>(book =>
{
book.Name = &#34;delegate book name&#34;;
book.Authors = new List<string> { &#34;delegate book author A&#34;, &#34;delegate book author A&#34; };
book.Bookmark = new BookmarkOptions { Remarks = &#34;delegate bookmark reamarks&#34; };
});
}
关于选项的更多理解,将在后续章节进行详细讲解。
注意事项
配置Key
- 不区分大小写。例如Name和name被视为等效的。
- 配置提供程序有很多种,如果在多个提供程序中添加了某个配置项,那么,只有在最后一个提供程序中配置的才会生效。
- 分层键:
- 在环境变量中,由于冒号(:)无法适用于所有平台,所以要使用全平台均支持的双下划线(__),它会在程序中自动转换为冒号(:)
- 在其他类型的配置中,一般均使用冒号(:)分隔符即可
- ConfigurationPath类提供了一些辅助方法。
配置Value
转自:xiaoxiaotank
链接:http://cnblogs.com/xiaoxiaotank/p/15367747.html
其他推荐:
玩转Github:ASP.NET Core入门学习资源汇总
学习C#有没有什么比较系统的资源?
.net core高频面试题有哪些?
玩转Github:强烈推荐这份.NET程序员面试手册,4万字干货!
有哪些不错的windows form开源项目推荐? |
|