Coding4Fun - .NET 依原始順序讀取 appsettings.json Key Value
0 |
Side Project 有個需求,要在 appsettings.json 定義要觀察的硬體偵測器,我第一個想到的做法是宣告一個 Sensors 直接用 "偵測器名稱": "偵測器ID" 的 Key/Value 形式列舉,像是這樣:
{
"Sensors": {
"SSD Temp": "/hdd/0/temperature/0",
"CPU Temp": "/intelcpu/0/temperature/0",
"CPU Load": "/intelcpu/0/load/0"
}
}
透過 config.GetSection("Sensors").GetChildren()
可列舉 Sensors 的 Key/Value,輕鬆取得偵測器資料:
using Microsoft.Extensions.Configuration;
Console.WriteLine(AppContext.BaseDirectory);
var config = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false)
.Build();
foreach (var item in config.GetSection("Sensors").GetChildren())
{
Console.WriteLine($"{item.Key}= {item.Value}");
}
但事與願違,雖然有讀到設定,但順序變了,CPU Load、CPU Temp、SSD Temp,感覺被排序過。
追進原始碼證實了這點。GetChildren() 背後會呼叫 GetChildrenImplementation(),在其中透過 GetChildKeys() 取得 Key 集合 (IEnumerable<string>),而 GetChildKeys() 裡有段排序邏輯 results.Sort(ConfigurationKeyComparer.Comparison);
對 Key 進行排序。
// InternalConfigurationRootExtensions.cs
internal static IEnumerable<IConfigurationSection> GetChildrenImplementation(this IConfigurationRoot root, string? path)
{
using ReferenceCountedProviders? reference = (root as ConfigurationManager)?.GetProvidersReference();
IEnumerable<IConfigurationProvider> providers = reference?.Providers ?? root.Providers;
IEnumerable<IConfigurationSection> children = providers
.Aggregate(Enumerable.Empty<string>(),
(seed, source) => source.GetChildKeys(seed, path))
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(key => root.GetSection(path == null ? key : ConfigurationPath.Combine(path, key)));
if (reference is null)
{
return children;
}
else
{
// Eagerly evaluate the IEnumerable before releasing the reference so we don't allow iteration over disposed providers.
return children.ToList();
}
}
// ConfigurationProvider.cs
public virtual IEnumerable<string> GetChildKeys(
IEnumerable<string> earlierKeys,
string? parentPath)
{
var results = new List<string>();
if (parentPath is null)
{
foreach (KeyValuePair<string, string?> kv in Data)
{
results.Add(Segment(kv.Key, 0));
}
}
else
{
Debug.Assert(ConfigurationPath.KeyDelimiter == ":");
foreach (KeyValuePair<string, string?> kv in Data)
{
if (kv.Key.Length > parentPath.Length &&
kv.Key.StartsWith(parentPath, StringComparison.OrdinalIgnoreCase) &&
kv.Key[parentPath.Length] == ':')
{
results.Add(Segment(kv.Key, parentPath.Length + 1));
}
}
}
results.AddRange(earlierKeys);
results.Sort(ConfigurationKeyComparer.Comparison);
return results;
}
研究了一下,appsettings.json 絕大部分的應用方式是傳 Key 取 Value,很少人會在意 Key 順序,因此也沒有公開 API 可讀取未排序的 Key/Value,我想搭便車的構想破滅。
若要保留設定順序,回歸自訂物件陣列是較簡單的做法。將 appsettings.json Sensors 改為陣列:
{
"Sensors": [
{ "Name":"SSD Temp", "Id":"/hdd/0/temperature/0" },
{ "Name":"CPU Temp", "Id":"/intelcpu/0/temperature/0" },
{ "Name":"CPU Load", "Id":"/intelcpu/0/load/0" }
]
}
專案參照 Microsoft.Extensions.Configuration.Binder 並修改如下:
using Microsoft.Extensions.Configuration;
Console.WriteLine(AppContext.BaseDirectory);
var config = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false)
.Build();
foreach (var item in config.GetSection("Sensors").Get<List<Sensor>>()!) {
Console.WriteLine($"{item.Name}= {item.Id}");
}
public class Sensor {
public string Id {get; set;}
public string Name {get; set;}
}
如此就能如願保留原始順序:
最後,基於好玩,既然已追進原始碼,我也試著用 Reflection 偷出原始順序 Dictionary:
存取非公開屬性的做法可能在改版升級後失效,不建議當成正式解法,這裡純屬好玩兼練手感,實際應用還是讓回歸正道。
Example of how to read key/value in appsetting.json withe their origin order.
Comments
Be the first to post a comment