using GFramework.Core.Abstractions.Events; using GFramework.Game.Abstractions.Config; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; namespace GFramework.Game.Config; /// /// 基于文件目录的 YAML 配置加载器。 /// 该实现用于 Runtime MVP 的文本配置接入阶段,通过显式注册表定义描述要加载的配置域, /// 再在一次加载流程中统一解析并写入配置注册表。 /// public sealed class YamlConfigLoader : IConfigLoader { private const string RootPathCannotBeNullOrWhiteSpaceMessage = "Root path cannot be null or whitespace."; private const string TableNameCannotBeNullOrWhiteSpaceMessage = "Table name cannot be null or whitespace."; private const string RelativePathCannotBeNullOrWhiteSpaceMessage = "Relative path cannot be null or whitespace."; private const string SchemaRelativePathCannotBeNullOrWhiteSpaceMessage = "Schema relative path cannot be null or whitespace."; private readonly IDeserializer _deserializer; private readonly Dictionary> _lastSuccessfulDependencies = new(StringComparer.Ordinal); private readonly List _registrations = new(); private readonly string _rootPath; /// /// 使用指定配置根目录创建 YAML 配置加载器。 /// /// 配置根目录。 /// 为空时抛出。 public YamlConfigLoader(string rootPath) { if (string.IsNullOrWhiteSpace(rootPath)) { throw new ArgumentException(RootPathCannotBeNullOrWhiteSpaceMessage, nameof(rootPath)); } _rootPath = rootPath; _deserializer = new DeserializerBuilder() .WithNamingConvention(CamelCaseNamingConvention.Instance) .IgnoreUnmatchedProperties() .Build(); } /// /// 获取配置根目录。 /// public string RootPath => _rootPath; /// /// 获取当前已注册的配置表定义数量。 /// public int RegistrationCount => _registrations.Count; /// public async Task LoadAsync(IConfigRegistry registry, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(registry); var loadedTables = new List(_registrations.Count); foreach (var registration in _registrations) { cancellationToken.ThrowIfCancellationRequested(); loadedTables.Add(await registration.LoadAsync(_rootPath, _deserializer, cancellationToken)); } CrossTableReferenceValidator.Validate(registry, loadedTables); // 仅当本轮所有配置表都成功加载后才写入注册表,避免暴露部分成功的中间状态。 foreach (var loadedTable in loadedTables) { RegistrationDispatcher.Register(registry, loadedTable.Name, loadedTable.Table); } UpdateLastSuccessfulDependencies(loadedTables); } /// /// 启用开发期热重载。 /// 该能力会监听已注册配置表对应的配置目录和 schema 文件,并在检测到文件变更后按表粒度重新加载。 /// 重载失败时会保留注册表中的旧表,避免开发期错误配置直接破坏当前运行时状态。 /// /// 要被热重载更新的配置注册表。 /// 单个配置表重载成功后的可选回调。 /// 单个配置表重载失败后的可选回调。 /// 防抖延迟;为空时默认使用 200 毫秒。 /// 用于停止热重载监听的注销句柄。 /// 为空时抛出。 public IUnRegister EnableHotReload( IConfigRegistry registry, Action? onTableReloaded = null, Action? onTableReloadFailed = null, TimeSpan? debounceDelay = null) { ArgumentNullException.ThrowIfNull(registry); return new HotReloadSession( _rootPath, _deserializer, registry, _registrations, _lastSuccessfulDependencies, onTableReloaded, onTableReloadFailed, debounceDelay ?? TimeSpan.FromMilliseconds(200)); } private void UpdateLastSuccessfulDependencies(IEnumerable loadedTables) { _lastSuccessfulDependencies.Clear(); foreach (var loadedTable in loadedTables) { _lastSuccessfulDependencies[loadedTable.Name] = loadedTable.ReferencedTableNames; } } /// /// 注册一个 YAML 配置表定义。 /// 主键提取逻辑由调用方显式提供,以避免在 Runtime MVP 阶段引入额外特性或约定推断。 /// /// 配置主键类型。 /// 配置值类型。 /// 配置表名称。 /// 相对配置根目录的子目录。 /// 配置项主键提取器。 /// 可选主键比较器。 /// 当前加载器实例,以便链式注册。 public YamlConfigLoader RegisterTable( string tableName, string relativePath, Func keySelector, IEqualityComparer? comparer = null) where TKey : notnull { return RegisterTableCore(tableName, relativePath, null, keySelector, comparer); } /// /// 注册一个带 schema 校验的 YAML 配置表定义。 /// 该重载会在 YAML 反序列化之前使用指定 schema 拒绝未知字段、缺失必填字段和基础类型错误, /// 以避免错误配置以默认值形式悄悄进入运行时。 /// /// 配置主键类型。 /// 配置值类型。 /// 配置表名称。 /// 相对配置根目录的子目录。 /// 相对配置根目录的 schema 文件路径。 /// 配置项主键提取器。 /// 可选主键比较器。 /// 当前加载器实例,以便链式注册。 public YamlConfigLoader RegisterTable( string tableName, string relativePath, string schemaRelativePath, Func keySelector, IEqualityComparer? comparer = null) where TKey : notnull { return RegisterTableCore(tableName, relativePath, schemaRelativePath, keySelector, comparer); } private YamlConfigLoader RegisterTableCore( string tableName, string relativePath, string? schemaRelativePath, Func keySelector, IEqualityComparer? comparer) where TKey : notnull { if (string.IsNullOrWhiteSpace(tableName)) { throw new ArgumentException(TableNameCannotBeNullOrWhiteSpaceMessage, nameof(tableName)); } if (string.IsNullOrWhiteSpace(relativePath)) { throw new ArgumentException(RelativePathCannotBeNullOrWhiteSpaceMessage, nameof(relativePath)); } ArgumentNullException.ThrowIfNull(keySelector); if (schemaRelativePath != null && string.IsNullOrWhiteSpace(schemaRelativePath)) { throw new ArgumentException( SchemaRelativePathCannotBeNullOrWhiteSpaceMessage, nameof(schemaRelativePath)); } _registrations.Add( new YamlTableRegistration( tableName, relativePath, schemaRelativePath, keySelector, comparer)); return this; } /// /// 负责在非泛型配置表与泛型注册表方法之间做分派。 /// 该静态助手将运行时反射局部封装在加载器内部,避免向外暴露弱类型注册 API。 /// private static class RegistrationDispatcher { /// /// 将强类型配置表写入注册表。 /// /// 目标配置注册表。 /// 配置表名称。 /// 已加载的配置表实例。 /// 当传入表未实现强类型配置表契约时抛出。 public static void Register(IConfigRegistry registry, string name, IConfigTable table) { var tableInterface = table.GetType() .GetInterfaces() .FirstOrDefault(static type => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IConfigTable<,>)); if (tableInterface == null) { throw new InvalidOperationException( $"Loaded config table '{name}' does not implement '{typeof(IConfigTable<,>).Name}'."); } var genericArguments = tableInterface.GetGenericArguments(); var method = typeof(IConfigRegistry) .GetMethod(nameof(IConfigRegistry.RegisterTable))! .MakeGenericMethod(genericArguments[0], genericArguments[1]); method.Invoke(registry, new object[] { name, table }); } } /// /// 定义 YAML 配置表注册项的统一内部契约。 /// private interface IYamlTableRegistration { /// /// 获取配置表名称。 /// string Name { get; } /// /// 获取相对配置根目录的子目录。 /// string RelativePath { get; } /// /// 获取相对配置根目录的 schema 文件路径;未启用 schema 校验时返回空。 /// string? SchemaRelativePath { get; } /// /// 从指定根目录加载配置表。 /// /// 配置根目录。 /// YAML 反序列化器。 /// 取消令牌。 /// 已加载的配置表结果。 Task LoadAsync( string rootPath, IDeserializer deserializer, CancellationToken cancellationToken); } /// /// YAML 配置表注册项。 /// /// 配置主键类型。 /// 配置项值类型。 private sealed class YamlTableRegistration : IYamlTableRegistration where TKey : notnull { private readonly IEqualityComparer? _comparer; private readonly Func _keySelector; /// /// 初始化 YAML 配置表注册项。 /// /// 配置表名称。 /// 相对配置根目录的子目录。 /// 相对配置根目录的 schema 文件路径;未启用 schema 校验时为空。 /// 配置项主键提取器。 /// 可选主键比较器。 public YamlTableRegistration( string name, string relativePath, string? schemaRelativePath, Func keySelector, IEqualityComparer? comparer) { Name = name; RelativePath = relativePath; SchemaRelativePath = schemaRelativePath; _keySelector = keySelector; _comparer = comparer; } /// /// 获取配置表名称。 /// public string Name { get; } /// /// 获取相对配置根目录的子目录。 /// public string RelativePath { get; } /// /// 获取相对配置根目录的 schema 文件路径;未启用 schema 校验时返回空。 /// public string? SchemaRelativePath { get; } /// public async Task LoadAsync( string rootPath, IDeserializer deserializer, CancellationToken cancellationToken) { var directoryPath = Path.Combine(rootPath, RelativePath); if (!Directory.Exists(directoryPath)) { throw new DirectoryNotFoundException( $"Config directory '{directoryPath}' was not found for table '{Name}'."); } YamlConfigSchema? schema = null; IReadOnlyCollection referencedTableNames = Array.Empty(); if (!string.IsNullOrEmpty(SchemaRelativePath)) { var schemaPath = Path.Combine(rootPath, SchemaRelativePath); schema = await YamlConfigSchemaValidator.LoadAsync(schemaPath, cancellationToken); referencedTableNames = schema.ReferencedTableNames; } var referenceUsages = new List(); var values = new List(); var files = Directory .EnumerateFiles(directoryPath, "*.*", SearchOption.TopDirectoryOnly) .Where(static path => path.EndsWith(".yaml", StringComparison.OrdinalIgnoreCase) || path.EndsWith(".yml", StringComparison.OrdinalIgnoreCase)) .OrderBy(static path => path, StringComparer.Ordinal) .ToArray(); foreach (var file in files) { cancellationToken.ThrowIfCancellationRequested(); string yaml; try { yaml = await File.ReadAllTextAsync(file, cancellationToken); } catch (Exception exception) { throw new InvalidOperationException( $"Failed to read config file '{file}' for table '{Name}'.", exception); } if (schema != null) { // 先按 schema 拒绝结构问题并提取跨表引用,避免被 IgnoreUnmatchedProperties 或默认值掩盖配置错误。 referenceUsages.AddRange( YamlConfigSchemaValidator.ValidateAndCollectReferences(schema, file, yaml)); } try { var value = deserializer.Deserialize(yaml); if (value == null) { throw new InvalidOperationException("YAML content was deserialized to null."); } values.Add(value); } catch (Exception exception) { throw new InvalidOperationException( $"Failed to deserialize config file '{file}' for table '{Name}' as '{typeof(TValue).Name}'.", exception); } } try { var table = new InMemoryConfigTable(values, _keySelector, _comparer); return new YamlTableLoadResult(Name, table, referencedTableNames, referenceUsages); } catch (Exception exception) { throw new InvalidOperationException( $"Failed to build config table '{Name}' from directory '{directoryPath}'.", exception); } } } /// /// 表示单个注册项加载完成后的中间结果。 /// 该结果同时携带配置表实例、schema 声明的依赖关系和 YAML 中提取出的实际引用,以便在批量提交前完成跨表一致性校验。 /// private sealed class YamlTableLoadResult { /// /// 初始化一个表加载结果。 /// /// 配置表名称。 /// 已构建好的配置表。 /// schema 声明的依赖表名称集合。 /// YAML 中提取出的实际引用集合。 public YamlTableLoadResult( string name, IConfigTable table, IReadOnlyCollection referencedTableNames, IReadOnlyCollection referenceUsages) { ArgumentNullException.ThrowIfNull(name); ArgumentNullException.ThrowIfNull(table); ArgumentNullException.ThrowIfNull(referencedTableNames); ArgumentNullException.ThrowIfNull(referenceUsages); Name = name; Table = table; ReferencedTableNames = referencedTableNames; ReferenceUsages = referenceUsages; } /// /// 获取配置表名称。 /// public string Name { get; } /// /// 获取已构建好的配置表。 /// public IConfigTable Table { get; } /// /// 获取 schema 声明的依赖表名称集合。 /// public IReadOnlyCollection ReferencedTableNames { get; } /// /// 获取 YAML 中提取出的实际引用集合。 /// public IReadOnlyCollection ReferenceUsages { get; } } /// /// 负责在所有注册项加载完成后执行跨表引用校验。 /// 该阶段在真正写入注册表之前运行,确保任何缺失目标表、主键类型不兼容或目标行不存在的情况都会整体回滚。 /// private static class CrossTableReferenceValidator { /// /// 使用本轮新加载结果与注册表中保留的旧表,一起验证跨表引用是否全部有效。 /// /// 当前配置注册表。 /// 本轮加载出的配置表集合。 public static void Validate(IConfigRegistry registry, IReadOnlyCollection loadedTables) { ArgumentNullException.ThrowIfNull(registry); ArgumentNullException.ThrowIfNull(loadedTables); var loadedTableLookup = loadedTables.ToDictionary(static table => table.Name, StringComparer.Ordinal); foreach (var loadedTable in loadedTables) { foreach (var referenceUsage in loadedTable.ReferenceUsages) { if (!TryResolveTargetTable(registry, loadedTableLookup, referenceUsage.ReferencedTableName, out var targetTable)) { throw new InvalidOperationException( $"Config file '{referenceUsage.YamlPath}' property '{referenceUsage.DisplayPath}' references table '{referenceUsage.ReferencedTableName}', but that table is not available in the current loader batch or registry."); } if (!TryConvertReferenceKey(referenceUsage, targetTable.KeyType, out var convertedKey, out var conversionError)) { throw new InvalidOperationException( $"Config file '{referenceUsage.YamlPath}' property '{referenceUsage.DisplayPath}' cannot target table '{referenceUsage.ReferencedTableName}' with key type '{targetTable.KeyType.Name}'. {conversionError}"); } if (!ContainsKey(targetTable, convertedKey!)) { throw new InvalidOperationException( $"Config file '{referenceUsage.YamlPath}' property '{referenceUsage.DisplayPath}' references missing key '{referenceUsage.RawValue}' in table '{referenceUsage.ReferencedTableName}'."); } } } } private static bool TryResolveTargetTable( IConfigRegistry registry, IReadOnlyDictionary loadedTableLookup, string tableName, out IConfigTable table) { if (loadedTableLookup.TryGetValue(tableName, out var loadedTable)) { table = loadedTable.Table; return true; } if (registry.TryGetTable(tableName, out var registeredTable) && registeredTable != null) { table = registeredTable; return true; } table = null!; return false; } private static bool TryConvertReferenceKey( YamlConfigReferenceUsage referenceUsage, Type targetKeyType, out object? convertedKey, out string errorMessage) { convertedKey = null; errorMessage = string.Empty; if (referenceUsage.ValueType == YamlConfigSchemaPropertyType.String) { if (targetKeyType != typeof(string)) { errorMessage = $"Reference values declared as schema type 'string' can currently only target string-key tables, but the target key type is '{targetKeyType.Name}'."; return false; } convertedKey = referenceUsage.RawValue; return true; } if (referenceUsage.ValueType != YamlConfigSchemaPropertyType.Integer) { errorMessage = $"Reference values currently only support schema scalar types 'string' and 'integer', but the actual type is '{referenceUsage.ValueType}'."; return false; } return TryConvertIntegerKey(referenceUsage.RawValue, targetKeyType, out convertedKey, out errorMessage); } private static bool TryConvertIntegerKey( string rawValue, Type targetKeyType, out object? convertedKey, out string errorMessage) { convertedKey = null; errorMessage = string.Empty; if (targetKeyType == typeof(int) && int.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intValue)) { convertedKey = intValue; return true; } if (targetKeyType == typeof(long) && long.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var longValue)) { convertedKey = longValue; return true; } if (targetKeyType == typeof(short) && short.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var shortValue)) { convertedKey = shortValue; return true; } if (targetKeyType == typeof(byte) && byte.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var byteValue)) { convertedKey = byteValue; return true; } if (targetKeyType == typeof(uint) && uint.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var uintValue)) { convertedKey = uintValue; return true; } if (targetKeyType == typeof(ulong) && ulong.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ulongValue)) { convertedKey = ulongValue; return true; } if (targetKeyType == typeof(ushort) && ushort.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var ushortValue)) { convertedKey = ushortValue; return true; } if (targetKeyType == typeof(sbyte) && sbyte.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var sbyteValue)) { convertedKey = sbyteValue; return true; } errorMessage = $"Reference value '{rawValue}' cannot be converted to supported target key type '{targetKeyType.Name}'. Integer references currently support the standard signed and unsigned integer CLR key types."; return false; } private static bool ContainsKey(IConfigTable table, object key) { var tableInterface = table.GetType() .GetInterfaces() .First(static type => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IConfigTable<,>)); var containsKeyMethod = tableInterface.GetMethod(nameof(IConfigTable.ContainsKey))!; return (bool)containsKeyMethod.Invoke(table, new[] { key })!; } } /// /// 封装开发期热重载所需的文件监听与按表重载逻辑。 /// 该会话只影响通过当前加载器注册的表,不尝试接管注册表中的其他来源数据。 /// private sealed class HotReloadSession : IUnRegister, IDisposable { private readonly TimeSpan _debounceDelay; private readonly Dictionary> _dependenciesByTable = new(StringComparer.Ordinal); private readonly IDeserializer _deserializer; private readonly object _gate = new(); private readonly Action? _onTableReloaded; private readonly Action? _onTableReloadFailed; private readonly Dictionary _registrations = new(StringComparer.Ordinal); private readonly IConfigRegistry _registry; private readonly Dictionary _reloadLocks = new(StringComparer.Ordinal); private readonly Dictionary _reloadTokens = new(StringComparer.Ordinal); private readonly string _rootPath; private readonly List _watchers = new(); private bool _disposed; /// /// 初始化一个热重载会话并立即开始监听文件变更。 /// /// 配置根目录。 /// YAML 反序列化器。 /// 要更新的配置注册表。 /// 已注册的配置表定义。 /// 最近一次成功加载后记录下来的跨表依赖图。 /// 单表重载成功回调。 /// 单表重载失败回调。 /// 监听事件防抖延迟。 public HotReloadSession( string rootPath, IDeserializer deserializer, IConfigRegistry registry, IEnumerable registrations, IReadOnlyDictionary> initialDependencies, Action? onTableReloaded, Action? onTableReloadFailed, TimeSpan debounceDelay) { ArgumentNullException.ThrowIfNull(rootPath); ArgumentNullException.ThrowIfNull(deserializer); ArgumentNullException.ThrowIfNull(registry); ArgumentNullException.ThrowIfNull(registrations); ArgumentNullException.ThrowIfNull(initialDependencies); _rootPath = rootPath; _deserializer = deserializer; _registry = registry; _onTableReloaded = onTableReloaded; _onTableReloadFailed = onTableReloadFailed; _debounceDelay = debounceDelay; foreach (var registration in registrations) { _registrations.Add(registration.Name, registration); _reloadLocks.Add(registration.Name, new SemaphoreSlim(1, 1)); _dependenciesByTable[registration.Name] = initialDependencies.TryGetValue(registration.Name, out var dependencies) ? dependencies : Array.Empty(); CreateWatchersForRegistration(registration); } } /// /// 释放热重载会话持有的文件监听器与等待资源。 /// public void Dispose() { List watchersToDispose; List reloadTokensToDispose; List reloadLocksToDispose; lock (_gate) { if (_disposed) { return; } _disposed = true; watchersToDispose = _watchers.ToList(); _watchers.Clear(); reloadTokensToDispose = _reloadTokens.Values.ToList(); _reloadTokens.Clear(); reloadLocksToDispose = _reloadLocks.Values.ToList(); _reloadLocks.Clear(); } foreach (var reloadToken in reloadTokensToDispose) { reloadToken.Cancel(); reloadToken.Dispose(); } foreach (var watcher in watchersToDispose) { watcher.Dispose(); } foreach (var reloadLock in reloadLocksToDispose) { reloadLock.Dispose(); } } /// /// 停止热重载监听。 /// public void UnRegister() { Dispose(); } private void CreateWatchersForRegistration(IYamlTableRegistration registration) { var configDirectoryPath = Path.Combine(_rootPath, registration.RelativePath); AddWatcher(configDirectoryPath, "*.yaml", registration.Name); AddWatcher(configDirectoryPath, "*.yml", registration.Name); if (string.IsNullOrEmpty(registration.SchemaRelativePath)) { return; } var schemaFullPath = Path.Combine(_rootPath, registration.SchemaRelativePath); var schemaDirectoryPath = Path.GetDirectoryName(schemaFullPath); if (string.IsNullOrWhiteSpace(schemaDirectoryPath)) { schemaDirectoryPath = _rootPath; } AddWatcher(schemaDirectoryPath, Path.GetFileName(schemaFullPath), registration.Name); } private void AddWatcher(string directoryPath, string filter, string tableName) { if (!Directory.Exists(directoryPath)) { return; } var watcher = new FileSystemWatcher(directoryPath, filter) { IncludeSubdirectories = false, NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.CreationTime | NotifyFilters.DirectoryName }; watcher.Changed += (_, _) => ScheduleReload(tableName); watcher.Created += (_, _) => ScheduleReload(tableName); watcher.Deleted += (_, _) => ScheduleReload(tableName); watcher.Renamed += (_, _) => ScheduleReload(tableName); watcher.Error += (_, eventArgs) => { var exception = eventArgs.GetException() ?? new InvalidOperationException( $"Hot reload watcher for table '{tableName}' encountered an unknown error."); InvokeReloadFailed(tableName, exception); }; watcher.EnableRaisingEvents = true; lock (_gate) { if (_disposed) { watcher.Dispose(); return; } _watchers.Add(watcher); } } private void ScheduleReload(string tableName) { CancellationTokenSource reloadTokenSource; lock (_gate) { if (_disposed) { return; } if (_reloadTokens.TryGetValue(tableName, out var previousTokenSource)) { previousTokenSource.Cancel(); previousTokenSource.Dispose(); } reloadTokenSource = new CancellationTokenSource(); _reloadTokens[tableName] = reloadTokenSource; } _ = Task.Run(async () => { try { await Task.Delay(_debounceDelay, reloadTokenSource.Token); await ReloadTableAsync(tableName, reloadTokenSource.Token); } catch (OperationCanceledException) when (reloadTokenSource.IsCancellationRequested) { // 新事件会替换旧任务;取消属于正常防抖行为。 } finally { lock (_gate) { if (_reloadTokens.TryGetValue(tableName, out var currentTokenSource) && ReferenceEquals(currentTokenSource, reloadTokenSource)) { _reloadTokens.Remove(tableName); } } reloadTokenSource.Dispose(); } }); } private async Task ReloadTableAsync(string tableName, CancellationToken cancellationToken) { if (!_registrations.ContainsKey(tableName)) { return; } var reloadLock = _reloadLocks[tableName]; await reloadLock.WaitAsync(cancellationToken); try { cancellationToken.ThrowIfCancellationRequested(); var affectedTableNames = GetAffectedTableNames(tableName); var loadedTables = new List(affectedTableNames.Count); // 目标表变更可能让依赖它的表立即失效,因此热重载需要按受影响闭包整体重验并整体提交。 foreach (var affectedTableName in affectedTableNames) { cancellationToken.ThrowIfCancellationRequested(); loadedTables.Add(await _registrations[affectedTableName].LoadAsync(_rootPath, _deserializer, cancellationToken)); } CrossTableReferenceValidator.Validate(_registry, loadedTables); foreach (var loadedTable in loadedTables) { RegistrationDispatcher.Register(_registry, loadedTable.Name, loadedTable.Table); _dependenciesByTable[loadedTable.Name] = loadedTable.ReferencedTableNames; } InvokeReloaded(tableName); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { // 防抖替换或会话关闭导致的取消不应视为错误。 } catch (Exception exception) { InvokeReloadFailed(tableName, exception); } finally { reloadLock.Release(); } } private IReadOnlyCollection GetAffectedTableNames(string changedTableName) { var affectedTableNames = new HashSet(StringComparer.Ordinal) { changedTableName }; var pendingTableNames = new Queue(); pendingTableNames.Enqueue(changedTableName); while (pendingTableNames.Count > 0) { var currentTableName = pendingTableNames.Dequeue(); foreach (var dependency in _dependenciesByTable) { if (!dependency.Value.Contains(currentTableName)) { continue; } if (affectedTableNames.Add(dependency.Key)) { pendingTableNames.Enqueue(dependency.Key); } } } return affectedTableNames .OrderBy(static name => name, StringComparer.Ordinal) .ToArray(); } private void InvokeReloaded(string tableName) { if (_onTableReloaded == null) { return; } try { _onTableReloaded(tableName); } catch { // 诊断回调不应反向破坏热重载流程。 } } private void InvokeReloadFailed(string tableName, Exception exception) { if (_onTableReloadFailed == null) { return; } try { _onTableReloadFailed(tableName, exception); } catch { // 诊断回调不应反向破坏热重载流程。 } } } }