feat(config): 添加基于文件目录的YAML配置加载器

- 实现YamlConfigLoader类,支持从文件目录加载YAML配置
- 提供RegisterTable方法注册配置表定义,支持schema校验
- 添加LoadAsync异步加载功能,支持批量加载配置表
- 实现EnableHotReload方法,支持开发期配置热重载
- 添加跨表引用校验功能,确保配置依赖关系正确性
- 支持YAML文件和YML文件格式,自动识别文件扩展名
- 提供配置表主键提取器和比较器自定义功能
- 实现文件变更监听和防抖机制,避免频繁重载
- 支持配置目录和schema文件路径的灵活配置
- 提供详细的加载异常信息和诊断支持
This commit is contained in:
GeWuYou 2026-04-06 07:53:46 +08:00
parent a416e093ee
commit 7da35c00b2
3 changed files with 52 additions and 14 deletions

View File

@ -1133,6 +1133,26 @@ public class YamlConfigLoaderTests
}
}
/// <summary>
/// 验证热重载会在启动前拒绝负的防抖延迟,避免后台延迟任务才暴露参数错误。
/// </summary>
[Test]
public void EnableHotReload_Should_Throw_When_Debounce_Delay_Is_Negative()
{
var loader = new YamlConfigLoader(_rootPath);
var registry = new ConfigRegistry();
var exception = Assert.Throws<ArgumentOutOfRangeException>(() =>
loader.EnableHotReload(
registry,
new YamlConfigHotReloadOptions
{
DebounceDelay = TimeSpan.FromMilliseconds(-1)
}));
Assert.That(exception!.ParamName, Is.EqualTo("options"));
}
/// <summary>
/// 验证热重载失败时会保留旧表状态,并通过失败回调暴露诊断信息。
/// </summary>

View File

@ -97,6 +97,9 @@ public sealed class YamlConfigLoader : IConfigLoader
/// <param name="debounceDelay">防抖延迟;为空时默认使用 200 毫秒。</param>
/// <returns>用于停止热重载监听的注销句柄。</returns>
/// <exception cref="ArgumentNullException">当 <paramref name="registry" /> 为空时抛出。</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// 当显式提供的 <paramref name="debounceDelay" /> 小于 <see cref="TimeSpan.Zero" /> 时抛出。
/// </exception>
public IUnRegister EnableHotReload(
IConfigRegistry registry,
Action<string>? onTableReloaded = null,
@ -122,12 +125,22 @@ public sealed class YamlConfigLoader : IConfigLoader
/// <param name="options">热重载配置选项;为空时使用默认选项。</param>
/// <returns>用于停止热重载监听的注销句柄。</returns>
/// <exception cref="ArgumentNullException">当 <paramref name="registry" /> 为空时抛出。</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// 当 <paramref name="options" /> 的 <see cref="YamlConfigHotReloadOptions.DebounceDelay" /> 小于
/// <see cref="TimeSpan.Zero" /> 时抛出。
/// </exception>
public IUnRegister EnableHotReload(
IConfigRegistry registry,
YamlConfigHotReloadOptions? options)
{
ArgumentNullException.ThrowIfNull(registry);
options ??= new YamlConfigHotReloadOptions();
if (options.DebounceDelay < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(
nameof(options),
"DebounceDelay must be greater than or equal to zero.");
}
return new HotReloadSession(
_rootPath,

View File

@ -95,8 +95,13 @@ public class SchemaConfigGeneratorTests
/// <summary>
/// 验证 schema 字段名无法映射为合法 C# 标识符时会直接给出诊断,而不是生成不可编译代码。
/// </summary>
[Test]
public void Run_Should_Report_Diagnostic_When_Schema_Key_Maps_To_Invalid_CSharp_Identifier()
/// <param name="schemaKey">会映射为非法 C# 标识符的 schema key。</param>
/// <param name="generatedIdentifier">当前命名规范化逻辑生成出的非法标识符。</param>
[TestCase("drop$item", "Drop$item")]
[TestCase("1-phase", "1Phase")]
public void Run_Should_Report_Diagnostic_When_Schema_Key_Maps_To_Invalid_CSharp_Identifier(
string schemaKey,
string generatedIdentifier)
{
const string source = """
namespace TestApp
@ -107,16 +112,16 @@ public class SchemaConfigGeneratorTests
}
""";
const string schema = """
{
"type": "object",
"required": ["id", "drop$item"],
"properties": {
"id": { "type": "integer" },
"drop$item": { "type": "string" }
}
}
""";
var schema = $$"""
{
"type": "object",
"required": ["id", "{{schemaKey}}"],
"properties": {
"id": { "type": "integer" },
"{{schemaKey}}": { "type": "string" }
}
}
""";
var result = SchemaGeneratorTestDriver.Run(
source,
@ -128,8 +133,8 @@ public class SchemaConfigGeneratorTests
{
Assert.That(diagnostic.Id, Is.EqualTo("GF_ConfigSchema_006"));
Assert.That(diagnostic.Severity, Is.EqualTo(DiagnosticSeverity.Error));
Assert.That(diagnostic.GetMessage(), Does.Contain("drop$item"));
Assert.That(diagnostic.GetMessage(), Does.Contain("Drop$item"));
Assert.That(diagnostic.GetMessage(), Does.Contain(schemaKey));
Assert.That(diagnostic.GetMessage(), Does.Contain(generatedIdentifier));
});
}