注:本文隸屬於《理解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 name
Authors:0=appsettings.ini book author A
Authors:1=appsettings.ini book author B
[Book:Bookmark]
Remarks=appsettings.ini bookmark remarks
環境變數配置提供程式
通過EnvironmentVariablesConfigurationProvider
在執行時從環境變數中載入配置。
Install-Package Microsoft.Extensions.Configuration.EnvironmentVariables
同樣的,只需呼叫AddEnvironmentVariables
擴充套件方法新增環境變數即可:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
// 新增字首為 My_ 的環境變數
config.AddEnvironmentVariables(prefix: "My_");
});
在新增環境變數時,通過指定引數prefix
,只讀取限定字首的環境變數。不過在讀取環境變數時,會將字首刪除。如果不指定引數prefix
,那麼會讀取所有環境變數。
當建立預設通用主機(Host)時,預設就已經新增了字首為DOTNET_
的環境變數,載入應用配置時,也新增了未限定字首的環境變數。另外,在 ASP.NET Core 中,配置 Web主機時,預設新增了字首為ASPNETCORE_
的環境變數。
需要注意的是,由於環境變數的分層鍵:
並不受所有平臺支援,而雙下劃線(__
)是全平臺支援的,所以要使用雙下劃線(__
)來代替冒號(:
)。
在 Windows 平臺下,可以通過set
或setx
命令進行環境變數配置,不過:
set
命令設定的環境變數是臨時的,僅在當前程式有效,這個程式就是當前cmd視窗啟動的。也就是說,當你開啟一個cmd視窗時,通過set
命令設定了環境變數,然後通過dotnet xxx.dll
啟動了你的應用程式,是可以讀取到環境變數的,但是在該cmd視窗之外,例如通過VS啟動應用程式,是無法讀取到該環境變數的。setx
命令設定的環境變數是持久化的。可選的新增/M
開關,表示將該環境變數配置到系統環境中(需要管理員許可權),否則,將新增到使用者環境中。
我更喜歡通過setx
去設定環境變數(記得以管理員身份執行哦):
# 注意,這裡的 My_ 是字首
setx My_Book__Name "Environment variables book name" /M
setx My_Book__Authors__0 "Environment variables book author A" /M
setx My_Book__Authors__1 "Environment variables book author B" /M
setx My_Book__Bookmark__Remarks "Environment variables bookmark remakrs" /M
配置完環境變數後,一定要記得重啟VS或cmd視窗,否則是無法讀取到最新的環境變數值的
連線字串字首的特殊處理
當沒有向AddEnvironmentVariables
傳入字首時,預設也會針對含有以下字首的環境變數進行特殊處理:
字首 | 環境變數Key | 配置Key | 配置提供程式 |
---|---|---|---|
MYSQLCONNSTR_ | MYSQLCONNSTR_{KEY} | ConnectionStrings:{KEY} | MySQL |
SQLCONNSTR_ | SQLCONNSTR_{KEY} | ConnectionStrings:{KEY} | SQL Server |
SQLAZURECONNSTR_ | SQLAZURECONNSTR_{KEY} | ConnectionStrings:{KEY} | Azure SQL |
CUSTOMCONNSTR_ | CUSTOMCONNSTR_{KEY} | ConnectionStrings:{KEY} | 自定義配置提供程式 |
在 launchSettings.json 中配置環境變數
在 ASP.NET Core 模板專案中,會生成一個 launchSettings.json 檔案,我們也可以在該檔案中配置環境變數。
需要注意的是,launchSettings.json 中的配置只用於開發環境,並且在該檔案中設定的環境變數會覆蓋在系統環境中設定的變數。
{
"WebApplication": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5000", // 設定環境變數 ASPNETCORE_URLS
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"My_Book__Name": "launchSettings.json Environment variables book name",
"My_Book__Authors__0": "launchSettings.json Environment variables book author A",
"My_Book__Authors__1": "launchSettings.json Environment variables book author B",
"My_Book__Bookmark__Remarks": "launchSettings.json Environment variables bookmark remarks"
}
}
}
雖然說在 launchSettings.json 中配置環境變數時可以使用冒號(:)作為分層鍵,但是我在測試過程中,發現當同時配置了系統環境變數時,程式讀取到的環境變數值會發生錯亂(一部分是系統環境變數,一部分是該檔案中的環境變數)。所以建議大家還是使用雙下劃線(__)作為分層鍵。
在Linux平臺,當設定的環境變數為URL時,需要設定為轉義後的URL。可以使用systemd-escaple工具:
$ systemd-escape http://localhost:5001
http:--localhost:5001
命令列配置提供程式
通過CommandLineConfigurationProvider
在執行時從命令列引數鍵值對中載入配置。
Install-Package Microsoft.Extensions.Configuration.CommandLine
通過呼叫AddCommandLine
擴充套件方法,並傳入引數args
:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
config.AddCommandLine(args);
});
有三種設定命令列引數的方式:
使用=
:
dotnet run Book:Name="Command line book name" Book:Authors:0="Command line book author A" Book:Authors:1="Command line book author B" Book:Bookmark:Remarks="Command line bookmark remarks"
使用/
:
dotnet run /Book:Name "Command line book name" /Book:Authors:0 "Command line book author A" /Book:Authors:1 "Command line book author B" /Book:Bookmark:Remarks "Command line bookmark remarks"
使用--
:
dotnet WebApplication5.dll --Book:Name "Command line book name" --Book:Authors:0 "Command line book author A" --Book:Authors:1 "Command line book author B" --Book:Bookmark:Remarks "Command line bookmark remarks"
交換對映
該功能是針對命令列配置引數進行key對映的,如你可以將n
對映為Name
,要求:
- 交換對映key必須以
-
或--
開頭。當使用-
開頭時,命令列引數書寫時也要以-
開頭,當使用--
開頭時,命令列引數書寫時可以以--
或/
開頭。 - 交換對映字典中的key不區分大小寫,不能包含重複key。如不能同時出現
-n
和-N
,但可以同時出現-n
和--n
接下來我們來對映一下:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
var switchMappings = new Dictionary<string, string>
{
["--bn"] = "Book:Name",
["-ba0"] = "Book:Authors:0",
["--ba1"] = "Book:Authors:1",
["--bmr"] = "Book:Bookmark:Remarks"
};
config.AddCommandLine(args, switchMappings);
});
然後以命令列命令啟動:
dotnet run --bn "Command line book name" -ba0 "Command line book author A" /ba1 "Command line book author B" --bmr="Command line bookmark remarks"
記憶體配置提供程式
通過MemoryConfigurationProvider
在執行時從記憶體中的集合中載入配置。
Install-Package Microsoft.Extensions.Configuration
通過呼叫AddInMemoryCollection
新增記憶體配置:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
config.AddInMemoryCollection(new Dictionary<string, string>
{
["Book:Name"] = "Memmory book name",
["Book:Authors:0"] = "Memory book author A",
["Book:Authors:1"] = "Memory book author B",
["Book:Bookmark:Remarks"] = "Memory bookmark remarks"
});
});
主機(Host)中的預設配置優先順序
約定:越後新增的配置提供程式優先順序越高,優先順序高的配置值會覆蓋優先順序低的配置值
在 主機(Host)中,我們介紹了Host
的啟動流程,根據預設的配置提供程式的新增順序,預設的優先順序從低到高為(我順便將WebHost
預設配置的也加進來了):
- 記憶體配置提供程式 環境變數配置提供程式(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("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
// 否則,返回 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
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
var bookName = Configuration.GetValue<string>("Book:Name", defaultValue: "Unknown");
Console.WriteLine(bookName);
}
}
該擴充套件的實質(預設實現)是在底層通過呼叫IConfigurationProvider.TryGet
方法,讀取ConfigurationProvider.Data
字典中的鍵值對。所以,只能通過該擴充套件方法讀取葉子節點的配置值。
GetSection
通過IConfiguration.GetSection方法,可以獲取到指定Key的配置子節點:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// 返回的 section 永遠不會為 null
IConfigurationSection bookSection = Configuration.GetSection(BookOptions.Book);
IConfigurationSection bookmarkSection = bookSection.GetSection("Bookmark");
// or
//IConfigurationSection bookmarkSection = Configuration.GetSection("Book:Bookmark");
var remarks = bookmarkSection["Remarks"];
Console.WriteLine(remarks);
}
}
GetChildren
通過IConfiguration.GetChildren方法,可以獲取到當前配置節點的直接子節點列表
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
// children 包含了 Name、Bookmark、Authors
var children = Configuration.GetSection(BookOptions.Book).GetChildren();
foreach (var child in children)
{
Console.WriteLine($"Key: {child.Key}\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
{
// 該子節點在其父節點中所表示的 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
:
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
- 均被儲存為字串