Merge pull request #196 from GeWuYou/feat/add-game-content-configuration-documentation

refactor: 重构配置目录API并新增诊断查询方法
This commit is contained in:
gewuyou 2026-04-09 10:18:17 +08:00 committed by GitHub
commit 55a51fb5d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 326 additions and 125 deletions

View File

@ -111,6 +111,57 @@ public class GeneratedConfigConsumerIntegrationTests
}); });
} }
/// <summary>
/// 验证项目级生成目录既能按配置域枚举元数据,也能直接复用聚合注册筛选规则产出启动诊断视图。
/// </summary>
[Test]
public void GeneratedConfigCatalog_Should_Expose_Domain_And_Registration_Diagnostic_Views()
{
Assert.That(GeneratedConfigCatalog.TryGetByTableName("monster", out var monsterMetadata), Is.True);
Assert.That(GeneratedConfigCatalog.TryGetByTableName("item", out var itemMetadata), Is.True);
var monsterDomainTables = GeneratedConfigCatalog.GetTablesInConfigDomain(MonsterConfigBindings.ConfigDomain);
var missingDomainTables = GeneratedConfigCatalog.GetTablesInConfigDomain("missing");
var itemOnlyRegistrationTables = GeneratedConfigCatalog.GetTablesForRegistration(
new GeneratedConfigRegistrationOptions
{
IncludedTableNames = new[] { ItemConfigBindings.TableName }
});
var predicateOnlyRegistrationTables = GeneratedConfigCatalog.GetTablesForRegistration(
new GeneratedConfigRegistrationOptions
{
TableFilter = static metadata =>
string.Equals(metadata.TableName, MonsterConfigBindings.TableName, StringComparison.Ordinal)
});
var monsterOnlyOptions = new GeneratedConfigRegistrationOptions
{
IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain }
};
var predicateOnlyOptions = new GeneratedConfigRegistrationOptions
{
TableFilter = static metadata =>
string.Equals(metadata.TableName, MonsterConfigBindings.TableName, StringComparison.Ordinal)
};
Assert.Multiple(() =>
{
Assert.That(monsterDomainTables.Select(static metadata => metadata.TableName),
Is.EqualTo(new[] { MonsterConfigBindings.TableName }));
Assert.That(missingDomainTables, Is.Empty);
Assert.That(itemOnlyRegistrationTables.Select(static metadata => metadata.TableName),
Is.EqualTo(new[] { ItemConfigBindings.TableName }));
Assert.That(predicateOnlyRegistrationTables.Select(static metadata => metadata.TableName),
Is.EqualTo(new[] { MonsterConfigBindings.TableName }));
Assert.That(GeneratedConfigCatalog.GetTablesForRegistration().Select(static metadata => metadata.TableName),
Is.SupersetOf(new[] { ItemConfigBindings.TableName, MonsterConfigBindings.TableName }));
Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(monsterMetadata, monsterOnlyOptions), Is.True);
Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(itemMetadata, monsterOnlyOptions), Is.False);
Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(monsterMetadata, predicateOnlyOptions), Is.True);
Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(itemMetadata, predicateOnlyOptions), Is.False);
Assert.That(GeneratedConfigCatalog.MatchesRegistrationOptions(monsterMetadata, options: null), Is.True);
});
}
/// <summary> /// <summary>
/// 验证聚合注册入口可以通过生成配置域、表名集合和自定义谓词收敛多表项目的启动粒度。 /// 验证聚合注册入口可以通过生成配置域、表名集合和自定义谓词收敛多表项目的启动粒度。
/// </summary> /// </summary>

View File

