diff --git a/GFramework.Game.Tests/Config/GeneratedConfigConsumerIntegrationTests.cs b/GFramework.Game.Tests/Config/GeneratedConfigConsumerIntegrationTests.cs index b3c726eb..b44a1cd5 100644 --- a/GFramework.Game.Tests/Config/GeneratedConfigConsumerIntegrationTests.cs +++ b/GFramework.Game.Tests/Config/GeneratedConfigConsumerIntegrationTests.cs @@ -111,6 +111,57 @@ public class GeneratedConfigConsumerIntegrationTests }); } + /// + /// 验证项目级生成目录既能按配置域枚举元数据,也能直接复用聚合注册筛选规则产出启动诊断视图。 + /// + [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); + }); + } + /// /// 验证聚合注册入口可以通过生成配置域、表名集合和自定义谓词收敛多表项目的启动粒度。 /// diff --git a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs index 24e53bd2..fc645f88 100644 --- a/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Config/SchemaConfigGeneratorTests.cs @@ -877,17 +877,26 @@ public class SchemaConfigGeneratorTests "public global::System.Collections.Generic.IEqualityComparer? MonsterComparer { get; init; }")); Assert.That(catalogSource, Does.Contain("return RegisterAllGeneratedConfigTables(loader, options: null);")); Assert.That(catalogSource, Does.Contain("GeneratedConfigRegistrationOptions? options")); + Assert.That(catalogSource, Does.Contain("loader.RegisterItemTable(effectiveOptions.ItemComparer);")); Assert.That(catalogSource, - Does.Contain("if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[0], options))")); - Assert.That(catalogSource, Does.Contain("loader.RegisterItemTable(options.ItemComparer);")); - Assert.That(catalogSource, - Does.Contain("if (ShouldRegisterTable(GeneratedConfigCatalog.Tables[1], options))")); - Assert.That(catalogSource, Does.Contain("loader.RegisterMonsterTable(options.MonsterComparer);")); + Does.Contain( + "if (GeneratedConfigCatalog.MatchesRegistrationOptions(GeneratedConfigCatalog.Tables[1], effectiveOptions))")); + Assert.That(catalogSource, Does.Contain("loader.RegisterMonsterTable(effectiveOptions.MonsterComparer);")); Assert.That(catalogSource, Does.Contain("ItemConfigBindings.Metadata.TableName")); Assert.That(catalogSource, Does.Contain("MonsterConfigBindings.Metadata.TableName")); Assert.That(catalogSource, 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 GetTablesInConfigDomain(string configDomain)")); + Assert.That(catalogSource, + Does.Contain( + "public static global::System.Collections.Generic.IReadOnlyList 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(")); }); } diff --git a/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/GeneratedConfigCatalog.g.txt b/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/GeneratedConfigCatalog.g.txt index e5b79994..97983970 100644 --- a/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/GeneratedConfigCatalog.g.txt +++ b/GFramework.SourceGenerators.Tests/Config/snapshots/SchemaConfigGenerator/GeneratedConfigCatalog.g.txt @@ -88,6 +88,106 @@ public static class GeneratedConfigCatalog metadata = default; return false; } + + /// + /// Resolves the generated table metadata entries that belong to the specified logical config domain. + /// + /// Logical config domain derived from the schema base name. + /// A deterministic metadata snapshot for the requested config domain, or an empty list when no generated table belongs to that domain. + /// When is null. + public static global::System.Collections.Generic.IReadOnlyList GetTablesInConfigDomain(string configDomain) + { + if (configDomain is null) + { + throw new global::System.ArgumentNullException(nameof(configDomain)); + } + + var matchedTables = new global::System.Collections.Generic.List(); + 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() : matchedTables.ToArray(); + } + + /// + /// Resolves the generated table metadata entries that aggregate registration would currently include under the supplied filters. + /// + /// Optional aggregate registration filters and comparer overrides. When null, every generated table remains eligible. + /// A deterministic metadata snapshot in the same order used by . + public static global::System.Collections.Generic.IReadOnlyList GetTablesForRegistration(GeneratedConfigRegistrationOptions? options = null) + { + var matchedTables = new global::System.Collections.Generic.List(); + foreach (var metadata in Tables) + { + if (MatchesRegistrationOptions(metadata, options)) + { + matchedTables.Add(metadata); + } + } + + return matchedTables.Count == 0 ? global::System.Array.Empty() : matchedTables.ToArray(); + } + + /// + /// Evaluates whether one generated table metadata entry remains eligible under the supplied aggregate registration filters. + /// + /// Generated table metadata under evaluation. + /// Optional aggregate registration filters and comparer overrides. When null, the metadata entry is always eligible. + /// when the generated table would be included by aggregate registration; otherwise . + 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; + } + + /// + /// Treats a null or empty allow-list as an unrestricted match, and otherwise performs ordinal string comparison against the generated metadata value. + /// + /// Optional caller-supplied allow-list. + /// Generated metadata value being evaluated. + /// when the value should remain eligible for registration; otherwise . + private static bool MatchesOptionalAllowList( + global::System.Collections.Generic.IReadOnlyCollection? 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; + } } /// @@ -154,64 +254,13 @@ public static class GeneratedConfigRegistrationExtensions 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; } - - /// - /// Applies the generated registration filters in a deterministic order so bootstrap code can narrow aggregate registration without hand-writing per-table calls. - /// - /// Generated table metadata under consideration. - /// Aggregate registration options supplied by the caller. - /// when the generated table should be registered; otherwise . - 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; - } - - /// - /// Treats a null or empty allow-list as an unrestricted match, and otherwise performs ordinal string comparison against the generated metadata value. - /// - /// Optional caller-supplied allow-list. - /// Generated metadata value being evaluated. - /// when the value should remain eligible for registration; otherwise . - private static bool MatchesOptionalAllowList( - global::System.Collections.Generic.IReadOnlyCollection? 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; - } } diff --git a/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs b/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs index e60c1892..ca249109 100644 --- a/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs +++ b/GFramework.SourceGenerators/Config/SchemaConfigGenerator.cs @@ -1240,6 +1240,125 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator builder.AppendLine(" metadata = default;"); builder.AppendLine(" return false;"); builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" /// "); + builder.AppendLine( + " /// Resolves the generated table metadata entries that belong to the specified logical config domain."); + builder.AppendLine(" /// "); + builder.AppendLine(" /// Logical config domain derived from the schema base name."); + builder.AppendLine( + " /// A deterministic metadata snapshot for the requested config domain, or an empty list when no generated table belongs to that domain."); + builder.AppendLine( + " /// When is null."); + builder.AppendLine( + " public static global::System.Collections.Generic.IReadOnlyList 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();"); + 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() : matchedTables.ToArray();"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" /// "); + builder.AppendLine( + " /// Resolves the generated table metadata entries that aggregate registration would currently include under the supplied filters."); + builder.AppendLine(" /// "); + builder.AppendLine( + " /// Optional aggregate registration filters and comparer overrides. When null, every generated table remains eligible."); + builder.AppendLine( + " /// A deterministic metadata snapshot in the same order used by ."); + builder.AppendLine( + " public static global::System.Collections.Generic.IReadOnlyList GetTablesForRegistration(GeneratedConfigRegistrationOptions? options = null)"); + builder.AppendLine(" {"); + builder.AppendLine(" var matchedTables = new global::System.Collections.Generic.List();"); + 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() : matchedTables.ToArray();"); + builder.AppendLine(" }"); + builder.AppendLine(); + builder.AppendLine(" /// "); + builder.AppendLine( + " /// Evaluates whether one generated table metadata entry remains eligible under the supplied aggregate registration filters."); + builder.AppendLine(" /// "); + builder.AppendLine(" /// Generated table metadata under evaluation."); + builder.AppendLine( + " /// Optional aggregate registration filters and comparer overrides. When null, the metadata entry is always eligible."); + builder.AppendLine( + " /// when the generated table would be included by aggregate registration; otherwise ."); + 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(" /// "); + 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(" /// "); + builder.AppendLine(" /// Optional caller-supplied allow-list."); + builder.AppendLine(" /// Generated metadata value being evaluated."); + builder.AppendLine( + " /// when the value should remain eligible for registration; otherwise ."); + builder.AppendLine(" private static bool MatchesOptionalAllowList("); + builder.AppendLine(" global::System.Collections.Generic.IReadOnlyCollection? 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("/// "); @@ -1340,83 +1459,23 @@ public sealed class SchemaConfigGenerator : IIncrementalGenerator builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(loader));"); builder.AppendLine(" }"); builder.AppendLine(); - builder.AppendLine(" options ??= new GeneratedConfigRegistrationOptions();"); + builder.AppendLine(" var effectiveOptions = options ?? new GeneratedConfigRegistrationOptions();"); builder.AppendLine(); for (var index = 0; index < schemas.Count; index++) { var schema = schemas[index]; 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( - $" loader.Register{schema.EntityName}Table(options.{schema.EntityName}Comparer);"); + $" loader.Register{schema.EntityName}Table(effectiveOptions.{schema.EntityName}Comparer);"); builder.AppendLine(" }"); builder.AppendLine(); } builder.AppendLine(" return loader;"); builder.AppendLine(" }"); - builder.AppendLine(); - builder.AppendLine(" /// "); - 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(" /// "); - builder.AppendLine(" /// Generated table metadata under consideration."); - builder.AppendLine( - " /// Aggregate registration options supplied by the caller."); - builder.AppendLine( - " /// when the generated table should be registered; otherwise ."); - 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(" /// "); - 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(" /// "); - builder.AppendLine(" /// Optional caller-supplied allow-list."); - builder.AppendLine(" /// Generated metadata value being evaluated."); - builder.AppendLine( - " /// when the value should remain eligible for registration; otherwise ."); - builder.AppendLine(" private static bool MatchesOptionalAllowList("); - builder.AppendLine(" global::System.Collections.Generic.IReadOnlyCollection? 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("}"); return builder.ToString().TrimEnd(); } diff --git a/docs/zh-CN/game/config-system.md b/docs/zh-CN/game/config-system.md index 09395cec..cf6b7eda 100644 --- a/docs/zh-CN/game/config-system.md +++ b/docs/zh-CN/game/config-system.md @@ -404,7 +404,7 @@ if (monsterTable.TryFindFirstByFaction("dungeon", out var firstDungeonMonster)) ### Architecture 推荐接入模板 -如果你的项目已经基于 `GFramework.Core.Architectures.Architecture` 组织初始化流程,推荐把配置系统接到 `OnInitialize()` 阶段,并把 `GameConfigBootstrap.Registry` 注册为 utility: +如果你的项目已经基于 `GFramework.Core.Architectures.Architecture` 组织初始化流程,并且当前宿主没有提供更上层的异步启动入口,可以把配置系统接到 `OnInitialize()` 阶段,并把 `GameConfigBootstrap.Registry` 注册为 utility: ```csharp 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,再走生成的强类型入口: ```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,也可以继续走聚合注册入口,而不是被迫退回逐表手写: ```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` 或空集合表示“不限制” -- `TableFilter` 会在上述白名单通过后执行,适合继续按 schema 路径、配置目录等元数据做更细的启动裁剪 +- `TableFilter` 会在上述白名单通过后执行,适合继续按 schema 路径、配置目录等元数据做更细粒度的启动裁剪 +- `GeneratedConfigCatalog.GetTablesForRegistration(...)` 与 `RegisterAllGeneratedConfigTables(...)` 复用同一套筛选规则,便于在启动日志和真实注册之间保持一致 - 未显式配置 comparer 的表,仍然使用各自 `Register{Entity}Table()` 的默认行为 - 需要自定义 comparer 的表,可以通过 `GeneratedConfigRegistrationOptions` 按表覆盖 - 当前 `ConfigDomain` 约定仍与生成表名保持一致,但建议优先引用 `*ConfigBindings.ConfigDomain`,为后续更细的分组策略保留稳定入口