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

Post a comment