@ -877,17 +877,26 @@ public class SchemaConfigGeneratorTests
"public global::System.Collections.Generic.IEqualityComparer<int>? MonsterComparer { get; init; }")); "public global::System.Collections.Generic.IEqualityComparer<int>? MonsterComparer { get; init; }"));
Assert.That(catalogSource, Does.Contain("return RegisterAllGeneratedConfigTables(loader, options: null);")); Assert.That(catalogSource, Does.Contain("return RegisterAllGeneratedConfigTables(loader, options: null);"));
Assert.That(catalogSource, Does.Contain("GeneratedConfigRegistrationOptions? options")); Assert.That(catalogSource, Does.Contain("GeneratedConfigRegistrationOptions? options"));
Assert.That(catalogSource, Does.Contain("loader.RegisterItemTable(effectiveOptions.ItemComparer);"));
Assert.That(catalogSource, Assert.That(catalogSource,
Does.Contain("if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[0], options))")); Does.Contain(
Assert.That(catalogSource, Does.Contain("loader.RegisterItemTable(options.ItemComparer);")); "if (GeneratedConfigCatalog.MatchesRegistrationOptions(GeneratedConfigCatalog.Tables[1], effectiveOptions))"));
Assert.That(catalogSource, Assert.That(catalogSource, Does.Contain("loader.RegisterMonsterTable(effectiveOptions.MonsterComparer);"));
Does.Contain("if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[1], options))"));
Assert.That(catalogSource, Does.Contain("loader.RegisterMonsterTable(options.MonsterComparer);"));
Assert.That(catalogSource, Does.Contain("ItemConfigBindings.Metadata.TableName")); Assert.That(catalogSource, Does.Contain("ItemConfigBindings.Metadata.TableName"));
Assert.That(catalogSource, Does.Contain("MonsterConfigBindings.Metadata.TableName")); Assert.That(catalogSource, Does.Contain("MonsterConfigBindings.Metadata.TableName"));
Assert.That(catalogSource, Assert.That(catalogSource,
Does.Contain("public static bool TryGetByTableName(string tableName, out TableMetadata metadata)")); Does.Contain("public static bool TryGetByTableName(string tableName, out TableMetadata metadata)"));
Assert.That(catalogSource, Does.Contain("private static bool ShouldRegisterTable(")); Assert.That(catalogSource,
Does.Contain(
"public static global::System.Collections.Generic.IReadOnlyList<TableMetadata> GetTablesInConfigDomain(string configDomain)"));
Assert.That(catalogSource,
Does.Contain(
"public static global::System.Collections.Generic.IReadOnlyList<TableMetadata> GetTablesForRegistration(GeneratedConfigRegistrationOptions? options = null)"));
Assert.That(catalogSource,
Does.Contain("public static bool MatchesRegistrationOptions("));
Assert.That(catalogSource,
Does.Contain(
"if (GeneratedConfigCatalog.MatchesRegistrationOptions(GeneratedConfigCatalog.Tables[0], effectiveOptions))"));
Assert.That(catalogSource, Does.Contain("private static bool MatchesOptionalAllowList(")); Assert.That(catalogSource, Does.Contain("private static bool MatchesOptionalAllowList("));
}); });
} }

View File

