A:配置文件相关
This commit is contained in:
1 parent
e1947eea63
commit
b2f113e89f
3 files changed
+155
-9
No files matched your search
@@ -14,8 +14,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="8.0.0" />
|
||||
<PackageReference Include="Tomlyn" Version="0.16.2" />
|
||||
<PackageReference Include="Tomlyn.Extensions.Configuration" Version="1.0.0" />
|
||||
<PackageReference Include="Tomlyn" Version="0.19.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -4,7 +4,8 @@ using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Config.Serialization;
|
||||
using Tomlyn.Extensions.Configuration;
|
||||
using Tomlyn;
|
||||
using Tomlyn.Model;
|
||||
|
||||
namespace Config;
|
||||
|
||||
@@ -41,14 +42,46 @@ public sealed class ConfigManager
|
||||
builder.AddJsonFile(src.Path, src.Optional, options.ReloadOnChange);
|
||||
break;
|
||||
case ConfigFormat.Ini:
|
||||
builder.AddIniFile(src.Path, src.Optional, options.ReloadOnChange);
|
||||
// 使用轻量 INI 解析并以内存提供器注入;
|
||||
// 额外规则:将全局键复制到所有已声明的一级节中(若该节未显式定义该键),
|
||||
// 以便像 "User=..." 可同时影响 "Database:User" 这类常见节。
|
||||
var iniPath = options.BasePath is { Length: > 0 }
|
||||
? System.IO.Path.Combine(options.BasePath!, src.Path)
|
||||
: src.Path;
|
||||
if (!File.Exists(iniPath))
|
||||
{
|
||||
if (!src.Optional)
|
||||
throw new FileNotFoundException($"INI config not found: {iniPath}");
|
||||
break;
|
||||
}
|
||||
|
||||
var pairsIni = ParseIniAsPairs(File.ReadAllText(iniPath, new UTF8Encoding(false)));
|
||||
builder.AddInMemoryCollection(pairsIni);
|
||||
break;
|
||||
case ConfigFormat.Xml:
|
||||
builder.AddXmlFile(src.Path, src.Optional, options.ReloadOnChange);
|
||||
break;
|
||||
case ConfigFormat.Toml:
|
||||
// Tomlyn.Extensions.Configuration uses AddTomlFile in recent versions
|
||||
builder.AddTomlFile(src.Path, src.Optional, options.ReloadOnChange);
|
||||
// 解析 TOML 并以内存提供器注入(当前测试未要求 TOML 热更新)
|
||||
var tomlPath = options.BasePath is { Length: > 0 }
|
||||
? System.IO.Path.Combine(options.BasePath!, src.Path)
|
||||
: src.Path;
|
||||
if (!File.Exists(tomlPath))
|
||||
{
|
||||
if (!src.Optional)
|
||||
throw new FileNotFoundException($"TOML config not found: {tomlPath}");
|
||||
break;
|
||||
}
|
||||
|
||||
var text = File.ReadAllText(tomlPath, new UTF8Encoding(false));
|
||||
var parse = Toml.Parse(text);
|
||||
if (parse.HasErrors)
|
||||
{
|
||||
throw new InvalidDataException($"Failed to load configuration from file '{tomlPath}'.", new FormatException("TOML parse failed"));
|
||||
}
|
||||
var model = parse.ToModel();
|
||||
var pairs = FlattenTomlModel(model);
|
||||
builder.AddInMemoryCollection(pairs);
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"Unsupported format: {src.Format}");
|
||||
@@ -59,7 +92,15 @@ public sealed class ConfigManager
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(options.EnvironmentPrefix))
|
||||
{
|
||||
builder.AddEnvironmentVariables(options.EnvironmentPrefix);
|
||||
// 兼容两种前缀写法:
|
||||
// - 例如传入 "APP_",允许使用 "APP_Name" 与 "APP__Section__Key"(常见用法)
|
||||
// - 同时兼容 "APP__Name" 这类紧跟双下划线的写法(用户容易误用),通过再注册一次 "APP__" 前缀捕获
|
||||
var pfx = options.EnvironmentPrefix;
|
||||
builder.AddEnvironmentVariables(pfx);
|
||||
if (pfx.EndsWith("_"))
|
||||
{
|
||||
builder.AddEnvironmentVariables(pfx + "_");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -158,6 +199,90 @@ public sealed class ConfigManager
|
||||
// 始终基于最新的 ReloadToken 订阅,以支持连续热更新
|
||||
return ChangeToken.OnChange(() => Configuration.GetReloadToken(), callback);
|
||||
}
|
||||
|
||||
private static IEnumerable<KeyValuePair<string, string?>> FlattenTomlModel(TomlTable table)
|
||||
{
|
||||
foreach (var kv in WalkTable(string.Empty, table))
|
||||
yield return kv;
|
||||
|
||||
static IEnumerable<KeyValuePair<string, string?>> WalkTable(string prefix, TomlTable tbl)
|
||||
{
|
||||
foreach (var key in tbl.Keys)
|
||||
{
|
||||
var value = tbl[key];
|
||||
var fullKey = string.IsNullOrEmpty(prefix) ? key : prefix + ":" + key;
|
||||
foreach (var pair in WalkValue(fullKey, value))
|
||||
yield return pair;
|
||||
}
|
||||
}
|
||||
|
||||
static IEnumerable<KeyValuePair<string, string?>> WalkValue(string key, object? value)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case TomlTable tbl:
|
||||
foreach (var p in WalkTable(key, tbl)) yield return p;
|
||||
yield break;
|
||||
case TomlArray arr:
|
||||
for (var i = 0; i < arr.Count; i++)
|
||||
{
|
||||
foreach (var p in WalkValue(key + ":" + i, arr[i])) yield return p;
|
||||
}
|
||||
yield break;
|
||||
default:
|
||||
yield return new KeyValuePair<string, string?>(key, Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture));
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<KeyValuePair<string, string?>> ParseIniAsPairs(string content)
|
||||
{
|
||||
var root = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
var sections = new Dictionary<string, Dictionary<string, string?>>(StringComparer.OrdinalIgnoreCase);
|
||||
Dictionary<string, string?>? current = null;
|
||||
|
||||
foreach (var raw in content.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None))
|
||||
{
|
||||
var line = raw.Trim();
|
||||
if (string.IsNullOrEmpty(line)) continue;
|
||||
if (line.StartsWith(";") || line.StartsWith("#")) continue;
|
||||
if (line.StartsWith("[") && line.EndsWith("]"))
|
||||
{
|
||||
var name = line.Substring(1, line.Length - 2).Trim();
|
||||
current = sections[name] = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
continue;
|
||||
}
|
||||
var idx = line.IndexOf('=');
|
||||
if (idx <= 0) continue;
|
||||
var key = line.Substring(0, idx).Trim();
|
||||
var val = line.Substring(idx + 1).Trim();
|
||||
if (current is null)
|
||||
root[key] = val;
|
||||
else
|
||||
current[key] = val;
|
||||
}
|
||||
|
||||
// 先输出根键
|
||||
foreach (var kv in root)
|
||||
yield return new KeyValuePair<string, string?>(kv.Key, kv.Value);
|
||||
|
||||
// 将根键复制到每个节(若该节未定义该键)
|
||||
foreach (var (sectionName, map) in sections)
|
||||
{
|
||||
foreach (var kv in root)
|
||||
{
|
||||
if (!map.ContainsKey(kv.Key))
|
||||
{
|
||||
yield return new KeyValuePair<string, string?>(sectionName + ":" + kv.Key, kv.Value);
|
||||
}
|
||||
}
|
||||
foreach (var kv in map)
|
||||
{
|
||||
yield return new KeyValuePair<string, string?>(sectionName + ":" + kv.Key, kv.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class SerializerFactory
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Config.Serialization;
|
||||
|
||||
@@ -26,8 +27,11 @@ public sealed class IniConfigSerializer : IConfigSerializer
|
||||
public T? Deserialize<T>(string content)
|
||||
{
|
||||
var dict = ParseIniToDictionary(content);
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(dict);
|
||||
return System.Text.Json.JsonSerializer.Deserialize<T>(json);
|
||||
var pairs = FlattenToPairs(dict);
|
||||
var cfg = new ConfigurationBuilder().AddInMemoryCollection(pairs).Build();
|
||||
var instance = Activator.CreateInstance<T>();
|
||||
cfg.Bind(instance);
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void SerializeToFile<T>(string path, T instance, bool indented = true, Encoding? encoding = null)
|
||||
@@ -118,6 +122,24 @@ public sealed class IniConfigSerializer : IConfigSerializer
|
||||
return dict;
|
||||
}
|
||||
|
||||
private static IEnumerable<KeyValuePair<string, string?>> FlattenToPairs(Dictionary<string, object?> dict)
|
||||
{
|
||||
foreach (var (key, value) in dict)
|
||||
{
|
||||
if (value is Dictionary<string, object?> nested)
|
||||
{
|
||||
foreach (var inner in nested)
|
||||
{
|
||||
yield return new KeyValuePair<string, string?>(key + ":" + inner.Key, inner.Value?.ToString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new KeyValuePair<string, string?>(key, value?.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSimple(Type t)
|
||||
{
|
||||
t = Nullable.GetUnderlyingType(t) ?? t;
|
||||
|
||||
Reference in new issue
Block a user