@ -88,6 +88,106 @@ public static class GeneratedConfigCatalog
metadata = default; metadata = default;
return false; return false;
} }
/// <summary>
/// Resolves the generated table metadata entries that belong to the specified logical config domain.
/// </summary>
/// <param name="configDomain">Logical config domain derived from the schema base name.</param>
/// <returns>A deterministic metadata snapshot for the requested config domain, or an empty list when no generated table belongs to that domain.</returns>
/// <exception cref="global::System.ArgumentNullException">When <paramref name="configDomain"/> is null.</exception>
public static global::System.Collections.Generic.IReadOnlyList<TableMetadata> GetTablesInConfigDomain(string configDomain)
{
if (configDomain is null)
{
throw new global::System.ArgumentNullException(nameof(configDomain));
}
var matchedTables = new global::System.Collections.Generic.List<TableMetadata>();
foreach (var metadata in Tables)
{
if (string.Equals(metadata.ConfigDomain, configDomain, global::System.StringComparison.Ordinal))
{
matchedTables.Add(metadata);
}
}
return matchedTables.Count == 0 ? global::System.Array.Empty<TableMetadata>() : matchedTables.ToArray();
}
/// <summary>
/// Resolves the generated table metadata entries that aggregate registration would currently include under the supplied filters.
/// </summary>
/// <param name="options">Optional aggregate registration filters and comparer overrides. When null, every generated table remains eligible.</param>
/// <returns>A deterministic metadata snapshot in the same order used by <see cref="GeneratedConfigRegistrationExtensions.RegisterAllGeneratedConfigTables(global::GFramework.Game.Config.YamlConfigLoader, GeneratedConfigRegistrationOptions?)" />.</returns>
public static global::System.Collections.Generic.IReadOnlyList<TableMetadata> GetTablesForRegistration(GeneratedConfigRegistrationOptions? options = null)
{
var matchedTables = new global::System.Collections.Generic.List<TableMetadata>();
foreach (var metadata in Tables)
{
if (MatchesRegistrationOptions(metadata, options))
{
matchedTables.Add(metadata);
}
}
return matchedTables.Count == 0 ? global::System.Array.Empty<TableMetadata>() : matchedTables.ToArray();
}
/// <summary>
/// Evaluates whether one generated table metadata entry remains eligible under the supplied aggregate registration filters.
/// </summary>
/// <param name="metadata">Generated table metadata under evaluation.</param>
/// <param name="options">Optional aggregate registration filters and comparer overrides. When null, the metadata entry is always eligible.</param>
/// <returns><see langword="true" /> when the generated table would be included by aggregate registration; otherwise <see langword="false" />.</returns>
public static bool MatchesRegistrationOptions(
TableMetadata metadata,
GeneratedConfigRegistrationOptions? options)
{
if (options is null)
{
return true;
}
// Apply cheap generated allow-lists before invoking the optional caller predicate so startup diagnostics stay aligned with real registration.
if (!MatchesOptionalAllowList(options.IncludedConfigDomains, metadata.ConfigDomain))
{
return false;
}
if (!MatchesOptionalAllowList(options.IncludedTableNames, metadata.TableName))
{
return false;
}
return options.TableFilter?.Invoke(metadata) ?? true;
}
/// <summary>
/// Treats a null or empty allow-list as an unrestricted match, and otherwise performs ordinal string comparison against the generated metadata value.
/// </summary>
/// <param name="allowedValues">Optional caller-supplied allow-list.</param>
/// <param name="candidate">Generated metadata value being evaluated.</param>
/// <returns><see langword="true" /> when the value should remain eligible for registration; otherwise <see langword="false" />.</returns>
private static bool MatchesOptionalAllowList(
global::System.Collections.Generic.IReadOnlyCollection<string>? allowedValues,
string candidate)
{
if (allowedValues is null || allowedValues.Count == 0)
{
return true;
}
foreach (var allowedValue in allowedValues)
{
if (allowedValue is not null &&
string.Equals(allowedValue, candidate, global::System.StringComparison.Ordinal))
{
return true;
}
}
return false;
}
} }
/// <summary> /// <summary>
@ -154,64 +254,13 @@ public static class GeneratedConfigRegistrationExtensions
throw new global::System.ArgumentNullException(nameof(loader)); throw new global::System.ArgumentNullException(nameof(loader));
} }
options ??= new GeneratedConfigRegistrationOptions(); var effectiveOptions = options ?? new GeneratedConfigRegistrationOptions();
if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[0], options)) if (GeneratedConfigCatalog.MatchesRegistrationOptions(GeneratedConfigCatalog.Tables[0], effectiveOptions))
{ {
loader.RegisterMonsterTable(options.MonsterComparer); loader.RegisterMonsterTable(effectiveOptions.MonsterComparer);
} }
return loader; return loader;
} }
/// <summary>
/// Applies the generated registration filters in a deterministic order so bootstrap code can narrow aggregate registration without hand-writing per-table calls.
/// </summary>
/// <param name="metadata">Generated table metadata under consideration.</param>
/// <param name="options">Aggregate registration options supplied by the caller.</param>
/// <returns><see langword="true" /> when the generated table should be registered; otherwise <see langword="false" />.</returns>
private static bool ShouldRegisterTable(
GeneratedConfigCatalog.TableMetadata metadata,
GeneratedConfigRegistrationOptions options)
{
// Apply cheap generated allow-lists before invoking the optional caller predicate so startup filtering stays predictable.
if (!MatchesOptionalAllowList(options.IncludedConfigDomains, metadata.ConfigDomain))
{
return false;
}
if (!MatchesOptionalAllowList(options.IncludedTableNames, metadata.TableName))
{
return false;
}
return options.TableFilter?.Invoke(metadata) ?? true;
}
/// <summary>
/// Treats a null or empty allow-list as an unrestricted match, and otherwise performs ordinal string comparison against the generated metadata value.
/// </summary>
/// <param name="allowedValues">Optional caller-supplied allow-list.</param>
/// <param name="candidate">Generated metadata value being evaluated.</param>
/// <returns><see langword="true" /> when the value should remain eligible for registration; otherwise <see langword="false" />.</returns>
private static bool MatchesOptionalAllowList(
global::System.Collections.Generic.IReadOnlyCollection<string>? allowedValues,
string candidate)
{
if (allowedValues is null || allowedValues.Count == 0)
{
return true;
}
foreach (var allowedValue in allowedValues)
{
if (allowedValue is not null &&
string.Equals(allowedValue, candidate, global::System.StringComparison.Ordinal))
{
return true;
}
}
return false;
}
} }

View File

@ -1240,6 +1240,125 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(" metadata = default;"); builder.AppendLine(" metadata = default;");
builder.AppendLine(" return false;"); builder.AppendLine(" return false;");
builder.AppendLine(" }"); builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(
" /// Resolves the generated table metadata entries that belong to the specified logical config domain.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" /// <param name=\"configDomain\">Logical config domain derived from the schema base name.</param>");
builder.AppendLine(
" /// <returns>A deterministic metadata snapshot for the requested config domain, or an empty list when no generated table belongs to that domain.</returns>");
builder.AppendLine(
" /// <exception cref=\"global::System.ArgumentNullException\">When <paramref name=\"configDomain\"/> is null.</exception>");
builder.AppendLine(
" public static global::System.Collections.Generic.IReadOnlyList<TableMetadata> GetTablesInConfigDomain(string configDomain)");
builder.AppendLine(" {");
builder.AppendLine(" if (configDomain is null)");
builder.AppendLine(" {");
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(configDomain));");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" var matchedTables = new global::System.Collections.Generic.List<TableMetadata>();");
builder.AppendLine(" foreach (var metadata in Tables)");
builder.AppendLine(" {");
builder.AppendLine(
" if (string.Equals(metadata.ConfigDomain, configDomain, global::System.StringComparison.Ordinal))");
builder.AppendLine(" {");
builder.AppendLine(" matchedTables.Add(metadata);");
builder.AppendLine(" }");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(
" return matchedTables.Count == 0 ? global::System.Array.Empty<TableMetadata>() : matchedTables.ToArray();");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(
" /// Resolves the generated table metadata entries that aggregate registration would currently include under the supplied filters.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(
" /// <param name=\"options\">Optional aggregate registration filters and comparer overrides. When null, every generated table remains eligible.</param>");
builder.AppendLine(
" /// <returns>A deterministic metadata snapshot in the same order used by <see cref=\"GeneratedConfigRegistrationExtensions.RegisterAllGeneratedConfigTables(global::GFramework.Game.Config.YamlConfigLoader, GeneratedConfigRegistrationOptions?)\" />.</returns>");
builder.AppendLine(
" public static global::System.Collections.Generic.IReadOnlyList<TableMetadata> GetTablesForRegistration(GeneratedConfigRegistrationOptions? options = null)");
builder.AppendLine(" {");
builder.AppendLine(" var matchedTables = new global::System.Collections.Generic.List<TableMetadata>();");
builder.AppendLine(" foreach (var metadata in Tables)");
builder.AppendLine(" {");
builder.AppendLine(" if (MatchesRegistrationOptions(metadata, options))");
builder.AppendLine(" {");
builder.AppendLine(" matchedTables.Add(metadata);");
builder.AppendLine(" }");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(
" return matchedTables.Count == 0 ? global::System.Array.Empty<TableMetadata>() : matchedTables.ToArray();");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(
" /// Evaluates whether one generated table metadata entry remains eligible under the supplied aggregate registration filters.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" /// <param name=\"metadata\">Generated table metadata under evaluation.</param>");
builder.AppendLine(
" /// <param name=\"options\">Optional aggregate registration filters and comparer overrides. When null, the metadata entry is always eligible.</param>");
builder.AppendLine(
" /// <returns><see langword=\"true\" /> when the generated table would be included by aggregate registration; otherwise <see langword=\"false\" />.</returns>");
builder.AppendLine(" public static bool MatchesRegistrationOptions(");
builder.AppendLine(" TableMetadata metadata,");
builder.AppendLine(" GeneratedConfigRegistrationOptions? options)");
builder.AppendLine(" {");
builder.AppendLine(" if (options is null)");
builder.AppendLine(" {");
builder.AppendLine(" return true;");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(
" // Apply cheap generated allow-lists before invoking the optional caller predicate so startup diagnostics stay aligned with real registration.");
builder.AppendLine(
" if (!MatchesOptionalAllowList(options.IncludedConfigDomains, metadata.ConfigDomain))");
builder.AppendLine(" {");
builder.AppendLine(" return false;");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" if (!MatchesOptionalAllowList(options.IncludedTableNames, metadata.TableName))");
builder.AppendLine(" {");
builder.AppendLine(" return false;");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" return options.TableFilter?.Invoke(metadata) ?? true;");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(
" /// Treats a null or empty allow-list as an unrestricted match, and otherwise performs ordinal string comparison against the generated metadata value.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" /// <param name=\"allowedValues\">Optional caller-supplied allow-list.</param>");
builder.AppendLine(" /// <param name=\"candidate\">Generated metadata value being evaluated.</param>");
builder.AppendLine(
" /// <returns><see langword=\"true\" /> when the value should remain eligible for registration; otherwise <see langword=\"false\" />.</returns>");
builder.AppendLine(" private static bool MatchesOptionalAllowList(");
builder.AppendLine(" global::System.Collections.Generic.IReadOnlyCollection<string>? allowedValues,");
builder.AppendLine(" string candidate)");
builder.AppendLine(" {");
builder.AppendLine(" if (allowedValues is null || allowedValues.Count == 0)");
builder.AppendLine(" {");
builder.AppendLine(" return true;");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" foreach (var allowedValue in allowedValues)");
builder.AppendLine(" {");
builder.AppendLine(" if (allowedValue is not null &&");
builder.AppendLine(
" string.Equals(allowedValue, candidate, global::System.StringComparison.Ordinal))");
builder.AppendLine(" {");
builder.AppendLine(" return true;");
builder.AppendLine(" }");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" return false;");
builder.AppendLine(" }");
builder.AppendLine("}"); builder.AppendLine("}");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine("/// <summary>"); builder.AppendLine("/// <summary>");
@ -1340,83 +1459,23 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(loader));"); builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(loader));");
builder.AppendLine(" }"); builder.AppendLine(" }");
builder.AppendLine(); builder.AppendLine();
builder.AppendLine(" options ??= new GeneratedConfigRegistrationOptions();"); builder.AppendLine(" var effectiveOptions = options ?? new GeneratedConfigRegistrationOptions();");
builder.AppendLine(); builder.AppendLine();
for (var index = 0; index < schemas.Count; index++) for (var index = 0; index < schemas.Count; index++)
{ {
var schema = schemas[index]; var schema = schemas[index];
builder.AppendLine( builder.AppendLine(
$" if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[{index.ToString(CultureInfo.InvariantCulture)}], options))"); $" if (GeneratedConfigCatalog.MatchesRegistrationOptions(GeneratedConfigCatalog.Tables[{index.ToString(CultureInfo.InvariantCulture)}], effectiveOptions))");
builder.AppendLine(" {"); builder.AppendLine(" {");
builder.AppendLine( builder.AppendLine(
$" loader.Register{schema.EntityName}Table(options.{schema.EntityName}Comparer);"); $" loader.Register{schema.EntityName}Table(effectiveOptions.{schema.EntityName}Comparer);");
builder.AppendLine(" }"); builder.AppendLine(" }");
builder.AppendLine(); builder.AppendLine();
} }
builder.AppendLine(" return loader;"); builder.AppendLine(" return loader;");
builder.AppendLine(" }"); builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(
" /// Applies the generated registration filters in a deterministic order so bootstrap code can narrow aggregate registration without hand-writing per-table calls.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" /// <param name=\"metadata\">Generated table metadata under consideration.</param>");
builder.AppendLine(
" /// <param name=\"options\">Aggregate registration options supplied by the caller.</param>");
builder.AppendLine(
" /// <returns><see langword=\"true\" /> when the generated table should be registered; otherwise <see langword=\"false\" />.</returns>");
builder.AppendLine(
" private static bool ShouldRegisterTable(");
builder.AppendLine(" GeneratedConfigCatalog.TableMetadata metadata,");
builder.AppendLine(" GeneratedConfigRegistrationOptions options)");
builder.AppendLine(" {");
builder.AppendLine(
" // Apply cheap generated allow-lists before invoking the optional caller predicate so startup filtering stays predictable.");
builder.AppendLine(
" if (!MatchesOptionalAllowList(options.IncludedConfigDomains, metadata.ConfigDomain))");
builder.AppendLine(" {");
builder.AppendLine(" return false;");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" if (!MatchesOptionalAllowList(options.IncludedTableNames, metadata.TableName))");
builder.AppendLine(" {");
builder.AppendLine(" return false;");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" return options.TableFilter?.Invoke(metadata) ?? true;");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" /// <summary>");
builder.AppendLine(
" /// Treats a null or empty allow-list as an unrestricted match, and otherwise performs ordinal string comparison against the generated metadata value.");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" /// <param name=\"allowedValues\">Optional caller-supplied allow-list.</param>");
builder.AppendLine(" /// <param name=\"candidate\">Generated metadata value being evaluated.</param>");
builder.AppendLine(
" /// <returns><see langword=\"true\" /> when the value should remain eligible for registration; otherwise <see langword=\"false\" />.</returns>");
builder.AppendLine(" private static bool MatchesOptionalAllowList(");
builder.AppendLine(" global::System.Collections.Generic.IReadOnlyCollection<string>? allowedValues,");
builder.AppendLine(" string candidate)");
builder.AppendLine(" {");
builder.AppendLine(" if (allowedValues is null || allowedValues.Count == 0)");
builder.AppendLine(" {");
builder.AppendLine(" return true;");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" foreach (var allowedValue in allowedValues)");
builder.AppendLine(" {");
builder.AppendLine(" if (allowedValue is not null &&");
builder.AppendLine(
" string.Equals(allowedValue, candidate, global::System.StringComparison.Ordinal))");
builder.AppendLine(" {");
builder.AppendLine(" return true;");
builder.AppendLine(" }");
builder.AppendLine(" }");
builder.AppendLine();
builder.AppendLine(" return false;");
builder.AppendLine(" }");
builder.AppendLine("}"); builder.AppendLine("}");
return builder.ToString().TrimEnd(); return builder.ToString().TrimEnd();
} }

View File

@ -404,7 +404,7 @@ if (monsterTable.TryFindFirstByFaction("dungeon", out var firstDungeonMonster))
### Architecture 推荐接入模板 ### Architecture 推荐接入模板
如果你的项目已经基于 `GFramework.Core.Architectures.Architecture` 组织初始化流程,推荐把配置系统接到 `OnInitialize()` 阶段,并把 `GameConfigBootstrap.Registry` 注册为 utility 如果你的项目已经基于 `GFramework.Core.Architectures.Architecture` 组织初始化流程,并且当前宿主没有提供更上层的异步启动入口,可以把配置系统接到 `OnInitialize()` 阶段,并把 `GameConfigBootstrap.Registry` 注册为 utility
```csharp ```csharp
using GFramework.Core.Architectures; using GFramework.Core.Architectures;
@ -440,6 +440,12 @@ public sealed class GameArchitecture : Architecture
} }
``` ```
这个模板里的 `.GetAwaiter().GetResult()` 只是“同步 `OnInitialize()` 到异步配置加载”的桥接写法,不应被理解为无条件推荐:
- 如果宿主已经提供异步组合根、启动器或更早的异步初始化阶段,优先在那里直接 `await _configBootstrap.InitializeAsync()`
- 只有在 `Architecture` 只暴露同步 `OnInitialize()`,且当前线程不存在需要恢复的 `SynchronizationContext` 时,才适合使用这类同步桥接
- 在 UI 线程、ASP.NET Classic 等存在活动 `SynchronizationContext` 的环境中,不要直接阻塞等待异步初始化;应把配置初始化前移到异步入口,或改为由不受该上下文约束的启动线程完成
初始化完成后,业务组件可以继续通过架构上下文读取 utility再走生成的强类型入口 初始化完成后,业务组件可以继续通过架构上下文读取 utility再走生成的强类型入口
```csharp ```csharp
@ -547,6 +553,15 @@ if (GeneratedConfigCatalog.TryGetByTableName("monster", out var metadata))
} }
``` ```
如果你希望先按配置域聚合出一组候选表,再决定是否进入启动链路,也可以直接查询目录:
```csharp
foreach (var metadata in GeneratedConfigCatalog.GetTablesInConfigDomain(MonsterConfigBindings.ConfigDomain))
{
Console.WriteLine(metadata.TableName);
}
```
如果你需要为某些表保留自定义 key comparer也可以继续走聚合注册入口而不是被迫退回逐表手写 如果你需要为某些表保留自定义 key comparer也可以继续走聚合注册入口而不是被迫退回逐表手写
```csharp ```csharp
@ -581,10 +596,28 @@ var loader = new YamlConfigLoader("config-root")
}); });
``` ```
如果你想在真正调用 `RegisterAllGeneratedConfigTables(...)` 之前,先把“这次会注册哪些表”输出到日志中,推荐直接复用同一份 options 做启动诊断,而不是手写一套平行筛选逻辑:
```csharp
var registrationOptions = new GeneratedConfigRegistrationOptions
{
IncludedConfigDomains = new[] { MonsterConfigBindings.ConfigDomain }
};
foreach (var metadata in GeneratedConfigCatalog.GetTablesForRegistration(registrationOptions))
{
Console.WriteLine($"Registering {metadata.TableName}");
}
var loader = new YamlConfigLoader("config-root")
.RegisterAllGeneratedConfigTables(registrationOptions);
```
这里的规则是: 这里的规则是:
- `IncludedConfigDomains``IncludedTableNames` 都按 `StringComparison.Ordinal` 做白名单匹配;传 `null` 或空集合表示“不限制” - `IncludedConfigDomains``IncludedTableNames` 都按 `StringComparison.Ordinal` 做白名单匹配;传 `null` 或空集合表示“不限制”
- `TableFilter` 会在上述白名单通过后执行,适合继续按 schema 路径、配置目录等元数据做更细的启动裁剪 - `TableFilter` 会在上述白名单通过后执行,适合继续按 schema 路径、配置目录等元数据做更细粒度的启动裁剪
- `GeneratedConfigCatalog.GetTablesForRegistration(...)``RegisterAllGeneratedConfigTables(...)` 复用同一套筛选规则,便于在启动日志和真实注册之间保持一致
- 未显式配置 comparer 的表,仍然使用各自 `Register{Entity}Table()` 的默认行为 - 未显式配置 comparer 的表,仍然使用各自 `Register{Entity}Table()` 的默认行为
- 需要自定义 comparer 的表,可以通过 `GeneratedConfigRegistrationOptions` 按表覆盖 - 需要自定义 comparer 的表,可以通过 `GeneratedConfigRegistrationOptions` 按表覆盖
- 当前 `ConfigDomain` 约定仍与生成表名保持一致,但建议优先引用 `*ConfigBindings.ConfigDomain`,为后续更细的分组策略保留稳定入口 - 当前 `ConfigDomain` 约定仍与生成表名保持一致,但建议优先引用 `*ConfigBindings.ConfigDomain`,为后续更细的分组策略保留稳定入口