From 23489570bf0f02b604246cfb0dd00480a631fce3 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:47:44 +0800 Subject: [PATCH 1/7] =?UTF-8?q?fix(analyzers):=20=E9=99=8D=E4=BD=8E=20Core?= =?UTF-8?q?=E3=80=81Cqrs=E3=80=81Godot=20=E4=B8=8E=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=99=A8=E7=9A=84=E6=9E=84=E5=BB=BA=E8=AD=A6=E5=91=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 清理 GFramework.Core 与 GFramework.Cqrs 中的大量低风险 Meziantou 警告 - 修复 GFramework.Godot 运行时中的 ConfigureAwait、StringComparison 与参数校验告警 - 调整 Core SourceGenerators 中的字符串比较、文件命名与局部长方法问题 - 拆分部分配置与缓存辅助类型文件以消除 file/type mismatch 告警 - 更新 warning reduction 跟踪与执行记录,保留下一批结构性告警的恢复点 --- .../Analyzers/ContextRegistrationAnalyzer.cs | 6 +- .../Analyzers/PriorityUsageAnalyzer.cs | 2 +- .../AutoRegisterModuleGenerator.cs | 20 +- ...ggerDiagnostic.cs => LoggerDiagnostics.cs} | 0 .../Enums/EnumExtensionsGenerator.cs | 33 ++- .../State/StateMachineSystemTests.cs | 2 +- GFramework.Core/Architectures/Architecture.cs | 9 +- .../Architectures/ArchitectureBootstrapper.cs | 4 +- .../Architectures/ArchitectureContext.cs | 18 +- .../Architectures/ArchitectureDisposer.cs | 10 +- .../Architectures/ArchitectureLifecycle.cs | 12 +- .../Command/AbstractAsyncCommand.cs | 4 +- .../Command/AbstractAsyncCommandWithInput.cs | 2 +- .../Command/AbstractAsyncCommandWithResult.cs | 2 +- .../Concurrency/AsyncKeyLockManager.cs | 7 +- .../Configuration/ConfigurationManager.cs | 11 +- GFramework.Core/Coroutine/CoroutineHandle.cs | 11 +- .../Coroutine/CoroutineScheduler.cs | 6 +- .../Coroutine/CoroutineStatistics.cs | 31 +-- .../Coroutine/Instructions/WaitForTask.T.cs | 4 +- .../Coroutine/Instructions/WaitForTask.cs | 4 +- .../Environment/EnvironmentBase.cs | 4 +- GFramework.Core/Events/EventStatistics.cs | 26 +-- GFramework.Core/Extensions/AsyncExtensions.cs | 4 +- .../ContextAwareCommandExtensions.cs | 6 +- .../Extensions/ContextAwareQueryExtensions.cs | 4 +- .../Extensions/NumericExtensions.cs | 4 +- .../Async/AsyncFunctionalExtensions.cs | 8 +- GFramework.Core/Functional/Result.T.cs | 6 +- GFramework.Core/Functional/Result.cs | 4 +- .../Functional/ResultExtensions.cs | 11 +- GFramework.Core/Ioc/MicrosoftDiContainer.cs | 5 +- .../CompactNumberLocalizationFormatter.cs | 8 +- .../Localization/LocalizationManager.cs | 14 +- .../Localization/LocalizationString.cs | 9 +- .../Localization/LocalizationTable.cs | 8 +- .../Logging/AppenderConfiguration.cs | 54 +++++ .../Logging/Appenders/AsyncLogAppender.cs | 4 +- .../Logging/Appenders/StatisticsAppender.cs | 28 +-- .../Logging/CachedLoggerFactory.cs | 4 +- GFramework.Core/Logging/CompositeLogger.cs | 6 +- .../Logging/ConfigurableLoggerFactory.cs | 78 +++++++ GFramework.Core/Logging/ConsoleLogger.cs | 5 +- .../Logging/FilterConfiguration.cs | 29 +++ .../Logging/Filters/SamplingFilter.cs | 6 +- .../Logging/Formatters/DefaultLogFormatter.cs | 5 +- .../Logging/Formatters/JsonLogFormatter.cs | 7 +- .../Logging/LoggingConfiguration.cs | 79 +------ .../Logging/LoggingConfigurationLoader.cs | 63 ------ .../Pool/AbstractObjectPoolSystem.cs | 4 +- GFramework.Core/Resource/ResourceCache.cs | 19 +- .../Resource/ResourceCacheEntry.cs | 27 +++ GFramework.Core/Resource/ResourceManager.cs | 6 +- .../Services/ServiceModuleManager.cs | 6 +- GFramework.Core/State/StateMachine.cs | 46 ++--- GFramework.Core/State/StateMachineSystem.cs | 8 +- .../Cqrs/Behaviors/LoggingBehavior.cs | 2 +- .../Cqrs/Behaviors/PerformanceBehavior.cs | 2 +- GFramework.Cqrs/Internal/CqrsDispatcher.cs | 7 +- GFramework.Cqrs/Internal/WeakKeyCache.cs | 95 +-------- GFramework.Cqrs/Internal/WeakTypePairCache.cs | 88 ++++++++ .../Architectures/AbstractArchitecture.cs | 8 +- .../Config/GodotYamlConfigDirectoryEntry.cs | 33 +++ .../Config/GodotYamlConfigEnvironment.cs | 163 +++++++++++++++ .../Config/GodotYamlConfigLoader.cs | 193 +----------------- .../Data/GodotResourceRepository.cs | 5 +- .../Extensions/GodotPathExtensions.cs | 6 +- GFramework.Godot/Extensions/NodeExtensions.cs | 4 +- GFramework.Godot/Logging/GodotLogger.cs | 7 +- GFramework.Godot/Scene/SceneBehaviorBase.cs | 14 +- .../Setting/Data/LocalizationMap.cs | 6 +- .../Setting/GodotGraphicsSettings.cs | 6 +- GFramework.Godot/Storage/GodotFileStorage.cs | 20 +- GFramework.Godot/UI/UiPageBehaviorFactory.cs | 4 +- 74 files changed, 781 insertions(+), 685 deletions(-) rename GFramework.Core.SourceGenerators/Diagnostics/{LoggerDiagnostic.cs => LoggerDiagnostics.cs} (100%) create mode 100644 GFramework.Core/Logging/AppenderConfiguration.cs create mode 100644 GFramework.Core/Logging/ConfigurableLoggerFactory.cs create mode 100644 GFramework.Core/Logging/FilterConfiguration.cs create mode 100644 GFramework.Core/Resource/ResourceCacheEntry.cs create mode 100644 GFramework.Cqrs/Internal/WeakTypePairCache.cs create mode 100644 GFramework.Godot/Config/GodotYamlConfigDirectoryEntry.cs create mode 100644 GFramework.Godot/Config/GodotYamlConfigEnvironment.cs diff --git a/GFramework.Core.SourceGenerators/Analyzers/ContextRegistrationAnalyzer.cs b/GFramework.Core.SourceGenerators/Analyzers/ContextRegistrationAnalyzer.cs index 57c49d08..dc6be9b0 100644 --- a/GFramework.Core.SourceGenerators/Analyzers/ContextRegistrationAnalyzer.cs +++ b/GFramework.Core.SourceGenerators/Analyzers/ContextRegistrationAnalyzer.cs @@ -635,7 +635,7 @@ public sealed class ContextRegistrationAnalyzer : DiagnosticAnalyzer if (targetMethod.TypeArguments[0] is not INamedTypeSymbol namedType) return false; - if (targetMethod.Name == "RegisterModel" && + if (string.Equals(targetMethod.Name, "RegisterModel", StringComparison.Ordinal) && SymbolHelpers.IsAssignableTo(targetMethod.ContainingType, symbols.IArchitectureType)) { componentKind = ComponentKind.Model; @@ -643,7 +643,7 @@ public sealed class ContextRegistrationAnalyzer : DiagnosticAnalyzer return true; } - if (targetMethod.Name == "RegisterSystem" && + if (string.Equals(targetMethod.Name, "RegisterSystem", StringComparison.Ordinal) && SymbolHelpers.IsAssignableTo(targetMethod.ContainingType, symbols.IArchitectureType)) { componentKind = ComponentKind.System; @@ -651,7 +651,7 @@ public sealed class ContextRegistrationAnalyzer : DiagnosticAnalyzer return true; } - if (targetMethod.Name == "RegisterUtility" && + if (string.Equals(targetMethod.Name, "RegisterUtility", StringComparison.Ordinal) && SymbolHelpers.IsAssignableTo(targetMethod.ContainingType, symbols.IArchitectureType)) { componentKind = ComponentKind.Utility; diff --git a/GFramework.Core.SourceGenerators/Analyzers/PriorityUsageAnalyzer.cs b/GFramework.Core.SourceGenerators/Analyzers/PriorityUsageAnalyzer.cs index 520448a6..0761765e 100644 --- a/GFramework.Core.SourceGenerators/Analyzers/PriorityUsageAnalyzer.cs +++ b/GFramework.Core.SourceGenerators/Analyzers/PriorityUsageAnalyzer.cs @@ -62,7 +62,7 @@ public sealed class PriorityUsageAnalyzer : DiagnosticAnalyzer var method = invocation.TargetMethod; // 检查方法名是否为 GetAll - if (method.Name != "GetAll") + if (!string.Equals(method.Name, "GetAll", StringComparison.Ordinal)) return; // 检查是否为泛型方法 diff --git a/GFramework.Core.SourceGenerators/Architectures/AutoRegisterModuleGenerator.cs b/GFramework.Core.SourceGenerators/Architectures/AutoRegisterModuleGenerator.cs index 93771bc1..fe593909 100644 --- a/GFramework.Core.SourceGenerators/Architectures/AutoRegisterModuleGenerator.cs +++ b/GFramework.Core.SourceGenerators/Architectures/AutoRegisterModuleGenerator.cs @@ -179,12 +179,7 @@ public sealed class AutoRegisterModuleGenerator : IIncrementalGenerator { var registrations = new List(); - foreach (var attribute in typeSymbol.GetAttributes() - // Roslyn 会把 partial 类型上的属性合并到同一个集合中。 - // 先按语法树标识排序,才能让每个文件内的 Span.Start 成为可比较的稳定顺序键。 - .OrderBy(GetAttributeSyntaxTreeOrderKey, StringComparer.Ordinal) - .ThenBy(GetAttributeOrder) - .ThenBy(GetAttributeTypeOrderKey, StringComparer.Ordinal)) + foreach (var attribute in GetOrderedRegistrationAttributes(typeSymbol)) { if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, registerModelAttribute)) { @@ -239,6 +234,16 @@ public sealed class AutoRegisterModuleGenerator : IIncrementalGenerator return registrations; } + private static IOrderedEnumerable GetOrderedRegistrationAttributes(INamedTypeSymbol typeSymbol) + { + // Roslyn 会把 partial 类型上的属性合并到同一个集合中。 + // 先按语法树标识排序,才能让每个文件内的 Span.Start 成为可比较的稳定顺序键。 + return typeSymbol.GetAttributes() + .OrderBy(GetAttributeSyntaxTreeOrderKey, StringComparer.Ordinal) + .ThenBy(GetAttributeOrder) + .ThenBy(GetAttributeTypeOrderKey, StringComparer.Ordinal); + } + private static bool TryCreateRegistration( SourceProductionContext context, INamedTypeSymbol ownerType, @@ -323,7 +328,8 @@ public sealed class AutoRegisterModuleGenerator : IIncrementalGenerator RegistrationKind.Model => "RegisterModel", RegistrationKind.System => "RegisterSystem", RegistrationKind.Utility => "RegisterUtility", - _ => throw new ArgumentOutOfRangeException(nameof(registration.Kind)) + _ => throw new InvalidOperationException( + $"Unsupported registration kind '{registration.Kind}'.") }); builder.Append("(new "); builder.Append(registration.ComponentTypeDisplayName); diff --git a/GFramework.Core.SourceGenerators/Diagnostics/LoggerDiagnostic.cs b/GFramework.Core.SourceGenerators/Diagnostics/LoggerDiagnostics.cs similarity index 100% rename from GFramework.Core.SourceGenerators/Diagnostics/LoggerDiagnostic.cs rename to GFramework.Core.SourceGenerators/Diagnostics/LoggerDiagnostics.cs diff --git a/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs b/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs index c89f18ca..ff1acccc 100644 --- a/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs +++ b/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs @@ -72,14 +72,7 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase ? null : symbol.ContainingNamespace.ToDisplayString(); - var generateIsMethods = GetNamedBooleanArgument( - attr, - nameof(GenerateEnumExtensionsAttribute.GenerateIsMethods), - true); - var generateIsInMethod = GetNamedBooleanArgument( - attr, - nameof(GenerateEnumExtensionsAttribute.GenerateIsInMethod), - true); + var generationOptions = GetGenerationOptions(attr); var enumName = symbol.Name; var fullEnumName = symbol.ToDisplayString(); var members = symbol.GetMembers() @@ -104,7 +97,7 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase // 两个生成开关是彼此独立的契约,需要分别控制输出,并保持空行布局稳定,便于快照精确回归。 var hasGeneratedMembers = false; - if (generateIsMethods) + if (generationOptions.GenerateIsMethods) { hasGeneratedMembers = AppendIsMethods( sb, @@ -112,7 +105,7 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase fullEnumName); } - if (generateIsInMethod) + if (generationOptions.GenerateIsInMethod) { if (hasGeneratedMembers) { @@ -130,6 +123,24 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase return sb.ToString(); } + /// + /// 读取枚举扩展生成选项,并在属性未显式指定时回退到契约默认值。 + /// + /// 待分析的特性数据。 + /// 包含各个生成开关的选项元组。 + private static (bool GenerateIsMethods, bool GenerateIsInMethod) GetGenerationOptions(AttributeData attribute) + { + return ( + GetNamedBooleanArgument( + attribute, + nameof(GenerateEnumExtensionsAttribute.GenerateIsMethods), + true), + GetNamedBooleanArgument( + attribute, + nameof(GenerateEnumExtensionsAttribute.GenerateIsInMethod), + true)); + } + /// /// 获取生成文件的提示名称 /// @@ -151,7 +162,7 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase { foreach (var namedArgument in attribute.NamedArguments) { - if (namedArgument.Key == argumentName && + if (string.Equals(namedArgument.Key, argumentName, StringComparison.Ordinal) && namedArgument.Value.Value is bool value) { return value; diff --git a/GFramework.Core.Tests/State/StateMachineSystemTests.cs b/GFramework.Core.Tests/State/StateMachineSystemTests.cs index 2c234238..9ee084ff 100644 --- a/GFramework.Core.Tests/State/StateMachineSystemTests.cs +++ b/GFramework.Core.Tests/State/StateMachineSystemTests.cs @@ -250,7 +250,7 @@ public class TestStateMachineSystemV5 : StateMachineSystem /// 获取状态机内部的状态字典 /// /// 类型到状态实例的映射字典 - public Dictionary GetStates() + public IDictionary GetStates() { return States; } diff --git a/GFramework.Core/Architectures/Architecture.cs b/GFramework.Core/Architectures/Architecture.cs index cf55b192..b9e2ecf6 100644 --- a/GFramework.Core/Architectures/Architecture.cs +++ b/GFramework.Core/Architectures/Architecture.cs @@ -288,7 +288,7 @@ public abstract class Architecture : IArchitecture { try { - await InitializeInternalAsync(true); + await InitializeInternalAsync(true).ConfigureAwait(false); } catch (Exception e) { @@ -304,7 +304,8 @@ public abstract class Architecture : IArchitecture /// 是否启用异步模式 private async Task InitializeInternalAsync(bool asyncMode) { - _context = await _bootstrapper.PrepareForInitializationAsync(_context, Configurator, asyncMode); + _context = await _bootstrapper.PrepareForInitializationAsync(_context, Configurator, asyncMode) + .ConfigureAwait(false); // === 用户 OnInitialize === _logger.Debug("Calling user OnInitialize()"); @@ -312,7 +313,7 @@ public abstract class Architecture : IArchitecture _logger.Debug("User OnInitialize() completed"); // === 组件初始化阶段 === - await _lifecycle.InitializeAllComponentsAsync(asyncMode); + await _lifecycle.InitializeAllComponentsAsync(asyncMode).ConfigureAwait(false); // === 初始化完成阶段 === _bootstrapper.CompleteInitialization(); @@ -337,7 +338,7 @@ public abstract class Architecture : IArchitecture /// public virtual async ValueTask DestroyAsync() { - await _lifecycle.DestroyAsync(); + await _lifecycle.DestroyAsync().ConfigureAwait(false); } /// diff --git a/GFramework.Core/Architectures/ArchitectureBootstrapper.cs b/GFramework.Core/Architectures/ArchitectureBootstrapper.cs index 343bcdd6..ae0bdd38 100644 --- a/GFramework.Core/Architectures/ArchitectureBootstrapper.cs +++ b/GFramework.Core/Architectures/ArchitectureBootstrapper.cs @@ -35,7 +35,7 @@ internal sealed class ArchitectureBootstrapper( var context = EnsureContext(existingContext); ConfigureServices(context, configurator); - await InitializeServiceModulesAsync(asyncMode); + await InitializeServiceModulesAsync(asyncMode).ConfigureAwait(false); return context; } @@ -117,6 +117,6 @@ internal sealed class ArchitectureBootstrapper( /// 是否允许异步初始化服务模块。 private async Task InitializeServiceModulesAsync(bool asyncMode) { - await services.ModuleManager.InitializeAllAsync(asyncMode); + await services.ModuleManager.InitializeAllAsync(asyncMode).ConfigureAwait(false); } } diff --git a/GFramework.Core/Architectures/ArchitectureContext.cs b/GFramework.Core/Architectures/ArchitectureContext.cs index e0ac2dd6..8a64e98e 100644 --- a/GFramework.Core/Architectures/ArchitectureContext.cs +++ b/GFramework.Core/Architectures/ArchitectureContext.cs @@ -97,7 +97,7 @@ public class ArchitectureContext : IArchitectureContext CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(request); - return await CqrsRuntime.SendAsync(this, request, cancellationToken); + return await CqrsRuntime.SendAsync(this, request, cancellationToken).ConfigureAwait(false); } /// @@ -124,7 +124,7 @@ public class ArchitectureContext : IArchitectureContext where TNotification : INotification { ArgumentNullException.ThrowIfNull(notification); - await CqrsRuntime.PublishAsync(this, notification, cancellationToken); + await CqrsRuntime.PublishAsync(this, notification, cancellationToken).ConfigureAwait(false); } /// @@ -151,7 +151,7 @@ public class ArchitectureContext : IArchitectureContext CancellationToken cancellationToken = default) where TCommand : IRequest { - await SendRequestAsync(command, cancellationToken); + await SendRequestAsync(command, cancellationToken).ConfigureAwait(false); } /// @@ -162,7 +162,7 @@ public class ArchitectureContext : IArchitectureContext IRequest command, CancellationToken cancellationToken = default) { - return await SendRequestAsync(command, cancellationToken); + return await SendRequestAsync(command, cancellationToken).ConfigureAwait(false); } #endregion @@ -205,7 +205,7 @@ public class ArchitectureContext : IArchitectureContext if (query == null) throw new ArgumentNullException(nameof(query)); var asyncQueryBus = GetOrCache(); if (asyncQueryBus == null) throw new InvalidOperationException("IAsyncQueryExecutor not registered"); - return await asyncQueryBus.SendAsync(query); + return await asyncQueryBus.SendAsync(query).ConfigureAwait(false); } /// @@ -219,7 +219,7 @@ public class ArchitectureContext : IArchitectureContext CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(query); - return await SendRequestAsync(query, cancellationToken); + return await SendRequestAsync(query, cancellationToken).ConfigureAwait(false); } #endregion @@ -356,7 +356,7 @@ public class ArchitectureContext : IArchitectureContext CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(command); - return await SendRequestAsync(command, cancellationToken); + return await SendRequestAsync(command, cancellationToken).ConfigureAwait(false); } /// @@ -368,7 +368,7 @@ public class ArchitectureContext : IArchitectureContext ArgumentNullException.ThrowIfNull(command); var commandBus = GetOrCache(); if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered"); - await commandBus.SendAsync(command); + await commandBus.SendAsync(command).ConfigureAwait(false); } /// @@ -382,7 +382,7 @@ public class ArchitectureContext : IArchitectureContext ArgumentNullException.ThrowIfNull(command); var commandBus = GetOrCache(); if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered"); - return await commandBus.SendAsync(command); + return await commandBus.SendAsync(command).ConfigureAwait(false); } /// diff --git a/GFramework.Core/Architectures/ArchitectureDisposer.cs b/GFramework.Core/Architectures/ArchitectureDisposer.cs index c2c7f553..e87d25a7 100644 --- a/GFramework.Core/Architectures/ArchitectureDisposer.cs +++ b/GFramework.Core/Architectures/ArchitectureDisposer.cs @@ -59,15 +59,15 @@ internal sealed class ArchitectureDisposer( if (currentPhase == ArchitecturePhase.None) { logger.Debug("Architecture destroy called but never initialized, cleaning up registered components"); - await CleanupComponentsAsync(); + await CleanupComponentsAsync().ConfigureAwait(false); return; } logger.Info("Starting architecture destruction"); enterPhase(ArchitecturePhase.Destroying); - await CleanupComponentsAsync(); - await services.ModuleManager.DestroyAllAsync(); + await CleanupComponentsAsync().ConfigureAwait(false); + await services.ModuleManager.DestroyAllAsync().ConfigureAwait(false); // Destroyed 广播依赖容器中的阶段监听器,必须在清空容器前完成。 enterPhase(ArchitecturePhase.Destroyed); @@ -93,7 +93,7 @@ internal sealed class ArchitectureDisposer( if (component is IAsyncDestroyable asyncDestroyable) { - await asyncDestroyable.DestroyAsync(); + await asyncDestroyable.DestroyAsync().ConfigureAwait(false); } else if (component is IDestroyable destroyable) { @@ -109,4 +109,4 @@ internal sealed class ArchitectureDisposer( _disposables.Clear(); _disposableSet.Clear(); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Architectures/ArchitectureLifecycle.cs b/GFramework.Core/Architectures/ArchitectureLifecycle.cs index 6cb8f3df..d503f68c 100644 --- a/GFramework.Core/Architectures/ArchitectureLifecycle.cs +++ b/GFramework.Core/Architectures/ArchitectureLifecycle.cs @@ -160,7 +160,7 @@ internal sealed class ArchitectureLifecycle( foreach (var utility in utilities) { logger.Debug($"Initializing utility: {utility.GetType().Name}"); - await InitializeComponentAsync(utility, asyncMode); + await InitializeComponentAsync(utility, asyncMode).ConfigureAwait(false); } logger.Info("All context utilities initialized"); @@ -178,7 +178,7 @@ internal sealed class ArchitectureLifecycle( foreach (var model in models) { logger.Debug($"Initializing model: {model.GetType().Name}"); - await InitializeComponentAsync(model, asyncMode); + await InitializeComponentAsync(model, asyncMode).ConfigureAwait(false); } logger.Info("All models initialized"); @@ -196,7 +196,7 @@ internal sealed class ArchitectureLifecycle( foreach (var system in systems) { logger.Debug($"Initializing system: {system.GetType().Name}"); - await InitializeComponentAsync(system, asyncMode); + await InitializeComponentAsync(system, asyncMode).ConfigureAwait(false); } logger.Info("All systems initialized"); @@ -218,7 +218,7 @@ internal sealed class ArchitectureLifecycle( private static async Task InitializeComponentAsync(IInitializable component, bool asyncMode) { if (asyncMode && component is IAsyncInitializable asyncInit) - await asyncInit.InitializeAsync(); + await asyncInit.InitializeAsync().ConfigureAwait(false); else component.Initialize(); } @@ -244,7 +244,7 @@ internal sealed class ArchitectureLifecycle( /// public async ValueTask DestroyAsync() { - await _disposer.DestroyAsync(CurrentPhase, EnterPhase); + await _disposer.DestroyAsync(CurrentPhase, EnterPhase).ConfigureAwait(false); } /// @@ -285,4 +285,4 @@ internal sealed class ArchitectureLifecycle( public Task WaitUntilReadyAsync() => _readyTcs.Task; #endregion -} \ No newline at end of file +} diff --git a/GFramework.Core/Command/AbstractAsyncCommand.cs b/GFramework.Core/Command/AbstractAsyncCommand.cs index 779f3314..e0d2ca1a 100644 --- a/GFramework.Core/Command/AbstractAsyncCommand.cs +++ b/GFramework.Core/Command/AbstractAsyncCommand.cs @@ -16,7 +16,7 @@ public abstract class AbstractAsyncCommand : ContextAwareBase, IAsyncCommand /// 表示异步操作的任务 async Task IAsyncCommand.ExecuteAsync() { - await OnExecuteAsync(); + await OnExecuteAsync().ConfigureAwait(false); } /// @@ -25,4 +25,4 @@ public abstract class AbstractAsyncCommand : ContextAwareBase, IAsyncCommand /// /// 表示异步操作的任务 protected abstract Task OnExecuteAsync(); -} \ No newline at end of file +} diff --git a/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs b/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs index d97ab90b..00fcaaba 100644 --- a/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs +++ b/GFramework.Core/Command/AbstractAsyncCommandWithInput.cs @@ -17,7 +17,7 @@ public abstract class AbstractAsyncCommand(TInput input) : ContextAwareB /// 表示异步操作的任务 async Task IAsyncCommand.ExecuteAsync() { - await OnExecuteAsync(input); + await OnExecuteAsync(input).ConfigureAwait(false); } /// diff --git a/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs b/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs index ae19458e..7d1f6a91 100644 --- a/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs +++ b/GFramework.Core/Command/AbstractAsyncCommandWithResult.cs @@ -18,7 +18,7 @@ public abstract class AbstractAsyncCommand(TInput input) : Cont /// 表示异步操作且包含结果的任务 async Task IAsyncCommand.ExecuteAsync() { - return await OnExecuteAsync(input); + return await OnExecuteAsync(input).ConfigureAwait(false); } /// diff --git a/GFramework.Core/Concurrency/AsyncKeyLockManager.cs b/GFramework.Core/Concurrency/AsyncKeyLockManager.cs index 9ad2384e..f4c70595 100644 --- a/GFramework.Core/Concurrency/AsyncKeyLockManager.cs +++ b/GFramework.Core/Concurrency/AsyncKeyLockManager.cs @@ -22,7 +22,7 @@ namespace GFramework.Core.Concurrency; public sealed class AsyncKeyLockManager : IAsyncKeyLockManager { private readonly Timer _cleanupTimer; - private readonly ConcurrentDictionary _locks = new(); + private readonly ConcurrentDictionary _locks = new(StringComparer.Ordinal); private readonly long _lockTimeoutMs; private volatile bool _disposed; @@ -119,7 +119,8 @@ public sealed class AsyncKeyLockManager : IAsyncKeyLockManager LastAccessTicks = kvp.Value.LastAccessTicks, // CurrentCount == 0 表示锁被持有,可能有等待者(近似值) WaitingCount = kvp.Value.Semaphore.CurrentCount == 0 ? 1 : 0 - }); + }, + StringComparer.Ordinal); } /// @@ -187,4 +188,4 @@ public sealed class AsyncKeyLockManager : IAsyncKeyLockManager Semaphore.Dispose(); } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Configuration/ConfigurationManager.cs b/GFramework.Core/Configuration/ConfigurationManager.cs index 2340dcce..5eddcabb 100644 --- a/GFramework.Core/Configuration/ConfigurationManager.cs +++ b/GFramework.Core/Configuration/ConfigurationManager.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Globalization; using System.IO; using System.Text.Json; using GFramework.Core.Abstractions.Configuration; @@ -32,7 +33,7 @@ public class ConfigurationManager : IConfigurationManager /// /// 配置存储字典(线程安全) /// - private readonly ConcurrentDictionary _configs = new(); + private readonly ConcurrentDictionary _configs = new(StringComparer.Ordinal); private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(ConfigurationManager)); @@ -45,7 +46,7 @@ public class ConfigurationManager : IConfigurationManager /// 配置监听器字典(线程安全) /// 键:配置键,值:监听器列表 /// - private readonly ConcurrentDictionary> _watchers = new(); + private readonly ConcurrentDictionary> _watchers = new(StringComparer.Ordinal); /// /// 获取配置数量 @@ -200,7 +201,7 @@ public class ConfigurationManager : IConfigurationManager /// public string SaveToJson() { - var dict = _configs.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + var dict = _configs.ToDictionary(kvp => kvp.Key, kvp => kvp.Value, StringComparer.Ordinal); return JsonSerializer.Serialize(dict, new JsonSerializerOptions { WriteIndented = true @@ -321,11 +322,11 @@ public class ConfigurationManager : IConfigurationManager // 尝试类型转换 try { - return (T)Convert.ChangeType(value, typeof(T)); + return (T)Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture); } catch { return default!; } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Coroutine/CoroutineHandle.cs b/GFramework.Core/Coroutine/CoroutineHandle.cs index e17c1dda..c595c852 100644 --- a/GFramework.Core/Coroutine/CoroutineHandle.cs +++ b/GFramework.Core/Coroutine/CoroutineHandle.cs @@ -85,6 +85,15 @@ public readonly struct CoroutineHandle : IEquatable return _id; } + /// + /// 返回协程句柄的稳定字符串表示,用于日志和诊断输出。 + /// + /// 包含内部标识符与键值的诊断字符串。 + public override string ToString() + { + return $"CoroutineHandle(Id={_id}, Key={Key})"; + } + /// /// 比较两个协程句柄是否相等 /// @@ -106,4 +115,4 @@ public readonly struct CoroutineHandle : IEquatable { return a._id != b._id; } -} \ No newline at end of file +} diff --git a/GFramework.Core/Coroutine/CoroutineScheduler.cs b/GFramework.Core/Coroutine/CoroutineScheduler.cs index 0a24ce6d..21f326ad 100644 --- a/GFramework.Core/Coroutine/CoroutineScheduler.cs +++ b/GFramework.Core/Coroutine/CoroutineScheduler.cs @@ -742,7 +742,7 @@ public sealed class CoroutineScheduler( var handler = OnCoroutineException; if (handler != null) { - Task.Run(() => + _ = Task.Run(() => { try { @@ -755,7 +755,7 @@ public sealed class CoroutineScheduler( }); } - _logger.Error($"[CoroutineScheduler] Coroutine {handle} failed with exception: {ex}"); + _logger.Error($"[CoroutineScheduler] Coroutine {handle.ToString()} failed with exception: {ex}"); FinalizeCoroutine(slotIndex, CoroutineCompletionStatus.Faulted, ex); } @@ -1003,4 +1003,4 @@ public sealed class CoroutineScheduler( } #endregion -} \ No newline at end of file +} diff --git a/GFramework.Core/Coroutine/CoroutineStatistics.cs b/GFramework.Core/Coroutine/CoroutineStatistics.cs index 56321195..69463d72 100644 --- a/GFramework.Core/Coroutine/CoroutineStatistics.cs +++ b/GFramework.Core/Coroutine/CoroutineStatistics.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Globalization; using GFramework.Core.Abstractions.Coroutine; namespace GFramework.Core.Coroutine; @@ -10,7 +11,7 @@ namespace GFramework.Core.Coroutine; internal sealed class CoroutineStatistics : ICoroutineStatistics { private readonly Dictionary _countByPriority = new(); - private readonly Dictionary _countByTag = new(); + private readonly Dictionary _countByTag = new(StringComparer.Ordinal); private readonly object _lock = new(); private int _activeCount; private double _maxExecutionTimeMs; @@ -109,29 +110,31 @@ internal sealed class CoroutineStatistics : ICoroutineStatistics public string GenerateReport() { var sb = new StringBuilder(); - sb.AppendLine("=== 协程统计报告 ==="); - sb.AppendLine($"总启动数: {TotalStarted}"); - sb.AppendLine($"总完成数: {TotalCompleted}"); - sb.AppendLine($"总失败数: {TotalFailed}"); - sb.AppendLine($"当前活跃: {ActiveCount}"); - sb.AppendLine($"当前暂停: {PausedCount}"); - sb.AppendLine($"平均执行时间: {AverageExecutionTimeMs:F2} ms"); - sb.AppendLine($"最大执行时间: {MaxExecutionTimeMs:F2} ms"); + sb.AppendLine(FormattableString.Invariant($"=== 协程统计报告 ===")); + sb.AppendLine(FormattableString.Invariant($"总启动数: {TotalStarted}")); + sb.AppendLine(FormattableString.Invariant($"总完成数: {TotalCompleted}")); + sb.AppendLine(FormattableString.Invariant($"总失败数: {TotalFailed}")); + sb.AppendLine(FormattableString.Invariant($"当前活跃: {ActiveCount}")); + sb.AppendLine(FormattableString.Invariant($"当前暂停: {PausedCount}")); + sb.AppendLine(FormattableString.Invariant( + $"平均执行时间: {AverageExecutionTimeMs.ToString("F2", CultureInfo.InvariantCulture)} ms")); + sb.AppendLine(FormattableString.Invariant( + $"最大执行时间: {MaxExecutionTimeMs.ToString("F2", CultureInfo.InvariantCulture)} ms")); lock (_lock) { if (_countByPriority.Count > 0) { - sb.AppendLine("\n按优先级统计:"); + sb.AppendLine(FormattableString.Invariant($"\n按优先级统计:")); foreach (var kvp in _countByPriority.OrderByDescending(x => x.Key)) - sb.AppendLine($" {kvp.Key}: {kvp.Value}"); + sb.AppendLine(FormattableString.Invariant($" {kvp.Key}: {kvp.Value}")); } if (_countByTag.Count > 0) { - sb.AppendLine("\n按标签统计:"); + sb.AppendLine(FormattableString.Invariant($"\n按标签统计:")); foreach (var kvp in _countByTag.OrderByDescending(x => x.Value)) - sb.AppendLine($" {kvp.Key}: {kvp.Value}"); + sb.AppendLine(FormattableString.Invariant($" {kvp.Key}: {kvp.Value}")); } } @@ -212,4 +215,4 @@ internal sealed class CoroutineStatistics : ICoroutineStatistics } } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Coroutine/Instructions/WaitForTask.T.cs b/GFramework.Core/Coroutine/Instructions/WaitForTask.T.cs index 2a613859..1e95b77b 100644 --- a/GFramework.Core/Coroutine/Instructions/WaitForTask.T.cs +++ b/GFramework.Core/Coroutine/Instructions/WaitForTask.T.cs @@ -24,7 +24,7 @@ public sealed class WaitForTask : IYieldInstruction _done = true; else // 注册完成回调 - _task.ContinueWith(_ => { _done = true; }, TaskContinuationOptions.ExecuteSynchronously); + _ = _task.ContinueWith(_ => { _done = true; }, TaskContinuationOptions.ExecuteSynchronously); } /// @@ -50,4 +50,4 @@ public sealed class WaitForTask : IYieldInstruction /// 获取等待是否已完成 /// public bool IsDone => _done; -} \ No newline at end of file +} diff --git a/GFramework.Core/Coroutine/Instructions/WaitForTask.cs b/GFramework.Core/Coroutine/Instructions/WaitForTask.cs index efc505fb..f8e09cf1 100644 --- a/GFramework.Core/Coroutine/Instructions/WaitForTask.cs +++ b/GFramework.Core/Coroutine/Instructions/WaitForTask.cs @@ -26,7 +26,7 @@ public sealed class WaitForTask : IYieldInstruction _done = true; else // 注册完成回调 - _task.ContinueWith(_ => { _done = true; }, TaskContinuationOptions.ExecuteSynchronously); + _ = _task.ContinueWith(_ => { _done = true; }, TaskContinuationOptions.ExecuteSynchronously); } /// @@ -47,4 +47,4 @@ public sealed class WaitForTask : IYieldInstruction /// 获取等待是否已完成 /// public bool IsDone => _done; -} \ No newline at end of file +} diff --git a/GFramework.Core/Environment/EnvironmentBase.cs b/GFramework.Core/Environment/EnvironmentBase.cs index 5c22dfd4..7895170e 100644 --- a/GFramework.Core/Environment/EnvironmentBase.cs +++ b/GFramework.Core/Environment/EnvironmentBase.cs @@ -11,7 +11,7 @@ public abstract class EnvironmentBase : ContextAwareBase, IEnvironment /// /// 存储环境值的字典,键为字符串,值为对象类型 /// - protected readonly Dictionary Values = new(); + protected readonly IDictionary Values = new Dictionary(StringComparer.Ordinal); /// /// 获取环境名称的抽象属性 @@ -84,4 +84,4 @@ public abstract class EnvironmentBase : ContextAwareBase, IEnvironment // 将键值对添加到Values字典中 Values[key] = value; } -} \ No newline at end of file +} diff --git a/GFramework.Core/Events/EventStatistics.cs b/GFramework.Core/Events/EventStatistics.cs index ff79c392..22ae8b2f 100644 --- a/GFramework.Core/Events/EventStatistics.cs +++ b/GFramework.Core/Events/EventStatistics.cs @@ -9,9 +9,9 @@ namespace GFramework.Core.Events; /// public sealed class EventStatistics : IEventStatistics { - private readonly Dictionary _listenerCountByType = new(); + private readonly Dictionary _listenerCountByType = new(StringComparer.Ordinal); private readonly object _lock = new(); - private readonly Dictionary _publishCountByType = new(); + private readonly Dictionary _publishCountByType = new(StringComparer.Ordinal); private long _totalFailed; private long _totalHandled; private long _totalPublished; @@ -85,27 +85,27 @@ public sealed class EventStatistics : IEventStatistics public string GenerateReport() { var sb = new StringBuilder(); - sb.AppendLine("=== 事件统计报告 ==="); - sb.AppendLine($"总发布数: {TotalPublished}"); - sb.AppendLine($"总处理数: {TotalHandled}"); - sb.AppendLine($"总失败数: {TotalFailed}"); - sb.AppendLine($"活跃事件类型: {ActiveEventTypes}"); - sb.AppendLine($"活跃监听器: {ActiveListeners}"); + sb.AppendLine(FormattableString.Invariant($"=== 事件统计报告 ===")); + sb.AppendLine(FormattableString.Invariant($"总发布数: {TotalPublished}")); + sb.AppendLine(FormattableString.Invariant($"总处理数: {TotalHandled}")); + sb.AppendLine(FormattableString.Invariant($"总失败数: {TotalFailed}")); + sb.AppendLine(FormattableString.Invariant($"活跃事件类型: {ActiveEventTypes}")); + sb.AppendLine(FormattableString.Invariant($"活跃监听器: {ActiveListeners}")); lock (_lock) { if (_publishCountByType.Count > 0) { - sb.AppendLine("\n按事件类型统计(发布次数):"); + sb.AppendLine(FormattableString.Invariant($"\n按事件类型统计(发布次数):")); foreach (var kvp in _publishCountByType.OrderByDescending(x => x.Value)) - sb.AppendLine($" {kvp.Key}: {kvp.Value}"); + sb.AppendLine(FormattableString.Invariant($" {kvp.Key}: {kvp.Value}")); } if (_listenerCountByType.Count > 0) { - sb.AppendLine("\n按事件类型统计(监听器数量):"); + sb.AppendLine(FormattableString.Invariant($"\n按事件类型统计(监听器数量):")); foreach (var kvp in _listenerCountByType.OrderByDescending(x => x.Value)) - sb.AppendLine($" {kvp.Key}: {kvp.Value}"); + sb.AppendLine(FormattableString.Invariant($" {kvp.Key}: {kvp.Value}")); } } @@ -162,4 +162,4 @@ public sealed class EventStatistics : IEventStatistics } } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Extensions/AsyncExtensions.cs b/GFramework.Core/Extensions/AsyncExtensions.cs index db2a7ec0..47124086 100644 --- a/GFramework.Core/Extensions/AsyncExtensions.cs +++ b/GFramework.Core/Extensions/AsyncExtensions.cs @@ -118,11 +118,11 @@ public static class AsyncExtensions try { - return await task; + return await task.ConfigureAwait(false); } catch (Exception ex) { return fallback(ex); } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Extensions/ContextAwareCommandExtensions.cs b/GFramework.Core/Extensions/ContextAwareCommandExtensions.cs index 876c9776..49676e56 100644 --- a/GFramework.Core/Extensions/ContextAwareCommandExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareCommandExtensions.cs @@ -54,7 +54,7 @@ public static class ContextAwareCommandExtensions ArgumentNullException.ThrowIfNull(command); var context = contextAware.GetContext(); - await context.SendCommandAsync(command); + await context.SendCommandAsync(command).ConfigureAwait(false); } /// @@ -72,6 +72,6 @@ public static class ContextAwareCommandExtensions ArgumentNullException.ThrowIfNull(command); var context = contextAware.GetContext(); - return await context.SendCommandAsync(command); + return await context.SendCommandAsync(command).ConfigureAwait(false); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Extensions/ContextAwareQueryExtensions.cs b/GFramework.Core/Extensions/ContextAwareQueryExtensions.cs index ce12e22f..63ad260c 100644 --- a/GFramework.Core/Extensions/ContextAwareQueryExtensions.cs +++ b/GFramework.Core/Extensions/ContextAwareQueryExtensions.cs @@ -41,6 +41,6 @@ public static class ContextAwareQueryExtensions ArgumentNullException.ThrowIfNull(query); var context = contextAware.GetContext(); - return await context.SendQueryAsync(query); + return await context.SendQueryAsync(query).ConfigureAwait(false); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Extensions/NumericExtensions.cs b/GFramework.Core/Extensions/NumericExtensions.cs index 448975b4..eaa079e8 100644 --- a/GFramework.Core/Extensions/NumericExtensions.cs +++ b/GFramework.Core/Extensions/NumericExtensions.cs @@ -26,7 +26,7 @@ public static class NumericExtensions public static bool Between(this T value, T min, T max, bool inclusive = true) where T : IComparable { if (min.CompareTo(max) > 0) - throw new ArgumentException($"最小值 ({min}) 不能大于最大值 ({max})"); + throw new ArgumentException($"最小值 ({min}) 不能大于最大值 ({max})", nameof(min)); if (inclusive) return value.CompareTo(min) >= 0 && value.CompareTo(max) <= 0; @@ -71,4 +71,4 @@ public static class NumericExtensions return (value - from) / (to - from); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Functional/Async/AsyncFunctionalExtensions.cs b/GFramework.Core/Functional/Async/AsyncFunctionalExtensions.cs index 5ce9dbd5..cc20ff34 100644 --- a/GFramework.Core/Functional/Async/AsyncFunctionalExtensions.cs +++ b/GFramework.Core/Functional/Async/AsyncFunctionalExtensions.cs @@ -54,14 +54,14 @@ public static class AsyncFunctionalExtensions { try { - return await taskFactory(); + return await taskFactory().ConfigureAwait(false); } catch (Exception ex) { // 若还有重试机会且允许重试,则等待后继续;否则统一包装为 AggregateException 抛出 if (attempt < maxRetries && shouldRetry(ex)) { - await Task.Delay(delay); + await Task.Delay(delay).ConfigureAwait(false); } else { @@ -99,7 +99,7 @@ public static class AsyncFunctionalExtensions try { - var result = await func(); + var result = await func().ConfigureAwait(false); return new Result(result); } catch (Exception ex) @@ -107,4 +107,4 @@ public static class AsyncFunctionalExtensions return new Result(ex); } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Functional/Result.T.cs b/GFramework.Core/Functional/Result.T.cs index ad347cab..06e381c0 100644 --- a/GFramework.Core/Functional/Result.T.cs +++ b/GFramework.Core/Functional/Result.T.cs @@ -201,7 +201,7 @@ public readonly struct Result : IEquatable>, IComparable> try { - return new Result(await f(_value!)); + return new Result(await f(_value!).ConfigureAwait(false)); } catch (Exception ex) { @@ -307,7 +307,7 @@ public readonly struct Result : IEquatable>, IComparable> if (IsSuccess) return EqualityComparer.Default.Equals(_value, other._value); if (IsFaulted) return Exception.GetType() == other.Exception.GetType() - && Exception.Message == other.Exception.Message; + && string.Equals(Exception.Message, other.Exception.Message, StringComparison.Ordinal); return true; // both Bottom } @@ -418,4 +418,4 @@ public readonly struct Result : IEquatable>, IComparable> ResultState.Faulted => $"Fail({Exception.Message})", _ => "(Bottom)" }; -} \ No newline at end of file +} diff --git a/GFramework.Core/Functional/Result.cs b/GFramework.Core/Functional/Result.cs index 994df5f2..7701eda8 100644 --- a/GFramework.Core/Functional/Result.cs +++ b/GFramework.Core/Functional/Result.cs @@ -126,7 +126,7 @@ public readonly struct Result : IEquatable return true; return _exception!.GetType() == other._exception!.GetType() && - _exception.Message == other._exception.Message; + string.Equals(_exception.Message, other._exception.Message, StringComparison.Ordinal); } /// @@ -216,4 +216,4 @@ public readonly struct Result : IEquatable ArgumentNullException.ThrowIfNull(func); return IsSuccess ? func() : Result.Failure(_exception!); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Functional/ResultExtensions.cs b/GFramework.Core/Functional/ResultExtensions.cs index a5cab9cc..6be7870d 100644 --- a/GFramework.Core/Functional/ResultExtensions.cs +++ b/GFramework.Core/Functional/ResultExtensions.cs @@ -111,6 +111,7 @@ public static class ResultExtensions ArgumentNullException.ThrowIfNull(binder); return result.IsSuccess ? await binder(result.Match(succ: v => v, fail: _ => throw new InvalidOperationException())) + .ConfigureAwait(false) : Result.Fail(result.Exception); } @@ -176,7 +177,7 @@ public static class ResultExtensions return result.Match( succ: value => predicate(value) ? result - : Result.Fail(new ArgumentException(errorMessage)), + : Result.Fail(new ArgumentException(errorMessage, nameof(errorMessage))), fail: _ => result ); } @@ -233,7 +234,7 @@ public static class ResultExtensions ArgumentNullException.ThrowIfNull(func); try { - return Result.Succeed(await func()); + return Result.Succeed(await func().ConfigureAwait(false)); } catch (Exception ex) { @@ -261,7 +262,7 @@ public static class ResultExtensions string errorMessage = "Value is null") where T : class => value is not null ? Result.Succeed(value) - : Result.Fail(new ArgumentNullException(errorMessage)); + : Result.Fail(new ArgumentNullException(nameof(value), errorMessage)); /// /// 将可空值类型转换为 Result @@ -271,7 +272,7 @@ public static class ResultExtensions string errorMessage = "Value is null") where T : struct => value.HasValue ? Result.Succeed(value.Value) - : Result.Fail(new ArgumentNullException(errorMessage)); + : Result.Fail(new ArgumentNullException(nameof(value), errorMessage)); #endregion -} \ No newline at end of file +} diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 9da7b6bc..4dd7034b 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -392,7 +392,10 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) var assemblyArray = assemblies.ToArray(); foreach (var assembly in assemblyArray) { - ArgumentNullException.ThrowIfNull(assembly); + if (assembly is null) + { + throw new ArgumentException("Assemblies collection cannot contain null items.", nameof(assemblies)); + } } _lock.EnterWriteLock(); diff --git a/GFramework.Core/Localization/Formatters/CompactNumberLocalizationFormatter.cs b/GFramework.Core/Localization/Formatters/CompactNumberLocalizationFormatter.cs index b37d892a..e91ee978 100644 --- a/GFramework.Core/Localization/Formatters/CompactNumberLocalizationFormatter.cs +++ b/GFramework.Core/Localization/Formatters/CompactNumberLocalizationFormatter.cs @@ -1,3 +1,4 @@ +using System.Globalization; using GFramework.Core.Abstractions.Localization; using GFramework.Core.Abstractions.Utility.Numeric; using GFramework.Core.Utility.Numeric; @@ -155,13 +156,14 @@ public sealed class CompactNumberLocalizationFormatter : ILocalizationFormatter ref bool trimTrailingZeros, ref bool useGroupingBelowThreshold) { + var formatProvider = CultureInfo.InvariantCulture; return key switch { - "maxDecimals" => int.TryParse(value, out maxDecimalPlaces), - "minDecimals" => int.TryParse(value, out minDecimalPlaces), + "maxDecimals" => int.TryParse(value, NumberStyles.Integer, formatProvider, out maxDecimalPlaces), + "minDecimals" => int.TryParse(value, NumberStyles.Integer, formatProvider, out minDecimalPlaces), "trimZeros" => bool.TryParse(value, out trimTrailingZeros), "grouping" => bool.TryParse(value, out useGroupingBelowThreshold), _ => true }; } -} \ No newline at end of file +} diff --git a/GFramework.Core/Localization/LocalizationManager.cs b/GFramework.Core/Localization/LocalizationManager.cs index 9c38cf17..184a76b3 100644 --- a/GFramework.Core/Localization/LocalizationManager.cs +++ b/GFramework.Core/Localization/LocalizationManager.cs @@ -27,8 +27,8 @@ public class LocalizationManager : AbstractSystem, ILocalizationManager public LocalizationManager(LocalizationConfig? config = null) { _config = config ?? new LocalizationConfig(); - _tables = new Dictionary>(); - _formatters = new Dictionary(); + _tables = new Dictionary>(StringComparer.Ordinal); + _formatters = new Dictionary(StringComparer.Ordinal); _languageChangeCallbacks = new List>(); _currentLanguage = _config.DefaultLanguage; _currentCulture = GetCultureInfo(_currentLanguage); @@ -53,7 +53,7 @@ public class LocalizationManager : AbstractSystem, ILocalizationManager throw new ArgumentNullException(nameof(languageCode)); } - if (_currentLanguage == languageCode) + if (string.Equals(_currentLanguage, languageCode, StringComparison.Ordinal)) { return; } @@ -227,11 +227,11 @@ public class LocalizationManager : AbstractSystem, ILocalizationManager return; // 已加载 } - var languageTables = new Dictionary(); + var languageTables = new Dictionary(StringComparer.Ordinal); // 加载回退语言(如果不是默认语言) Dictionary? fallbackTables = null; - if (languageCode != _config.FallbackLanguage) + if (!string.Equals(languageCode, _config.FallbackLanguage, StringComparison.Ordinal)) { LoadLanguage(_config.FallbackLanguage); _tables.TryGetValue(_config.FallbackLanguage, out fallbackTables); @@ -264,7 +264,7 @@ public class LocalizationManager : AbstractSystem, ILocalizationManager { var json = File.ReadAllText(filePath); var data = JsonSerializer.Deserialize>(json); - return data ?? new Dictionary(); + return data ?? new Dictionary(StringComparer.Ordinal); } /// @@ -314,4 +314,4 @@ public class LocalizationManager : AbstractSystem, ILocalizationManager } } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Localization/LocalizationString.cs b/GFramework.Core/Localization/LocalizationString.cs index 15832ab0..c925cf9f 100644 --- a/GFramework.Core/Localization/LocalizationString.cs +++ b/GFramework.Core/Localization/LocalizationString.cs @@ -18,7 +18,10 @@ public class LocalizationString : ILocalizationString /// 预编译的静态正则表达式,用于格式化字符串中的变量替换 /// private static readonly Regex FormatVariableRegex = - new(FormatVariablePattern, RegexOptions.Compiled | RegexOptions.CultureInvariant); + new( + FormatVariablePattern, + RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture, + TimeSpan.FromSeconds(1)); private readonly ILocalizationManager _manager; private readonly Dictionary _variables; @@ -35,7 +38,7 @@ public class LocalizationString : ILocalizationString _manager = manager ?? throw new ArgumentNullException(nameof(manager)); Table = table ?? throw new ArgumentNullException(nameof(table)); Key = key ?? throw new ArgumentNullException(nameof(key)); - _variables = new Dictionary(); + _variables = new Dictionary(StringComparer.Ordinal); } /// @@ -234,4 +237,4 @@ public class LocalizationString : ILocalizationString { return manager.GetFormatter(name); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Localization/LocalizationTable.cs b/GFramework.Core/Localization/LocalizationTable.cs index 51090892..58528775 100644 --- a/GFramework.Core/Localization/LocalizationTable.cs +++ b/GFramework.Core/Localization/LocalizationTable.cs @@ -32,8 +32,8 @@ public class LocalizationTable : ILocalizationTable { Name = name ?? throw new ArgumentNullException(nameof(name)); Language = language ?? throw new ArgumentNullException(nameof(language)); - _data = new Dictionary(data); - _overrides = new Dictionary(); + _data = new Dictionary(data, StringComparer.Ordinal); + _overrides = new Dictionary(StringComparer.Ordinal); Fallback = fallback; } @@ -105,7 +105,7 @@ public class LocalizationTable : ILocalizationTable /// 包含所有键的可枚举集合 public IEnumerable GetKeys() { - var keys = new HashSet(_data.Keys); + var keys = new HashSet(_data.Keys, StringComparer.Ordinal); keys.UnionWith(_overrides.Keys); if (Fallback != null) @@ -133,4 +133,4 @@ public class LocalizationTable : ILocalizationTable _overrides[key] = value; } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/AppenderConfiguration.cs b/GFramework.Core/Logging/AppenderConfiguration.cs new file mode 100644 index 00000000..5922ee8b --- /dev/null +++ b/GFramework.Core/Logging/AppenderConfiguration.cs @@ -0,0 +1,54 @@ +using GFramework.Core.Abstractions.Logging; + +namespace GFramework.Core.Logging; + +/// +/// Appender 配置。 +/// +public sealed class AppenderConfiguration +{ + /// + /// Appender 类型(Console, File, RollingFile, Async)。 + /// + public string Type { get; set; } = string.Empty; + + /// + /// 格式化器类型(Default, Json)。 + /// + public string Formatter { get; set; } = "Default"; + + /// + /// 文件路径(仅用于 File 和 RollingFile)。 + /// + public string? FilePath { get; set; } + + /// + /// 是否使用颜色(仅用于 Console)。 + /// + public bool UseColors { get; set; } = true; + + /// + /// 缓冲区大小(仅用于 Async)。 + /// + public int BufferSize { get; set; } = 10000; + + /// + /// 最大文件大小(仅用于 RollingFile,字节)。 + /// + public long MaxFileSize { get; set; } = 10 * 1024 * 1024; + + /// + /// 最大文件数量(仅用于 RollingFile)。 + /// + public int MaxFileCount { get; set; } = 5; + + /// + /// 过滤器配置。 + /// + public FilterConfiguration? Filter { get; set; } + + /// + /// 内部 Appender 配置(仅用于 Async)。 + /// + public AppenderConfiguration? InnerAppender { get; set; } +} diff --git a/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs b/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs index af70014f..0a906f46 100644 --- a/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs +++ b/GFramework.Core/Logging/Appenders/AsyncLogAppender.cs @@ -162,7 +162,7 @@ public sealed class AsyncLogAppender : ILogAppender { try { - await foreach (var entry in _channel.Reader.ReadAllAsync(cancellationToken)) + await foreach (var entry in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { try { @@ -235,4 +235,4 @@ public sealed class AsyncLogAppender : ILogAppender // 错误观察者只用于诊断,绝不能反向影响日志处理线程的生命周期。 } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/Appenders/StatisticsAppender.cs b/GFramework.Core/Logging/Appenders/StatisticsAppender.cs index ec2cd008..48fb3f87 100644 --- a/GFramework.Core/Logging/Appenders/StatisticsAppender.cs +++ b/GFramework.Core/Logging/Appenders/StatisticsAppender.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using System.Text; +using System.Globalization; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Abstractions.Time; using GFramework.Core.Time; @@ -13,7 +14,7 @@ namespace GFramework.Core.Logging.Appenders; public sealed class StatisticsAppender : ILogAppender { private readonly ConcurrentDictionary _levelCounts = new(); - private readonly ConcurrentDictionary _loggerCounts = new(); + private readonly ConcurrentDictionary _loggerCounts = new(StringComparer.Ordinal); private readonly ITimeProvider _timeProvider; private long _errorCount; private long _startTimeTicks; @@ -127,7 +128,7 @@ public sealed class StatisticsAppender : ILogAppender /// public IReadOnlyDictionary GetLoggerCounts() { - return new Dictionary(_loggerCounts); + return new Dictionary(_loggerCounts, StringComparer.Ordinal); } /// @@ -151,27 +152,28 @@ public sealed class StatisticsAppender : ILogAppender var startTime = StartTime; var now = _timeProvider.UtcNow; - report.AppendLine("=== 日志统计报告 ==="); - report.AppendLine($"统计时间: {startTime:yyyy-MM-dd HH:mm:ss} - {now:yyyy-MM-dd HH:mm:ss}"); - report.AppendLine($"运行时长: {Uptime}"); - report.AppendLine($"总日志数: {TotalCount}"); - report.AppendLine($"错误日志数: {ErrorCount}"); - report.AppendLine($"错误率: {ErrorRate:P2}"); + report.AppendLine(FormattableString.Invariant($"=== 日志统计报告 ===")); + report.AppendLine(FormattableString.Invariant( + $"统计时间: {startTime.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)} - {now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)}")); + report.AppendLine(FormattableString.Invariant($"运行时长: {Uptime}")); + report.AppendLine(FormattableString.Invariant($"总日志数: {TotalCount}")); + report.AppendLine(FormattableString.Invariant($"错误日志数: {ErrorCount}")); + report.AppendLine(FormattableString.Invariant($"错误率: {ErrorRate:P2}")); report.AppendLine(); - report.AppendLine("按级别统计:"); + report.AppendLine(FormattableString.Invariant($"按级别统计:")); foreach (var level in Enum.GetValues()) { var count = GetCountByLevel(level); if (count > 0) { var percentage = (double)count / TotalCount; - report.AppendLine($" {level}: {count} ({percentage:P2})"); + report.AppendLine(FormattableString.Invariant($" {level}: {count} ({percentage:P2})")); } } report.AppendLine(); - report.AppendLine("按日志记录器统计 (Top 10):"); + report.AppendLine(FormattableString.Invariant($"按日志记录器统计 (Top 10):")); var topLoggers = _loggerCounts .OrderByDescending(kvp => kvp.Value) .Take(10); @@ -179,9 +181,9 @@ public sealed class StatisticsAppender : ILogAppender foreach (var (logger, count) in topLoggers) { var percentage = (double)count / TotalCount; - report.AppendLine($" {logger}: {count} ({percentage:P2})"); + report.AppendLine(FormattableString.Invariant($" {logger}: {count} ({percentage:P2})")); } return report.ToString(); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/CachedLoggerFactory.cs b/GFramework.Core/Logging/CachedLoggerFactory.cs index 220fc2ad..edea5745 100644 --- a/GFramework.Core/Logging/CachedLoggerFactory.cs +++ b/GFramework.Core/Logging/CachedLoggerFactory.cs @@ -8,7 +8,7 @@ namespace GFramework.Core.Logging; /// public sealed class CachedLoggerFactory : ILoggerFactory { - private readonly ConcurrentDictionary _cache = new(); + private readonly ConcurrentDictionary _cache = new(StringComparer.Ordinal); private readonly ILoggerFactory _innerFactory; /// @@ -31,4 +31,4 @@ public sealed class CachedLoggerFactory : ILoggerFactory var cacheKey = $"{name}:{minLevel}"; return _cache.GetOrAdd(cacheKey, _ => _innerFactory.GetLogger(name, minLevel)); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/CompositeLogger.cs b/GFramework.Core/Logging/CompositeLogger.cs index fe5dda40..c551f91e 100644 --- a/GFramework.Core/Logging/CompositeLogger.cs +++ b/GFramework.Core/Logging/CompositeLogger.cs @@ -74,7 +74,7 @@ public sealed class CompositeLogger : AbstractLogger, IDisposable if (!IsEnabled(level)) return; var propsDict = properties.Length > 0 - ? properties.ToDictionary(p => p.Key, p => p.Value) + ? properties.ToDictionary(p => p.Key, p => p.Value, StringComparer.Ordinal) : null; var entry = new LogEntry( @@ -104,7 +104,7 @@ public sealed class CompositeLogger : AbstractLogger, IDisposable if (!IsEnabled(level)) return; var propsDict = properties.Length > 0 - ? properties.ToDictionary(p => p.Key, p => p.Value) + ? properties.ToDictionary(p => p.Key, p => p.Value, StringComparer.Ordinal) : null; var entry = new LogEntry( @@ -131,4 +131,4 @@ public sealed class CompositeLogger : AbstractLogger, IDisposable appender.Flush(); } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/ConfigurableLoggerFactory.cs b/GFramework.Core/Logging/ConfigurableLoggerFactory.cs new file mode 100644 index 00000000..9bea072f --- /dev/null +++ b/GFramework.Core/Logging/ConfigurableLoggerFactory.cs @@ -0,0 +1,78 @@ +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Logging.Appenders; + +namespace GFramework.Core.Logging; + +/// +/// 可配置的 Logger 工厂。 +/// +internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable +{ + private readonly ILogAppender[] _appenders; + private readonly LoggingConfiguration _config; + private bool _disposed; + + /// + /// 初始化一个基于日志配置创建输出管线的工厂实例。 + /// + /// 日志配置。 + public ConfigurableLoggerFactory(LoggingConfiguration config) + { + _config = config ?? throw new ArgumentNullException(nameof(config)); + _appenders = config.Appenders.Select(LoggingConfigurationLoader.CreateAppender).ToArray(); + } + + /// + /// 释放内部 Appender 持有的资源。 + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + foreach (var appender in _appenders) + { + if (appender is IDisposable disposable) + { + disposable.Dispose(); + } + } + + _disposed = true; + } + + /// + /// 为指定名称创建日志记录器,并应用最匹配的命名空间级别配置。 + /// + /// 日志记录器名称。 + /// 调用方传入的默认最小级别。 + /// 可写入日志的记录器实例。 + public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info) + { + var effectiveLevel = _config.MinLevel; + + foreach (var kvp in _config.LoggerLevels) + { + if (string.Equals(name, kvp.Key, StringComparison.Ordinal) || + name.StartsWith(kvp.Key + ".", StringComparison.Ordinal)) + { + effectiveLevel = kvp.Value; + break; + } + } + + if (_appenders.Length == 0) + { + return new ConsoleLogger(name, effectiveLevel); + } + + if (_appenders.Length == 1 && _appenders[0] is ConsoleAppender) + { + return new ConsoleLogger(name, effectiveLevel); + } + + return new CompositeLogger(name, effectiveLevel, _appenders); + } +} diff --git a/GFramework.Core/Logging/ConsoleLogger.cs b/GFramework.Core/Logging/ConsoleLogger.cs index c852d785..e1f0f023 100644 --- a/GFramework.Core/Logging/ConsoleLogger.cs +++ b/GFramework.Core/Logging/ConsoleLogger.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.IO; using GFramework.Core.Abstractions.Logging; @@ -35,7 +36,7 @@ public sealed class ConsoleLogger( /// 异常信息,可为空 protected override void Write(LogLevel level, string message, Exception? exception) { - var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff"); + var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture); var levelStr = LevelStrings[(int)level]; var log = $"[{timestamp}] {levelStr} [{Name()}] {message}"; @@ -81,4 +82,4 @@ public sealed class ConsoleLogger( } #endregion -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/FilterConfiguration.cs b/GFramework.Core/Logging/FilterConfiguration.cs new file mode 100644 index 00000000..7e9d8db8 --- /dev/null +++ b/GFramework.Core/Logging/FilterConfiguration.cs @@ -0,0 +1,29 @@ +using GFramework.Core.Abstractions.Logging; + +namespace GFramework.Core.Logging; + +/// +/// 过滤器配置。 +/// +public sealed class FilterConfiguration +{ + /// + /// 过滤器类型(LogLevel, Namespace, Composite)。 + /// + public string Type { get; set; } = "LogLevel"; + + /// + /// 最小日志级别(用于 LogLevel 过滤器)。 + /// + public LogLevel? MinLevel { get; set; } + + /// + /// 命名空间前缀列表(用于 Namespace 过滤器)。 + /// + public List? Namespaces { get; set; } + + /// + /// 子过滤器列表(用于 Composite 过滤器)。 + /// + public List? Filters { get; set; } +} diff --git a/GFramework.Core/Logging/Filters/SamplingFilter.cs b/GFramework.Core/Logging/Filters/SamplingFilter.cs index 9bdf4a31..0d170ef3 100644 --- a/GFramework.Core/Logging/Filters/SamplingFilter.cs +++ b/GFramework.Core/Logging/Filters/SamplingFilter.cs @@ -14,7 +14,7 @@ public sealed class SamplingFilter : ILogFilter private const int DefaultMaxLoggers = 1000; private readonly int _maxLoggers; private readonly int _sampleRate; - private readonly ConcurrentDictionary _samplingStates = new(); + private readonly ConcurrentDictionary _samplingStates = new(StringComparer.Ordinal); private readonly ITimeProvider _timeProvider; private readonly TimeSpan _timeWindow; @@ -69,7 +69,7 @@ public sealed class SamplingFilter : ILogFilter foreach (var kvp in _samplingStates) { - if (kvp.Key == "*") continue; // 不清理共享状态 + if (string.Equals(kvp.Key, "*", StringComparison.Ordinal)) continue; // 不清理共享状态 if (kvp.Value.IsStale(now, staleThreshold)) { @@ -132,4 +132,4 @@ public sealed class SamplingFilter : ILogFilter return now - lastAccess > staleThreshold; } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/Formatters/DefaultLogFormatter.cs b/GFramework.Core/Logging/Formatters/DefaultLogFormatter.cs index 4c2dcef0..0d723343 100644 --- a/GFramework.Core/Logging/Formatters/DefaultLogFormatter.cs +++ b/GFramework.Core/Logging/Formatters/DefaultLogFormatter.cs @@ -1,5 +1,6 @@ using System; using System.Text; +using System.Globalization; using GFramework.Core.Abstractions.Logging; namespace GFramework.Core.Logging.Formatters; @@ -26,7 +27,7 @@ public sealed class DefaultLogFormatter : ILogFormatter /// 格式化后的日志字符串 public string Format(LogEntry entry) { - var timestamp = entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff"); + var timestamp = entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture); var levelStr = LevelStrings[(int)entry.Level]; var sb = new StringBuilder(); @@ -54,4 +55,4 @@ public sealed class DefaultLogFormatter : ILogFormatter return sb.ToString(); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/Formatters/JsonLogFormatter.cs b/GFramework.Core/Logging/Formatters/JsonLogFormatter.cs index a48ce7e2..61ee1d41 100644 --- a/GFramework.Core/Logging/Formatters/JsonLogFormatter.cs +++ b/GFramework.Core/Logging/Formatters/JsonLogFormatter.cs @@ -1,4 +1,5 @@ using System.Text.Json; +using System.Globalization; using GFramework.Core.Abstractions.Logging; namespace GFramework.Core.Logging.Formatters; @@ -21,9 +22,9 @@ public sealed class JsonLogFormatter : ILogFormatter /// JSON 格式的日志字符串 public string Format(LogEntry entry) { - var logObject = new Dictionary + var logObject = new Dictionary(StringComparer.Ordinal) { - ["timestamp"] = entry.Timestamp.ToString("O"), // ISO 8601 格式 + ["timestamp"] = entry.Timestamp.ToString("O", CultureInfo.InvariantCulture), // ISO 8601 格式 ["level"] = entry.Level.ToString().ToUpperInvariant(), ["logger"] = entry.LoggerName, ["message"] = entry.Message @@ -49,4 +50,4 @@ public sealed class JsonLogFormatter : ILogFormatter return JsonSerializer.Serialize(logObject, JsonOptions); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Logging/LoggingConfiguration.cs b/GFramework.Core/Logging/LoggingConfiguration.cs index 76681a96..c9cec380 100644 --- a/GFramework.Core/Logging/LoggingConfiguration.cs +++ b/GFramework.Core/Logging/LoggingConfiguration.cs @@ -20,82 +20,5 @@ public sealed class LoggingConfiguration /// /// 特定 Logger 的日志级别配置 /// - public Dictionary LoggerLevels { get; set; } = new(); + public Dictionary LoggerLevels { get; set; } = new(StringComparer.Ordinal); } - -/// -/// Appender 配置 -/// -public sealed class AppenderConfiguration -{ - /// - /// Appender 类型(Console, File, RollingFile, Async) - /// - public string Type { get; set; } = string.Empty; - - /// - /// 格式化器类型(Default, Json) - /// - public string Formatter { get; set; } = "Default"; - - /// - /// 文件路径(仅用于 File 和 RollingFile) - /// - public string? FilePath { get; set; } - - /// - /// 是否使用颜色(仅用于 Console) - /// - public bool UseColors { get; set; } = true; - - /// - /// 缓冲区大小(仅用于 Async) - /// - public int BufferSize { get; set; } = 10000; - - /// - /// 最大文件大小(仅用于 RollingFile,字节) - /// - public long MaxFileSize { get; set; } = 10 * 1024 * 1024; // 10MB - - /// - /// 最大文件数量(仅用于 RollingFile) - /// - public int MaxFileCount { get; set; } = 5; - - /// - /// 过滤器配置 - /// - public FilterConfiguration? Filter { get; set; } - - /// - /// 内部 Appender 配置(仅用于 Async) - /// - public AppenderConfiguration? InnerAppender { get; set; } -} - -/// -/// 过滤器配置 -/// -public sealed class FilterConfiguration -{ - /// - /// 过滤器类型(LogLevel, Namespace, Composite) - /// - public string Type { get; set; } = "LogLevel"; - - /// - /// 最小日志级别(用于 LogLevel 过滤器) - /// - public LogLevel? MinLevel { get; set; } - - /// - /// 命名空间前缀列表(用于 Namespace 过滤器) - /// - public List? Namespaces { get; set; } - - /// - /// 子过滤器列表(用于 Composite 过滤器) - /// - public List? Filters { get; set; } -} \ No newline at end of file diff --git a/GFramework.Core/Logging/LoggingConfigurationLoader.cs b/GFramework.Core/Logging/LoggingConfigurationLoader.cs index d0565b67..545787e2 100644 --- a/GFramework.Core/Logging/LoggingConfigurationLoader.cs +++ b/GFramework.Core/Logging/LoggingConfigurationLoader.cs @@ -127,66 +127,3 @@ public static class LoggingConfigurationLoader }; } } - -/// -/// 可配置的 Logger 工厂 -/// -internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable -{ - private readonly ILogAppender[] _appenders; - private readonly LoggingConfiguration _config; - private bool _disposed; - - public ConfigurableLoggerFactory(LoggingConfiguration config) - { - _config = config ?? throw new ArgumentNullException(nameof(config)); - _appenders = config.Appenders.Select(LoggingConfigurationLoader.CreateAppender).ToArray(); - } - - public void Dispose() - { - if (_disposed) - return; - - foreach (var appender in _appenders) - { - if (appender is IDisposable disposable) - { - disposable.Dispose(); - } - } - - _disposed = true; - } - - public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info) - { - // 检查是否有特定 Logger 的级别配置(支持前缀匹配) - var effectiveLevel = _config.MinLevel; - - foreach (var kvp in _config.LoggerLevels) - { - // 精确匹配或前缀匹配(命名空间层级) - if (name == kvp.Key || name.StartsWith(kvp.Key + ".", StringComparison.Ordinal)) - { - effectiveLevel = kvp.Value; - break; - } - } - - // 如果没有 Appender,返回简单的 ConsoleLogger - if (_appenders.Length == 0) - { - return new ConsoleLogger(name, effectiveLevel); - } - - // 如果只有一个 Appender 且是 ConsoleAppender,优化为 ConsoleLogger - if (_appenders.Length == 1 && _appenders[0] is ConsoleAppender) - { - return new ConsoleLogger(name, effectiveLevel); - } - - // 返回 CompositeLogger - return new CompositeLogger(name, effectiveLevel, _appenders); - } -} \ No newline at end of file diff --git a/GFramework.Core/Pool/AbstractObjectPoolSystem.cs b/GFramework.Core/Pool/AbstractObjectPoolSystem.cs index 7afe321c..95c25b3c 100644 --- a/GFramework.Core/Pool/AbstractObjectPoolSystem.cs +++ b/GFramework.Core/Pool/AbstractObjectPoolSystem.cs @@ -15,7 +15,7 @@ public abstract class AbstractObjectPoolSystem /// /// 存储对象池的字典,键为池标识,值为池信息 /// - protected readonly Dictionary Pools = new(); + protected readonly IDictionary Pools = new Dictionary(); /// /// 获取对象池中的对象,如果池中没有可用对象则创建新的对象 @@ -254,4 +254,4 @@ public abstract class AbstractObjectPoolSystem /// public int ActiveCount { get; set; } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Resource/ResourceCache.cs b/GFramework.Core/Resource/ResourceCache.cs index 440e93b0..cffa2908 100644 --- a/GFramework.Core/Resource/ResourceCache.cs +++ b/GFramework.Core/Resource/ResourceCache.cs @@ -3,19 +3,8 @@ using System.Collections.Concurrent; namespace GFramework.Core.Resource; /// -/// 资源缓存条目 -/// -internal sealed class ResourceCacheEntry(object resource, Type resourceType) -{ - public object Resource { get; } = resource ?? throw new ArgumentNullException(nameof(resource)); - public Type ResourceType { get; } = resourceType ?? throw new ArgumentNullException(nameof(resourceType)); - public int ReferenceCount { get; set; } - public DateTime LastAccessTime { get; set; } = DateTime.UtcNow; -} - -/// -/// 资源缓存系统,管理已加载资源的缓存和引用计数 -/// 线程安全:所有公共方法都是线程安全的 +/// 资源缓存系统,管理已加载资源的缓存和引用计数。 +/// 线程安全:所有公共方法都是线程安全的。 /// internal sealed class ResourceCache { @@ -24,7 +13,7 @@ internal sealed class ResourceCache /// private const string PathCannotBeNullOrEmptyMessage = "Path cannot be null or whitespace."; - private readonly ConcurrentDictionary _cache = new(); + private readonly ConcurrentDictionary _cache = new(StringComparer.Ordinal); private readonly object _lock = new(); /// @@ -208,4 +197,4 @@ internal sealed class ResourceCache return unreferencedPaths; } -} \ No newline at end of file +} diff --git a/GFramework.Core/Resource/ResourceCacheEntry.cs b/GFramework.Core/Resource/ResourceCacheEntry.cs new file mode 100644 index 00000000..46536f59 --- /dev/null +++ b/GFramework.Core/Resource/ResourceCacheEntry.cs @@ -0,0 +1,27 @@ +namespace GFramework.Core.Resource; + +/// +/// 资源缓存条目。 +/// +internal sealed class ResourceCacheEntry(object resource, Type resourceType) +{ + /// + /// 获取缓存中的资源实例。 + /// + public object Resource { get; } = resource ?? throw new ArgumentNullException(nameof(resource)); + + /// + /// 获取资源的运行时类型。 + /// + public Type ResourceType { get; } = resourceType ?? throw new ArgumentNullException(nameof(resourceType)); + + /// + /// 获取或设置当前引用计数。 + /// + public int ReferenceCount { get; set; } + + /// + /// 获取或设置最近访问时间。 + /// + public DateTime LastAccessTime { get; set; } = DateTime.UtcNow; +} diff --git a/GFramework.Core/Resource/ResourceManager.cs b/GFramework.Core/Resource/ResourceManager.cs index 11c30a05..ebd30bc7 100644 --- a/GFramework.Core/Resource/ResourceManager.cs +++ b/GFramework.Core/Resource/ResourceManager.cs @@ -104,7 +104,7 @@ public class ResourceManager : IResourceManager try { - var resource = await loader.LoadAsync(path); + var resource = await loader.LoadAsync(path).ConfigureAwait(false); lock (_loadLock) { // 双重检查 @@ -231,7 +231,7 @@ public class ResourceManager : IResourceManager /// public async Task PreloadAsync(string path) where T : class { - await LoadAsync(path); + await LoadAsync(path).ConfigureAwait(false); } /// @@ -324,4 +324,4 @@ public class ResourceManager : IResourceManager } } } -} \ No newline at end of file +} diff --git a/GFramework.Core/Services/ServiceModuleManager.cs b/GFramework.Core/Services/ServiceModuleManager.cs index a3965f4d..73e895c9 100644 --- a/GFramework.Core/Services/ServiceModuleManager.cs +++ b/GFramework.Core/Services/ServiceModuleManager.cs @@ -30,7 +30,7 @@ public sealed class ServiceModuleManager : IServiceModuleManager return; } - if (_modules.Any(m => m.ModuleName == module.ModuleName)) + if (_modules.Any(m => string.Equals(m.ModuleName, module.ModuleName, StringComparison.Ordinal))) { _logger.Warn($"Module {module.ModuleName} already registered"); return; @@ -109,7 +109,7 @@ public sealed class ServiceModuleManager : IServiceModuleManager if (asyncMode && module is IAsyncInitializable asyncInitializable) { - await asyncInitializable.InitializeAsync(); + await asyncInitializable.InitializeAsync().ConfigureAwait(false); } else { @@ -137,7 +137,7 @@ public sealed class ServiceModuleManager : IServiceModuleManager try { _logger.Debug($"Destroying module: {module.ModuleName}"); - await module.DestroyAsync(); + await module.DestroyAsync().ConfigureAwait(false); } catch (Exception ex) { diff --git a/GFramework.Core/State/StateMachine.cs b/GFramework.Core/State/StateMachine.cs index 2d0c3f58..f0e8dfa1 100644 --- a/GFramework.Core/State/StateMachine.cs +++ b/GFramework.Core/State/StateMachine.cs @@ -17,7 +17,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine /// /// 存储所有已注册状态的字典,键为状态类型,值为状态实例 /// - protected readonly Dictionary States = new(); + protected readonly IDictionary States = new Dictionary(); /// /// 获取当前激活的状态 @@ -45,7 +45,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine /// 要注销的状态类型 public async Task UnregisterAsync() where T : IState { - await _transitionLock.WaitAsync(); + await _transitionLock.WaitAsync().ConfigureAwait(false); try { var stateToUnregister = PrepareUnregister(out var isCurrentState); @@ -53,7 +53,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine if (isCurrentState) { - await ExecuteExitAsync(Current!, null); + await ExecuteExitAsync(Current!, null).ConfigureAwait(false); Current = null; } @@ -73,7 +73,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine /// 如果可以切换则返回true,否则返回false public async Task CanChangeToAsync() where T : IState { - await _transitionLock.WaitAsync(); + await _transitionLock.WaitAsync().ConfigureAwait(false); try { if (!States.TryGetValue(typeof(T), out var target)) @@ -81,7 +81,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine if (Current == null) return true; - return await CanTransitionToAsync(Current, target); + return await CanTransitionToAsync(Current, target).ConfigureAwait(false); } finally { @@ -98,7 +98,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine /// 当目标状态未注册时抛出 public async Task ChangeToAsync() where T : IState { - await _transitionLock.WaitAsync(); + await _transitionLock.WaitAsync().ConfigureAwait(false); try { IState target; @@ -114,15 +114,15 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine if (currentSnapshot != null) { - var canTransition = await CanTransitionToAsync(currentSnapshot, target); + var canTransition = await CanTransitionToAsync(currentSnapshot, target).ConfigureAwait(false); if (!canTransition) { - await OnTransitionRejectedAsync(currentSnapshot, target); + await OnTransitionRejectedAsync(currentSnapshot, target).ConfigureAwait(false); return false; } } - await ChangeInternalAsync(target); + await ChangeInternalAsync(target).ConfigureAwait(false); return true; } finally @@ -190,13 +190,13 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine /// 如果成功回退则返回true,否则返回false public async Task GoBackAsync() { - await _transitionLock.WaitAsync(); + await _transitionLock.WaitAsync().ConfigureAwait(false); try { var previousState = FindValidPreviousState(); if (previousState == null) return false; - await ChangeInternalWithoutHistoryAsync(previousState); + await ChangeInternalWithoutHistoryAsync(previousState).ConfigureAwait(false); return true; } finally @@ -282,13 +282,13 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine if (Current == next) return; var old = Current; - await OnStateChangingAsync(old, next); + await OnStateChangingAsync(old, next).ConfigureAwait(false); - await ExecuteExitAsync(old, next); + await ExecuteExitAsync(old, next).ConfigureAwait(false); Current = next; - await ExecuteEnterAsync(Current, old); + await ExecuteEnterAsync(Current, old).ConfigureAwait(false); - await OnStateChangedAsync(old, Current); + await OnStateChangedAsync(old, Current).ConfigureAwait(false); } /// @@ -300,18 +300,18 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine if (Current == next) return; var old = Current; - await OnStateChangingAsync(old, next); + await OnStateChangingAsync(old, next).ConfigureAwait(false); - await ExecuteExitAsync(old, next); + await ExecuteExitAsync(old, next).ConfigureAwait(false); AddToHistory(old); Current = next; - await ExecuteEnterAsync(Current, old); + await ExecuteEnterAsync(Current, old).ConfigureAwait(false); - await OnStateChangedAsync(old, Current); + await OnStateChangedAsync(old, Current).ConfigureAwait(false); } /// @@ -344,7 +344,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine if (state == null) return; if (state is IAsyncState asyncState) - await asyncState.OnEnterAsync(from); + await asyncState.OnEnterAsync(from).ConfigureAwait(false); else state.OnEnter(from); } @@ -357,7 +357,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine if (state == null) return; if (state is IAsyncState asyncState) - await asyncState.OnExitAsync(to); + await asyncState.OnExitAsync(to).ConfigureAwait(false); else state.OnExit(to); } @@ -368,7 +368,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine private static async Task CanTransitionToAsync(IState current, IState target) { if (current is IAsyncState asyncState) - return await asyncState.CanTransitionToAsync(target); + return await asyncState.CanTransitionToAsync(target).ConfigureAwait(false); return current.CanTransitionTo(target); } @@ -432,4 +432,4 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine OnStateChanged(from, to); return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/GFramework.Core/State/StateMachineSystem.cs b/GFramework.Core/State/StateMachineSystem.cs index 773a13a2..5fe02103 100644 --- a/GFramework.Core/State/StateMachineSystem.cs +++ b/GFramework.Core/State/StateMachineSystem.cs @@ -72,7 +72,7 @@ public class StateMachineSystem : StateMachine, IStateMachineSystem { if (Current is IAsyncState asyncState) { - await asyncState.OnExitAsync(null); // ✅ 正确等待异步清理 + await asyncState.OnExitAsync(null).ConfigureAwait(false); // ✅ 正确等待异步清理 } else { @@ -87,7 +87,7 @@ public class StateMachineSystem : StateMachine, IStateMachineSystem { if (state is IAsyncDestroyable asyncDestroyable) { - await asyncDestroyable.DestroyAsync(); + await asyncDestroyable.DestroyAsync().ConfigureAwait(false); } else if (state is IDestroyable destroyable) { @@ -106,7 +106,7 @@ public class StateMachineSystem : StateMachine, IStateMachineSystem protected override async Task ChangeInternalAsync(IState next) { var old = Current; - await base.ChangeInternalAsync(next); + await base.ChangeInternalAsync(next).ConfigureAwait(false); // 发送状态变更事件,通知监听者状态已发生改变 this.SendEvent(new StateChangedEvent @@ -115,4 +115,4 @@ public class StateMachineSystem : StateMachine, IStateMachineSystem NewState = Current }); } -} \ No newline at end of file +} diff --git a/GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs b/GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs index 63896c92..7cc2b57b 100644 --- a/GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs +++ b/GFramework.Cqrs/Cqrs/Behaviors/LoggingBehavior.cs @@ -50,7 +50,7 @@ public sealed class LoggingBehavior : IPipelineBehavior : IPipelineBehavior try { - return await next(message, cancellationToken); + return await next(message, cancellationToken).ConfigureAwait(false); } finally { diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index 5f7fa643..108f6c03 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -73,7 +73,7 @@ internal sealed class CqrsDispatcher( foreach (var handler in handlers) { PrepareHandler(handler, context); - await dispatchBinding.Invoker(handler, notification, cancellationToken); + await dispatchBinding.Invoker(handler, notification, cancellationToken).ConfigureAwait(false); } } @@ -106,9 +106,10 @@ internal sealed class CqrsDispatcher( PrepareHandler(behavior, context); if (behaviors.Count == 0) - return await dispatchBinding.RequestInvoker(handler, request, cancellationToken); + return await dispatchBinding.RequestInvoker(handler, request, cancellationToken).ConfigureAwait(false); - return await dispatchBinding.PipelineInvoker(handler, behaviors, request, cancellationToken); + return await dispatchBinding.PipelineInvoker(handler, behaviors, request, cancellationToken) + .ConfigureAwait(false); } /// diff --git a/GFramework.Cqrs/Internal/WeakKeyCache.cs b/GFramework.Cqrs/Internal/WeakKeyCache.cs index dadf2db4..2d97bd3d 100644 --- a/GFramework.Cqrs/Internal/WeakKeyCache.cs +++ b/GFramework.Cqrs/Internal/WeakKeyCache.cs @@ -44,8 +44,8 @@ internal sealed class WeakKeyCache if (entries.TryGetValue(key, out cachedValue)) return cachedValue; - var createdValue = valueFactory(key); - ArgumentNullException.ThrowIfNull(createdValue); + var createdValue = valueFactory(key) ?? + throw new InvalidOperationException("The value factory returned null."); entries.Add(key, createdValue); return createdValue; } @@ -78,8 +78,8 @@ internal sealed class WeakKeyCache if (entries.TryGetValue(key, out cachedValue)) return cachedValue; - var createdValue = valueFactory(key, state); - ArgumentNullException.ThrowIfNull(createdValue); + var createdValue = valueFactory(key, state) ?? + throw new InvalidOperationException("The value factory returned null."); entries.Add(key, createdValue); return createdValue; } @@ -125,90 +125,3 @@ internal sealed class WeakKeyCache return TryGetValue(key, out var value) ? value : null; } } - -/// -/// 提供以两段 为键的弱引用缓存。 -/// 适用于请求/响应或流请求/响应这类组合类型元数据的复用场景。 -/// -/// 缓存值类型。 -/// -/// 第一层和第二层键都使用弱键缓存,因此只要任一类型不再被外部引用, -/// 对应条目都允许被 GC 清理,并在后续首次访问时重新建立。 -/// -internal sealed class WeakTypePairCache - where TValue : class -{ - private readonly WeakKeyCache> _entries = new(); - - /// - /// 获取指定类型对对应的缓存值;若未命中则创建并写入。 - /// - /// 第一段类型键。 - /// 第二段类型键。 - /// 创建缓存值的工厂方法。 - /// 已存在或新创建的缓存值。 - /// - /// 或 - /// 。 - /// - public TValue GetOrAdd(Type primaryType, Type secondaryType, Func valueFactory) - { - ArgumentNullException.ThrowIfNull(primaryType); - ArgumentNullException.ThrowIfNull(secondaryType); - ArgumentNullException.ThrowIfNull(valueFactory); - - var secondaryEntries = _entries.GetOrAdd(primaryType, static _ => new WeakKeyCache()); - return secondaryEntries.GetOrAdd( - secondaryType, - (PrimaryType: primaryType, Factory: valueFactory), - static (cachedSecondaryType, state) => state.Factory(state.PrimaryType, cachedSecondaryType)); - } - - /// - /// 尝试读取指定类型对的缓存值,而不触发新的创建逻辑。 - /// - /// 第一段类型键。 - /// 第二段类型键。 - /// 命中时返回的缓存值。 - /// 若命中当前缓存则为 ;否则为 - /// - /// 。 - /// - public bool TryGetValue(Type primaryType, Type secondaryType, out TValue? value) - { - ArgumentNullException.ThrowIfNull(primaryType); - ArgumentNullException.ThrowIfNull(secondaryType); - - if (_entries.TryGetValue(primaryType, out var secondaryEntries) && - secondaryEntries is not null) - return secondaryEntries.TryGetValue(secondaryType, out value); - - value = null; - return false; - } - - /// - /// 清空当前缓存实例。 - /// - /// - /// 该方法主要服务于测试,避免同一进程里的静态缓存污染后续断言。 - /// - public void Clear() - { - _entries.Clear(); - } - - /// - /// 返回指定类型对当前命中的缓存对象;若未命中则返回 。 - /// - /// 第一段类型键。 - /// 第二段类型键。 - /// 当前缓存对象,或 - /// - /// 该入口仅用于测试通过反射观察缓存状态,不应用于运行时代码路径。 - /// - public TValue? GetValueOrDefaultForTesting(Type primaryType, Type secondaryType) - { - return TryGetValue(primaryType, secondaryType, out var value) ? value : null; - } -} diff --git a/GFramework.Cqrs/Internal/WeakTypePairCache.cs b/GFramework.Cqrs/Internal/WeakTypePairCache.cs new file mode 100644 index 00000000..4742a908 --- /dev/null +++ b/GFramework.Cqrs/Internal/WeakTypePairCache.cs @@ -0,0 +1,88 @@ +namespace GFramework.Cqrs.Internal; + +/// +/// 提供以两段 为键的弱引用缓存。 +/// 适用于请求/响应或流请求/响应这类组合类型元数据的复用场景。 +/// +/// 缓存值类型。 +/// +/// 第一层和第二层键都使用弱键缓存,因此只要任一类型不再被外部引用, +/// 对应条目都允许被 GC 清理,并在后续首次访问时重新建立。 +/// +internal sealed class WeakTypePairCache + where TValue : class +{ + private readonly WeakKeyCache> _entries = new(); + + /// + /// 获取指定类型对对应的缓存值;若未命中则创建并写入。 + /// + /// 第一段类型键。 + /// 第二段类型键。 + /// 创建缓存值的工厂方法。 + /// 已存在或新创建的缓存值。 + /// + /// 或 + /// 。 + /// + public TValue GetOrAdd(Type primaryType, Type secondaryType, Func valueFactory) + { + ArgumentNullException.ThrowIfNull(primaryType); + ArgumentNullException.ThrowIfNull(secondaryType); + ArgumentNullException.ThrowIfNull(valueFactory); + + var secondaryEntries = _entries.GetOrAdd(primaryType, static _ => new WeakKeyCache()); + return secondaryEntries.GetOrAdd( + secondaryType, + (PrimaryType: primaryType, Factory: valueFactory), + static (cachedSecondaryType, state) => state.Factory(state.PrimaryType, cachedSecondaryType)); + } + + /// + /// 尝试读取指定类型对的缓存值,而不触发新的创建逻辑。 + /// + /// 第一段类型键。 + /// 第二段类型键。 + /// 命中时返回的缓存值。 + /// 若命中当前缓存则为 ;否则为 + /// + /// 。 + /// + public bool TryGetValue(Type primaryType, Type secondaryType, out TValue? value) + { + ArgumentNullException.ThrowIfNull(primaryType); + ArgumentNullException.ThrowIfNull(secondaryType); + + if (_entries.TryGetValue(primaryType, out var secondaryEntries) && + secondaryEntries is not null) + return secondaryEntries.TryGetValue(secondaryType, out value); + + value = null; + return false; + } + + /// + /// 清空当前缓存实例。 + /// + /// + /// 该方法主要服务于测试,避免同一进程里的静态缓存污染后续断言。 + /// + public void Clear() + { + _entries.Clear(); + } + + /// + /// 返回指定类型对当前命中的缓存对象;若未命中则返回 。 + /// + /// 第一段类型键。 + /// 第二段类型键。 + /// 当前缓存对象,或 + /// + /// 该入口仅用于测试通过反射观察缓存状态,不应用于运行时代码路径。 + /// + public TValue? GetValueOrDefaultForTesting(Type primaryType, Type secondaryType) + { + return TryGetValue(primaryType, secondaryType, out var value) ? value : null; + } +} diff --git a/GFramework.Godot/Architectures/AbstractArchitecture.cs b/GFramework.Godot/Architectures/AbstractArchitecture.cs index 45f20e4d..aebb2df5 100644 --- a/GFramework.Godot/Architectures/AbstractArchitecture.cs +++ b/GFramework.Godot/Architectures/AbstractArchitecture.cs @@ -85,7 +85,7 @@ public abstract class AbstractArchitecture( Name = _architectureAnchorName }; - _anchor.Bind(() => DestroyAsync().AsTask()); + _anchor.Bind(() => _ = DestroyAsync().AsTask()); tree.Root.CallDeferred(Node.MethodName.AddChild, _anchor); } @@ -106,7 +106,7 @@ public abstract class AbstractArchitecture( throw new InvalidOperationException("Anchor not initialized"); // 等待锚点准备就绪 - await _anchor.WaitUntilReadyAsync(); + await _anchor.WaitUntilReadyAsync().ConfigureAwait(false); // 延迟调用将扩展节点添加为锚点的子节点 _anchor.CallDeferred(Node.MethodName.AddChild, module.Node); @@ -136,6 +136,6 @@ public abstract class AbstractArchitecture( _extensions.Clear(); - await base.DestroyAsync(); + await base.DestroyAsync().ConfigureAwait(false); } -} \ No newline at end of file +} diff --git a/GFramework.Godot/Config/GodotYamlConfigDirectoryEntry.cs b/GFramework.Godot/Config/GodotYamlConfigDirectoryEntry.cs new file mode 100644 index 00000000..839b832b --- /dev/null +++ b/GFramework.Godot/Config/GodotYamlConfigDirectoryEntry.cs @@ -0,0 +1,33 @@ +namespace GFramework.Godot.Config; + +/// +/// 描述一次目录枚举返回的单个子项。 +/// +/// +/// 该结构只承载目录扫描阶段需要的最小信息。 +/// 必须是单个目录项名称,而不是包含父目录的完整路径; +/// 对于 Godot 路径和普通路径都遵循相同约定,便于加载器统一做后续拼接与过滤。 +/// +internal readonly record struct GodotYamlConfigDirectoryEntry +{ + /// + /// 初始化一个目录枚举结果项。 + /// + /// 当前目录项的名称,不包含父目录路径。 + /// 指示该目录项是否为子目录。 + public GodotYamlConfigDirectoryEntry(string name, bool isDirectory) + { + Name = name; + IsDirectory = isDirectory; + } + + /// + /// 获取当前目录项的名称,不包含父目录路径。 + /// + public string Name { get; } + + /// + /// 获取一个值,指示当前目录项是否为子目录。 + /// + public bool IsDirectory { get; } +} diff --git a/GFramework.Godot/Config/GodotYamlConfigEnvironment.cs b/GFramework.Godot/Config/GodotYamlConfigEnvironment.cs new file mode 100644 index 00000000..79a40390 --- /dev/null +++ b/GFramework.Godot/Config/GodotYamlConfigEnvironment.cs @@ -0,0 +1,163 @@ +using System.IO; +using FileAccess = Godot.FileAccess; + +namespace GFramework.Godot.Config; + +/// +/// 抽象 与具体宿主环境之间的 Godot 路径和文件访问边界。 +/// +/// +/// 该抽象存在的原因,是编辑器态与导出态对 res://user:// 的访问方式不同: +/// 编辑器态通常可以把 Godot 特殊路径全局化后直接落到普通文件系统,而导出态往往只能通过 Godot API 读取原始文本资源, +/// 再把它们复制到运行时缓存目录。 在目录不存在或当前环境无法枚举时必须返回 +/// ,用来表达“不可访问”而不是抛出未找到异常; 则应保留底层读取失败异常, +/// 交由加载器包装成配置诊断。对于普通文件系统路径,应遵循 / 语义; +/// 对于 Godot 特殊路径,则应使用引擎提供的路径解析和读取能力。 +/// +internal sealed class GodotYamlConfigEnvironment +{ + /// + /// 初始化一个可替换的 Godot YAML 配置宿主环境抽象。 + /// + /// 返回当前进程是否处于 Godot 编辑器态的委托。 + /// + /// 把 Godot 特殊路径转换为普通绝对路径的委托。 + /// 当前加载器仅会在输入为 res://user:// 时调用它,返回值必须为非空绝对路径。 + /// + /// + /// 枚举指定目录直接子项的委托。 + /// 当目录不存在、无法访问或当前环境无法枚举该路径时,必须返回 。 + /// + /// + /// 检查指定路径上的文件是否存在的委托。 + /// 输入既可能是 Godot 特殊路径,也可能是普通绝对路径。 + /// + /// + /// 读取指定文件完整字节内容的委托。 + /// 当文件缺失或读取失败时,应抛出底层异常,由加载器统一包装为配置加载诊断。 + /// + /// 任一委托参数为 时抛出。 + public GodotYamlConfigEnvironment( + Func isEditor, + Func globalizePath, + Func?> enumerateDirectory, + Func fileExists, + Func readAllBytes) + { + IsEditor = isEditor ?? throw new ArgumentNullException(nameof(isEditor)); + GlobalizePath = globalizePath ?? throw new ArgumentNullException(nameof(globalizePath)); + EnumerateDirectory = enumerateDirectory ?? throw new ArgumentNullException(nameof(enumerateDirectory)); + FileExists = fileExists ?? throw new ArgumentNullException(nameof(fileExists)); + ReadAllBytes = readAllBytes ?? throw new ArgumentNullException(nameof(readAllBytes)); + } + + /// + /// 获取默认的 Godot 运行时环境实现。 + /// + /// + /// 默认实现使用 检测编辑器态, + /// 使用 处理 Godot 特殊路径, + /// 并在 Godot 路径与普通路径之间切换对应的枚举和读取 API。 + /// + public static GodotYamlConfigEnvironment Default { get; } = new( + static () => OS.HasFeature("editor"), + static path => ProjectSettings.GlobalizePath(path), + EnumerateDirectoryCore, + FileExistsCore, + ReadAllBytesCore); + + /// + /// 获取用于判断当前进程是否处于编辑器态的委托。 + /// + public Func IsEditor { get; } + + /// + /// 获取把 Godot 特殊路径转换为普通绝对路径的委托。 + /// + /// + /// 当前加载器只会对 res://user:// 路径调用该委托。 + /// 返回空字符串会被视为无效环境实现,并在后续路径解析阶段触发异常。 + /// + public Func GlobalizePath { get; } + + /// + /// 获取用于枚举目录直接子项的委托。 + /// + /// + /// 当目录不存在、无法访问,或当前环境无法枚举给定路径时,该委托必须返回 。 + /// 返回的集合只应包含当前目录下的直接子项,调用方会自行过滤隐藏项、子目录与非 YAML 文件。 + /// + public Func?> EnumerateDirectory { get; } + + /// + /// 获取用于检查文件是否存在的委托。 + /// + public Func FileExists { get; } + + /// + /// 获取用于读取文件完整字节内容的委托。 + /// + /// + /// 该委托在路径不存在、权限不足或 I/O 失败时应抛出底层异常,以便加载器保留失败原因并生成诊断信息。 + /// + public Func ReadAllBytes { get; } + + private static IReadOnlyList? EnumerateDirectoryCore(string path) + { + if (!path.IsGodotPath()) + { + if (!Directory.Exists(path)) + { + return null; + } + + return Directory + .EnumerateFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly) + .Select(static entryPath => new GodotYamlConfigDirectoryEntry( + Path.GetFileName(entryPath), + Directory.Exists(entryPath))) + .ToArray(); + } + + using var directory = DirAccess.Open(path); + if (directory == null) + { + return null; + } + + var entries = new List(); + var listDirectoryError = directory.ListDirBegin(); + if (listDirectoryError != Error.Ok) + { + return null; + } + + while (true) + { + var name = directory.GetNext(); + if (string.IsNullOrEmpty(name)) + { + break; + } + + entries.Add(new GodotYamlConfigDirectoryEntry(name, directory.CurrentIsDir())); + } + + directory.ListDirEnd(); + return entries; + } + + private static bool FileExistsCore(string path) + { + return path.IsGodotPath() + ? FileAccess.FileExists(path) + : File.Exists(path); + } + + private static byte[] ReadAllBytesCore(string path) + { + return path.IsGodotPath() + ? FileAccess.GetFileAsBytes(path) + : File.ReadAllBytes(path); + } +} diff --git a/GFramework.Godot/Config/GodotYamlConfigLoader.cs b/GFramework.Godot/Config/GodotYamlConfigLoader.cs index 51742da9..56e4c415 100644 --- a/GFramework.Godot/Config/GodotYamlConfigLoader.cs +++ b/GFramework.Godot/Config/GodotYamlConfigLoader.cs @@ -153,7 +153,7 @@ public sealed class GodotYamlConfigLoader : IConfigLoader SynchronizeRuntimeCache(cancellationToken); } - await _loader.LoadAsync(registry, cancellationToken); + await _loader.LoadAsync(registry, cancellationToken).ConfigureAwait(false); } /// @@ -511,194 +511,3 @@ public sealed class GodotYamlConfigLoader : IConfigLoader innerException); } } - -/// -/// 抽象 与具体宿主环境之间的 Godot 路径和文件访问边界。 -/// -/// -/// 该抽象存在的原因,是编辑器态与导出态对 res://user:// 的访问方式不同: -/// 编辑器态通常可以把 Godot 特殊路径全局化后直接落到普通文件系统,而导出态往往只能通过 Godot API 读取原始文本资源, -/// 再把它们复制到运行时缓存目录。 在目录不存在或当前环境无法枚举时必须返回 -/// ,用来表达“不可访问”而不是抛出未找到异常; 则应保留底层读取失败异常, -/// 交由加载器包装成配置诊断。对于普通文件系统路径,应遵循 / 语义; -/// 对于 Godot 特殊路径,则应使用引擎提供的路径解析和读取能力。 -/// -internal sealed class GodotYamlConfigEnvironment -{ - /// - /// 初始化一个可替换的 Godot YAML 配置宿主环境抽象。 - /// - /// 返回当前进程是否处于 Godot 编辑器态的委托。 - /// - /// 把 Godot 特殊路径转换为普通绝对路径的委托。 - /// 当前加载器仅会在输入为 res://user:// 时调用它,返回值必须为非空绝对路径。 - /// - /// - /// 枚举指定目录直接子项的委托。 - /// 当目录不存在、无法访问或当前环境无法枚举该路径时,必须返回 。 - /// - /// - /// 检查指定路径上的文件是否存在的委托。 - /// 输入既可能是 Godot 特殊路径,也可能是普通绝对路径。 - /// - /// - /// 读取指定文件完整字节内容的委托。 - /// 当文件缺失或读取失败时,应抛出底层异常,由加载器统一包装为配置加载诊断。 - /// - /// 任一委托参数为 时抛出。 - public GodotYamlConfigEnvironment( - Func isEditor, - Func globalizePath, - Func?> enumerateDirectory, - Func fileExists, - Func readAllBytes) - { - IsEditor = isEditor ?? throw new ArgumentNullException(nameof(isEditor)); - GlobalizePath = globalizePath ?? throw new ArgumentNullException(nameof(globalizePath)); - EnumerateDirectory = enumerateDirectory ?? throw new ArgumentNullException(nameof(enumerateDirectory)); - FileExists = fileExists ?? throw new ArgumentNullException(nameof(fileExists)); - ReadAllBytes = readAllBytes ?? throw new ArgumentNullException(nameof(readAllBytes)); - } - - /// - /// 获取默认的 Godot 运行时环境实现。 - /// - /// - /// 默认实现使用 检测编辑器态, - /// 使用 处理 Godot 特殊路径, - /// 并在 Godot 路径与普通路径之间切换对应的枚举和读取 API。 - /// - public static GodotYamlConfigEnvironment Default { get; } = new( - static () => OS.HasFeature("editor"), - static path => ProjectSettings.GlobalizePath(path), - EnumerateDirectoryCore, - FileExistsCore, - ReadAllBytesCore); - - /// - /// 获取用于判断当前进程是否处于编辑器态的委托。 - /// - public Func IsEditor { get; } - - /// - /// 获取把 Godot 特殊路径转换为普通绝对路径的委托。 - /// - /// - /// 当前加载器只会对 res://user:// 路径调用该委托。 - /// 返回空字符串会被视为无效环境实现,并在后续路径解析阶段触发异常。 - /// - public Func GlobalizePath { get; } - - /// - /// 获取用于枚举目录直接子项的委托。 - /// - /// - /// 当目录不存在、无法访问,或当前环境无法枚举给定路径时,该委托必须返回 。 - /// 返回的集合只应包含当前目录下的直接子项,调用方会自行过滤隐藏项、子目录与非 YAML 文件。 - /// - public Func?> EnumerateDirectory { get; } - - /// - /// 获取用于检查文件是否存在的委托。 - /// - public Func FileExists { get; } - - /// - /// 获取用于读取文件完整字节内容的委托。 - /// - /// - /// 该委托在路径不存在、权限不足或 I/O 失败时应抛出底层异常,以便加载器保留失败原因并生成诊断信息。 - /// - public Func ReadAllBytes { get; } - - private static IReadOnlyList? EnumerateDirectoryCore(string path) - { - if (!path.IsGodotPath()) - { - if (!Directory.Exists(path)) - { - return null; - } - - return Directory - .EnumerateFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly) - .Select(static entryPath => new GodotYamlConfigDirectoryEntry( - Path.GetFileName(entryPath), - Directory.Exists(entryPath))) - .ToArray(); - } - - using var directory = DirAccess.Open(path); - if (directory == null) - { - return null; - } - - var entries = new List(); - var listDirectoryError = directory.ListDirBegin(); - if (listDirectoryError != Error.Ok) - { - return null; - } - - while (true) - { - var name = directory.GetNext(); - if (string.IsNullOrEmpty(name)) - { - break; - } - - entries.Add(new GodotYamlConfigDirectoryEntry(name, directory.CurrentIsDir())); - } - - directory.ListDirEnd(); - return entries; - } - - private static bool FileExistsCore(string path) - { - return path.IsGodotPath() - ? FileAccess.FileExists(path) - : File.Exists(path); - } - - private static byte[] ReadAllBytesCore(string path) - { - return path.IsGodotPath() - ? FileAccess.GetFileAsBytes(path) - : File.ReadAllBytes(path); - } -} - -/// -/// 描述一次目录枚举返回的单个子项。 -/// -/// -/// 该结构只承载目录扫描阶段需要的最小信息。 -/// 必须是单个目录项名称,而不是包含父目录的完整路径; -/// 对于 Godot 路径和普通路径都遵循相同约定,便于加载器统一做后续拼接与过滤。 -/// -internal readonly record struct GodotYamlConfigDirectoryEntry -{ - /// - /// 初始化一个目录枚举结果项。 - /// - /// 当前目录项的名称,不包含父目录路径。 - /// 指示该目录项是否为子目录。 - public GodotYamlConfigDirectoryEntry(string name, bool isDirectory) - { - Name = name; - IsDirectory = isDirectory; - } - - /// - /// 获取当前目录项的名称,不包含父目录路径。 - /// - public string Name { get; } - - /// - /// 获取一个值,指示当前目录项是否为子目录。 - /// - public bool IsDirectory { get; } -} diff --git a/GFramework.Godot/Data/GodotResourceRepository.cs b/GFramework.Godot/Data/GodotResourceRepository.cs index 0035e7d4..eb8b819f 100644 --- a/GFramework.Godot/Data/GodotResourceRepository.cs +++ b/GFramework.Godot/Data/GodotResourceRepository.cs @@ -209,7 +209,8 @@ public class GodotResourceRepository } // 只处理.tres和.res扩展名的资源文件 - if (!entry.EndsWith(".tres") && !entry.EndsWith(".res")) + if (!entry.EndsWith(".tres", StringComparison.OrdinalIgnoreCase) && + !entry.EndsWith(".res", StringComparison.OrdinalIgnoreCase)) return; // 加载资源文件 @@ -224,4 +225,4 @@ public class GodotResourceRepository if (!_storage.TryAdd(resource.Key, resource)) Log.Warn($"Duplicate key detected: {resource.Key}"); } -} \ No newline at end of file +} diff --git a/GFramework.Godot/Extensions/GodotPathExtensions.cs b/GFramework.Godot/Extensions/GodotPathExtensions.cs index 5469482d..c4cfda45 100644 --- a/GFramework.Godot/Extensions/GodotPathExtensions.cs +++ b/GFramework.Godot/Extensions/GodotPathExtensions.cs @@ -13,7 +13,7 @@ public static class GodotPathExtensions /// 如果路径以 "user://" 开头且不为空,则返回 true;否则返回 false。 public static bool IsUserPath(this string path) { - return !string.IsNullOrEmpty(path) && path.StartsWith("user://"); + return !string.IsNullOrEmpty(path) && path.StartsWith("user://", StringComparison.Ordinal); } /// @@ -23,7 +23,7 @@ public static class GodotPathExtensions /// 如果路径以 "res://" 开头且不为空,则返回 true;否则返回 false。 public static bool IsResPath(this string path) { - return !string.IsNullOrEmpty(path) && path.StartsWith("res://"); + return !string.IsNullOrEmpty(path) && path.StartsWith("res://", StringComparison.Ordinal); } /// @@ -35,4 +35,4 @@ public static class GodotPathExtensions { return path.IsUserPath() || path.IsResPath(); } -} \ No newline at end of file +} diff --git a/GFramework.Godot/Extensions/NodeExtensions.cs b/GFramework.Godot/Extensions/NodeExtensions.cs index a0106418..b9aabb66 100644 --- a/GFramework.Godot/Extensions/NodeExtensions.cs +++ b/GFramework.Godot/Extensions/NodeExtensions.cs @@ -176,7 +176,7 @@ public static class NodeExtensions public static async Task AddChildXAsync(this Node parent, Node child) { parent.AddChild(child); - await child.WaitUntilReadyAsync(); + await child.WaitUntilReadyAsync().ConfigureAwait(false); } /// @@ -285,4 +285,4 @@ public static class NodeExtensions return t; throw new InvalidCastException($"Cannot cast {node} to {typeof(T)}"); } -} \ No newline at end of file +} diff --git a/GFramework.Godot/Logging/GodotLogger.cs b/GFramework.Godot/Logging/GodotLogger.cs index 62529213..e0fc2948 100644 --- a/GFramework.Godot/Logging/GodotLogger.cs +++ b/GFramework.Godot/Logging/GodotLogger.cs @@ -1,4 +1,5 @@ -using GFramework.Core.Abstractions.Logging; +using System.Globalization; +using GFramework.Core.Abstractions.Logging; using GFramework.Core.Logging; using Godot; @@ -36,7 +37,7 @@ public sealed class GodotLogger( protected override void Write(LogLevel level, string message, Exception? exception) { // 构造时间戳和日志前缀 - var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff"); + var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture); var levelStr = LevelStrings[(int)level]; var logPrefix = $"[{timestamp}] {levelStr} [{Name()}]"; @@ -71,4 +72,4 @@ public sealed class GodotLogger( break; } } -} \ No newline at end of file +} diff --git a/GFramework.Godot/Scene/SceneBehaviorBase.cs b/GFramework.Godot/Scene/SceneBehaviorBase.cs index d2c30da5..e3a0b083 100644 --- a/GFramework.Godot/Scene/SceneBehaviorBase.cs +++ b/GFramework.Godot/Scene/SceneBehaviorBase.cs @@ -114,7 +114,7 @@ public abstract class SceneBehaviorBase : ISceneBehavior // 调用可选接口 if (_scene != null) - await _scene.OnLoadAsync(param); + await _scene.OnLoadAsync(param).ConfigureAwait(false); _isLoaded = true; _isTransitioning = false; @@ -130,7 +130,7 @@ public abstract class SceneBehaviorBase : ISceneBehavior _isTransitioning = true; if (_scene != null) - await _scene.OnEnterAsync(); + await _scene.OnEnterAsync().ConfigureAwait(false); _isActive = true; _isTransitioning = false; @@ -144,7 +144,7 @@ public abstract class SceneBehaviorBase : ISceneBehavior public virtual async ValueTask OnPauseAsync() { if (_scene != null) - await _scene.OnPauseAsync(); + await _scene.OnPauseAsync().ConfigureAwait(false); // 暂停处理 Owner.SetProcess(false); @@ -165,7 +165,7 @@ public abstract class SceneBehaviorBase : ISceneBehavior return; if (_scene != null) - await _scene.OnResumeAsync(); + await _scene.OnResumeAsync().ConfigureAwait(false); // 恢复处理 Owner.SetProcess(true); @@ -185,7 +185,7 @@ public abstract class SceneBehaviorBase : ISceneBehavior _isTransitioning = true; if (_scene != null) - await _scene.OnExitAsync(); + await _scene.OnExitAsync().ConfigureAwait(false); _isActive = false; } @@ -198,7 +198,7 @@ public abstract class SceneBehaviorBase : ISceneBehavior public virtual async ValueTask OnUnloadAsync() { if (_scene != null) - await _scene.OnUnloadAsync(); + await _scene.OnUnloadAsync().ConfigureAwait(false); // 释放节点 Owner.QueueFreeX(); @@ -208,4 +208,4 @@ public abstract class SceneBehaviorBase : ISceneBehavior } #endregion -} \ No newline at end of file +} diff --git a/GFramework.Godot/Setting/Data/LocalizationMap.cs b/GFramework.Godot/Setting/Data/LocalizationMap.cs index 1864f4ae..fffcd0f7 100644 --- a/GFramework.Godot/Setting/Data/LocalizationMap.cs +++ b/GFramework.Godot/Setting/Data/LocalizationMap.cs @@ -24,7 +24,7 @@ public class LocalizationMap /// /// 用户语言 -> Godot locale 映射表。 /// - public Dictionary LanguageMap { get; set; } = new() + public Dictionary LanguageMap { get; set; } = new(StringComparer.Ordinal) { { "简体中文", "zh_CN" }, { "English", "en" } @@ -33,7 +33,7 @@ public class LocalizationMap /// /// 用户语言 -> GFramework 本地化语言码映射表。 /// - public Dictionary FrameworkLanguageMap { get; set; } = new() + public Dictionary FrameworkLanguageMap { get; set; } = new(StringComparer.Ordinal) { { "简体中文", "zhs" }, { "English", "eng" } @@ -68,4 +68,4 @@ public class LocalizationMap return FrameworkLanguageMap.GetValueOrDefault(storedLanguage, DefaultFrameworkLanguage); } -} \ No newline at end of file +} diff --git a/GFramework.Godot/Setting/GodotGraphicsSettings.cs b/GFramework.Godot/Setting/GodotGraphicsSettings.cs index e6870cef..6f2db267 100644 --- a/GFramework.Godot/Setting/GodotGraphicsSettings.cs +++ b/GFramework.Godot/Setting/GodotGraphicsSettings.cs @@ -14,7 +14,7 @@ public class GodotGraphicsSettings(ISettingsModel model) : IResetApplyAbleSettin /// 应用图形设置到Godot引擎 /// /// 异步任务 - public async Task Apply() + public Task Apply() { var settings = model.GetData(); // 创建分辨率向量 @@ -40,7 +40,7 @@ public class GodotGraphicsSettings(ISettingsModel model) : IResetApplyAbleSettin DisplayServer.WindowSetPosition(pos); } - await Task.CompletedTask; + return Task.CompletedTask; } /// @@ -62,4 +62,4 @@ public class GodotGraphicsSettings(ISettingsModel model) : IResetApplyAbleSettin /// 该属性返回图形设置数据的具体类型信息。 /// public Type DataType { get; } = typeof(GraphicsSettings); -} \ No newline at end of file +} diff --git a/GFramework.Godot/Storage/GodotFileStorage.cs b/GFramework.Godot/Storage/GodotFileStorage.cs index 12565205..a835f64a 100644 --- a/GFramework.Godot/Storage/GodotFileStorage.cs +++ b/GFramework.Godot/Storage/GodotFileStorage.cs @@ -81,8 +81,9 @@ public sealed class GodotFileStorage : IStorage, IDisposable { ObjectDisposedException.ThrowIf(_disposed, this); var path = ToAbsolutePath(key); + var pathLock = await _lockManager.AcquireLockAsync(path).ConfigureAwait(false); - await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false)) + await using (pathLock.ConfigureAwait(false)) { // 处理Godot文件系统路径的删除操作 if (path.IsGodotPath()) @@ -179,8 +180,9 @@ public sealed class GodotFileStorage : IStorage, IDisposable { ObjectDisposedException.ThrowIf(_disposed, this); var path = ToAbsolutePath(key); + var pathLock = await _lockManager.AcquireLockAsync(path).ConfigureAwait(false); - await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false)) + await using (pathLock.ConfigureAwait(false)) { if (!path.IsGodotPath()) return File.Exists(path); using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read); @@ -238,8 +240,9 @@ public sealed class GodotFileStorage : IStorage, IDisposable { ObjectDisposedException.ThrowIf(_disposed, this); var path = ToAbsolutePath(key); + var pathLock = await _lockManager.AcquireLockAsync(path).ConfigureAwait(false); - await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false)) + await using (pathLock.ConfigureAwait(false)) { string content; @@ -290,7 +293,7 @@ public sealed class GodotFileStorage : IStorage, IDisposable dir.ListDirEnd(); return (IReadOnlyList)result; - }); + }).ConfigureAwait(false); } /// @@ -319,7 +322,7 @@ public sealed class GodotFileStorage : IStorage, IDisposable dir.ListDirEnd(); return (IReadOnlyList)result; - }); + }).ConfigureAwait(false); } /// @@ -345,7 +348,7 @@ public sealed class GodotFileStorage : IStorage, IDisposable var fullPath = ToAbsolutePath(path); if (!DirAccess.DirExistsAbsolute(fullPath)) DirAccess.MakeDirRecursiveAbsolute(fullPath); - }); + }).ConfigureAwait(false); } #endregion @@ -378,8 +381,9 @@ public sealed class GodotFileStorage : IStorage, IDisposable { ObjectDisposedException.ThrowIf(_disposed, this); var path = ToAbsolutePath(key); + var pathLock = await _lockManager.AcquireLockAsync(path).ConfigureAwait(false); - await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false)) + await using (pathLock.ConfigureAwait(false)) { var content = _serializer.Serialize(value); if (path.IsGodotPath()) @@ -397,4 +401,4 @@ public sealed class GodotFileStorage : IStorage, IDisposable } #endregion -} \ No newline at end of file +} diff --git a/GFramework.Godot/UI/UiPageBehaviorFactory.cs b/GFramework.Godot/UI/UiPageBehaviorFactory.cs index 9f5042e6..8b5f8319 100644 --- a/GFramework.Godot/UI/UiPageBehaviorFactory.cs +++ b/GFramework.Godot/UI/UiPageBehaviorFactory.cs @@ -40,7 +40,7 @@ public static class UiPageBehaviorFactory UiLayer.Modal => new ModalLayerUiPageBehavior(owner, key), UiLayer.Toast => new ToastLayerUiPageBehavior(owner, key), UiLayer.Topmost => new TopmostLayerUiPageBehavior(owner, key), - _ => throw new ArgumentException($"Unsupported UI layer: {layer}") + _ => throw new ArgumentException($"Unsupported UI layer: {layer}", nameof(layer)) }; } -} \ No newline at end of file +} From 3f25ea56248209426e20f1d757c43d31b3510b6a Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:57:49 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat(core):=20=E6=B7=BB=E5=8A=A0=E5=87=BD?= =?UTF-8?q?=E6=95=B0=E5=BC=8F=E7=BC=96=E7=A8=8B=E6=89=A9=E5=B1=95=E5=92=8C?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E6=B3=A8=E5=85=A5=E5=AE=B9=E5=99=A8=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现 ResultExtensions 扩展类,提供 Combine、Map、Bind 等函数式操作 - 添加 MicrosoftDiContainer 依赖注入容器,支持单例、瞬态、作用域服务注册 - 实现 CQRS 管道行为和处理器注册功能 - 添加本地化字符串实现和相关工具方法 - 提供线程安全的多读单写锁保护机制 - 实现服务实例的优先级排序和批量获取功能 --- .../Functional/ResultExtensions.cs | 2 +- GFramework.Core/Ioc/MicrosoftDiContainer.cs | 2 +- .../Localization/LocalizationString.cs | 29 ++++++++++++++----- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/GFramework.Core/Functional/ResultExtensions.cs b/GFramework.Core/Functional/ResultExtensions.cs index 6be7870d..3d9d7d2d 100644 --- a/GFramework.Core/Functional/ResultExtensions.cs +++ b/GFramework.Core/Functional/ResultExtensions.cs @@ -177,7 +177,7 @@ public static class ResultExtensions return result.Match( succ: value => predicate(value) ? result - : Result.Fail(new ArgumentException(errorMessage, nameof(errorMessage))), + : Result.Fail(new ArgumentException(errorMessage)), fail: _ => result ); } diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index 4dd7034b..9192e289 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -394,7 +394,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) { if (assembly is null) { - throw new ArgumentException("Assemblies collection cannot contain null items.", nameof(assemblies)); + throw new ArgumentNullException(nameof(assemblies), "Assemblies collection cannot contain null items."); } } diff --git a/GFramework.Core/Localization/LocalizationString.cs b/GFramework.Core/Localization/LocalizationString.cs index c925cf9f..c926d73b 100644 --- a/GFramework.Core/Localization/LocalizationString.cs +++ b/GFramework.Core/Localization/LocalizationString.cs @@ -8,11 +8,26 @@ namespace GFramework.Core.Localization; /// public class LocalizationString : ILocalizationString { + /// + /// 正则分组名:变量名。 + /// + private const string VariableGroupName = "variable"; + + /// + /// 正则分组名:格式化器名。 + /// + private const string FormatterGroupName = "formatter"; + + /// + /// 正则分组名:格式化器参数。 + /// + private const string FormatterArgsGroupName = "args"; + /// /// 匹配 {variableName} 或 {variableName:formatter:args} 的正则表达式模式 /// private const string FormatVariablePattern = - @"\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([a-zA-Z_][a-zA-Z0-9_]*)(?::([^}]+))?)?\}"; + @"\{(?[a-zA-Z_][a-zA-Z0-9_]*)(?::(?[a-zA-Z_][a-zA-Z0-9_]*)(?::(?[^}]+))?)?\}"; /// /// 预编译的静态正则表达式,用于格式化字符串中的变量替换 @@ -157,13 +172,13 @@ public class LocalizationString : ILocalizationString Dictionary variables, ILocalizationManager manager) { - var variableName = match.Groups[1].Value; + var variableName = match.Groups[VariableGroupName].Value; if (!variables.TryGetValue(variableName, out var value)) { return match.Value; } - var formatterName = GetOptionalGroupValue(match, 2); + var formatterName = GetOptionalGroupValue(match, FormatterGroupName); if (string.IsNullOrEmpty(formatterName)) { return FormatValue(value, manager); @@ -190,7 +205,7 @@ public class LocalizationString : ILocalizationString ILocalizationManager manager, out string result) { - var formatterArgs = GetOptionalGroupValue(match, 3) ?? string.Empty; + var formatterArgs = GetOptionalGroupValue(match, FormatterArgsGroupName) ?? string.Empty; if (GetFormatter(manager, formatterName) is { } formatter && formatter.TryFormat(formatterArgs, value, manager.CurrentCulture, out result)) { @@ -220,11 +235,11 @@ public class LocalizationString : ILocalizationString /// 获取正则表达式匹配组中的可选值 /// /// 正则表达式匹配结果 - /// 要获取的组索引 + /// 要获取的命名组 /// 如果该组匹配成功则返回其值;否则返回 null - private static string? GetOptionalGroupValue(Match match, int groupIndex) + private static string? GetOptionalGroupValue(Match match, string groupName) { - return match.Groups[groupIndex].Success ? match.Groups[groupIndex].Value : null; + return match.Groups[groupName].Success ? match.Groups[groupName].Value : null; } /// From e3652db030204e4bb01f75f24a3158018f2a6acf Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sat, 18 Apr 2026 19:37:25 +0800 Subject: [PATCH 3/7] =?UTF-8?q?fix/review-followups:=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=AE=A1=E6=9F=A5=E5=8F=8D=E9=A6=88=E5=B9=B6=E8=A1=A5=E5=85=85?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E6=B5=81=E7=A8=8B=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 Core、Cqrs、Godot 与 Game 模块中的异常契约、空值校验和线程亲和性问题 - 更新 Settings API 为 ApplyAsync 并同步实现、调用点、测试与中文文档 - 补充 AGENTS.md 中的构建校验、自动提交与分支创建规则 - 整理 Logging、WeakCache 与 Resource 相关实现的行为与文档一致性 --- AGENTS.md | 18 ++++++++ .../Resource/IResourceManager.cs | 19 ++++++--- .../Extensions/NumericExtensionsTests.cs | 20 ++++++++- .../Extensions/ResultExtensionsTests.cs | 9 +++- .../Functional/ResultTests.cs | 18 +++++++- .../Logging/LoggingConfigurationTests.cs | 31 +++++++++++++- .../Coroutine/Instructions/WaitForTask.cs | 3 -- .../Extensions/NumericExtensions.cs | 4 ++ GFramework.Core/Functional/Result.cs | 14 +++++-- .../Functional/ResultExtensions.cs | 2 +- .../Logging/AppenderConfiguration.cs | 2 - .../Logging/ConfigurableLoggerFactory.cs | 18 +++++--- .../Logging/Filters/SamplingFilter.cs | 5 ++- GFramework.Core/Resource/ResourceManager.cs | 16 +++++++- GFramework.Cqrs/Internal/WeakKeyCache.cs | 12 ++---- GFramework.Cqrs/Internal/WeakTypePairCache.cs | 3 ++ .../Setting/IApplyAbleSettings.cs | 10 +++-- .../Setting/GodotLocalizationSettingsTests.cs | 14 +++---- .../Setting/SettingsSystemTests.cs | 2 +- GFramework.Game/Setting/SettingsModel.cs | 4 +- GFramework.Game/Setting/SettingsSystem.cs | 4 +- .../Architectures/AbstractArchitecture.cs | 41 +++++++++++++++++-- .../Config/GodotYamlConfigEnvironment.cs | 26 ++++++++++-- GFramework.Godot/Scene/SceneBehaviorBase.cs | 6 +-- .../Setting/GodotAudioSettings.cs | 6 +-- .../Setting/GodotGraphicsSettings.cs | 6 +-- .../Setting/GodotLocalizationSettings.cs | 6 +-- GFramework.Godot/UI/UiPageBehaviorFactory.cs | 1 + docs/zh-CN/game/setting.md | 4 +- docs/zh-CN/godot/setting.md | 30 +++++++------- 30 files changed, 268 insertions(+), 86 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4fd3b7b8..9cf4ec07 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,24 @@ All AI agents and contributors must follow these rules when writing, reviewing, - After resolving the host Windows Git path, prefer an explicit session-local binding for subsequent commands so the shell does not fall back to Linux `/usr/bin/git` later in the same WSL session. +## Git Workflow Rules + +- Every completed task MUST pass at least one build validation before it is considered done. +- If the task changes multiple projects or shared abstractions, prefer a solution-level or affected-project + `dotnet build ... -c Release`; otherwise use the smallest build command that still proves the result compiles. +- If the required build passes and there are task-related staged or unstaged changes, contributors MUST create a Git + commit automatically instead of leaving the task uncommitted, unless the user explicitly says not to commit. +- Commit messages MUST use Conventional Commits format: `(): `. +- The commit `summary` MUST use simplified Chinese and briefly describe the main change. +- The commit `body` MUST use unordered list items, and each item MUST start with a verb such as `新增`、`修复`、`优化`、 + `更新`、`补充`、`重构`. +- Each commit body bullet MUST describe one independent change point; avoid repeated or redundant descriptions. +- Keep technical terms in English when they are established project terms, such as `API`、`Model`、`System`. +- If a new task starts while the current branch is `main`, contributors MUST first try to update local `main` from the + remote, then create and switch to a dedicated branch before making substantive changes. +- The branch naming rule for a new task branch is `/`, where `` should match the intended + Conventional Commit category as closely as practical. + ## Subagent Usage Rules - Use subagents only when the task is complex, the context is likely to grow too large, or the work can be split into diff --git a/GFramework.Core.Abstractions/Resource/IResourceManager.cs b/GFramework.Core.Abstractions/Resource/IResourceManager.cs index 267f24f6..36655180 100644 --- a/GFramework.Core.Abstractions/Resource/IResourceManager.cs +++ b/GFramework.Core.Abstractions/Resource/IResourceManager.cs @@ -22,11 +22,14 @@ public interface IResourceManager : IUtility T? Load(string path) where T : class; /// - /// 异步加载资源 + /// 异步加载指定路径的资源,并在缓存中对并发加载进行去重。 /// /// 资源类型 - /// 资源路径 - /// 资源实例,如果加载失败返回 null + /// 资源路径,不能为空或空白。 + /// 加载成功返回资源实例;加载失败返回 + /// 为空或空白时抛出。 + /// 当未注册对应资源加载器时抛出。 + /// 实现内部可能使用 ConfigureAwait(false),异步延续不保证回到调用线程。 Task LoadAsync(string path) where T : class; /// @@ -70,10 +73,14 @@ public interface IResourceManager : IUtility void UnregisterLoader() where T : class; /// - /// 预加载资源(加载但不返回) + /// 预加载资源到缓存中。 /// /// 资源类型 - /// 资源路径 + /// 资源路径,不能为空或空白。 + /// 表示预加载流程完成的任务。 + /// 为空或空白时抛出。 + /// 当未注册对应资源加载器时抛出。 + /// 内部委托给 ,同样不捕获同步上下文。 Task PreloadAsync(string path) where T : class; /// @@ -86,4 +93,4 @@ public interface IResourceManager : IUtility /// /// 资源释放策略 void SetReleaseStrategy(IResourceReleaseStrategy strategy); -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Extensions/NumericExtensionsTests.cs b/GFramework.Core.Tests/Extensions/NumericExtensionsTests.cs index 51a06b65..31fbbc54 100644 --- a/GFramework.Core.Tests/Extensions/NumericExtensionsTests.cs +++ b/GFramework.Core.Tests/Extensions/NumericExtensionsTests.cs @@ -118,6 +118,24 @@ public class NumericExtensionsTests Assert.Throws(() => value.Between(100, 0)); } + /// + /// 测试Between方法在引用类型参数为null时抛出ArgumentNullException + /// + [Test] + public void Between_Should_Throw_ArgumentNullException_When_Reference_Arguments_Are_Null() + { + string? value = "m"; + string? min = "a"; + string? max = "z"; + + Assert.Multiple(() => + { + Assert.Throws(() => NumericExtensions.Between(value!, null!, max!)); + Assert.Throws(() => NumericExtensions.Between(value!, min!, null!)); + Assert.Throws(() => NumericExtensions.Between(null!, min!, max!)); + }); + } + /// /// 测试Lerp方法在t为0时返回起始值 /// @@ -205,4 +223,4 @@ public class NumericExtensionsTests // Arrange & Act & Assert Assert.Throws(() => 50f.InverseLerp(100f, 100f)); } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Extensions/ResultExtensionsTests.cs b/GFramework.Core.Tests/Extensions/ResultExtensionsTests.cs index 11992e00..9d363da5 100644 --- a/GFramework.Core.Tests/Extensions/ResultExtensionsTests.cs +++ b/GFramework.Core.Tests/Extensions/ResultExtensionsTests.cs @@ -319,7 +319,12 @@ public class ResultExtensionsTests { var result = Result.Succeed(-1); var ensured = result.Ensure(x => x > 0, "Value must be positive"); - Assert.That(ensured.Exception.Message, Is.EqualTo("Value must be positive")); + Assert.Multiple(() => + { + Assert.That(ensured.Exception.Message, Does.StartWith("Value must be positive")); + Assert.That(ensured.Exception, Is.TypeOf()); + Assert.That(((ArgumentException)ensured.Exception).ParamName, Is.EqualTo("result")); + }); } /// @@ -635,4 +640,4 @@ public class ResultExtensionsTests Assert.That(result.IsSuccess, Is.True); } -} \ No newline at end of file +} diff --git a/GFramework.Core.Tests/Functional/ResultTests.cs b/GFramework.Core.Tests/Functional/ResultTests.cs index a290e3f1..e3a3ad28 100644 --- a/GFramework.Core.Tests/Functional/ResultTests.cs +++ b/GFramework.Core.Tests/Functional/ResultTests.cs @@ -429,4 +429,20 @@ public class ResultTests // Assert Assert.That(str, Is.EqualTo("Fail(Test error)")); } -} \ No newline at end of file + + /// + /// 测试default(Result)不会在相等性比较、哈希计算和字符串格式化中触发空引用异常 + /// + [Test] + public void Default_Result_Should_Be_Safe_For_Equality_HashCode_And_ToString() + { + var result = default(Result); + + Assert.Multiple(() => + { + Assert.That(result.Equals(default(Result)), Is.True); + Assert.That(result.GetHashCode(), Is.EqualTo(0)); + Assert.That(result.ToString(), Is.EqualTo("Fail(null)")); + }); + } +} diff --git a/GFramework.Core.Tests/Logging/LoggingConfigurationTests.cs b/GFramework.Core.Tests/Logging/LoggingConfigurationTests.cs index be1e33f5..edae0876 100644 --- a/GFramework.Core.Tests/Logging/LoggingConfigurationTests.cs +++ b/GFramework.Core.Tests/Logging/LoggingConfigurationTests.cs @@ -143,6 +143,35 @@ public class LoggingConfigurationTests Assert.That(logger3.IsInfoEnabled(), Is.True); } + [Test] + public void CreateFactory_WithOverlappingLoggerPrefixes_ShouldPreferLongestPrefixMatch() + { + var json = @"{ + ""minLevel"": ""Info"", + ""appenders"": [ + { + ""type"": ""Console"", + ""formatter"": ""Default"" + } + ], + ""loggerLevels"": { + ""GFramework"": ""Warning"", + ""GFramework.Core"": ""Trace"" + } + }"; + + var config = LoggingConfigurationLoader.LoadFromJsonString(json); + var factory = LoggingConfigurationLoader.CreateFactory(config); + + var logger = factory.GetLogger("GFramework.Core.Logging"); + + Assert.Multiple(() => + { + Assert.That(logger.IsTraceEnabled(), Is.True); + Assert.That(logger.IsDebugEnabled(), Is.True); + }); + } + [Test] public void CreateFactory_WithInvalidAppenderType_ShouldThrowException() { @@ -308,4 +337,4 @@ public class LoggingConfigurationTests Assert.That(config.Appenders[2].MaxFileSize, Is.EqualTo(10485760)); Assert.That(config.LoggerLevels.Count, Is.EqualTo(3)); } -} \ No newline at end of file +} diff --git a/GFramework.Core/Coroutine/Instructions/WaitForTask.cs b/GFramework.Core/Coroutine/Instructions/WaitForTask.cs index f8e09cf1..d214d259 100644 --- a/GFramework.Core/Coroutine/Instructions/WaitForTask.cs +++ b/GFramework.Core/Coroutine/Instructions/WaitForTask.cs @@ -2,9 +2,6 @@ using GFramework.Core.Abstractions.Coroutine; namespace GFramework.Core.Coroutine.Instructions; -/// -/// 等待Task完成的等待指令 -/// /// /// 等待Task完成的等待指令 /// diff --git a/GFramework.Core/Extensions/NumericExtensions.cs b/GFramework.Core/Extensions/NumericExtensions.cs index eaa079e8..31e0b471 100644 --- a/GFramework.Core/Extensions/NumericExtensions.cs +++ b/GFramework.Core/Extensions/NumericExtensions.cs @@ -25,6 +25,10 @@ public static class NumericExtensions /// public static bool Between(this T value, T min, T max, bool inclusive = true) where T : IComparable { + ArgumentNullException.ThrowIfNull(value); + ArgumentNullException.ThrowIfNull(min); + ArgumentNullException.ThrowIfNull(max); + if (min.CompareTo(max) > 0) throw new ArgumentException($"最小值 ({min}) 不能大于最大值 ({max})", nameof(min)); diff --git a/GFramework.Core/Functional/Result.cs b/GFramework.Core/Functional/Result.cs index 7701eda8..034a4c5d 100644 --- a/GFramework.Core/Functional/Result.cs +++ b/GFramework.Core/Functional/Result.cs @@ -125,7 +125,10 @@ public readonly struct Result : IEquatable if (_isSuccess) return true; - return _exception!.GetType() == other._exception!.GetType() && + if (_exception is null || other._exception is null) + return _exception is null && other._exception is null; + + return _exception.GetType() == other._exception.GetType() && string.Equals(_exception.Message, other._exception.Message, StringComparison.Ordinal); } @@ -144,7 +147,12 @@ public readonly struct Result : IEquatable [Pure] public override int GetHashCode() { - return _isSuccess ? 1 : HashCode.Combine(_exception!.GetType(), _exception.Message); + if (_isSuccess) + return 1; + + return _exception is null + ? 0 + : HashCode.Combine(_exception.GetType(), _exception.Message); } /// @@ -171,7 +179,7 @@ public readonly struct Result : IEquatable /// Result 的字符串表示 [Pure] public override string ToString() => - _isSuccess ? "Success" : $"Fail({_exception!.Message})"; + _isSuccess ? "Success" : (_exception != null ? $"Fail({_exception.Message})" : "Fail(null)"); /// /// 尝试执行一个无返回值的操作,并根据执行结果返回成功或失败的 Result diff --git a/GFramework.Core/Functional/ResultExtensions.cs b/GFramework.Core/Functional/ResultExtensions.cs index 3d9d7d2d..650b4577 100644 --- a/GFramework.Core/Functional/ResultExtensions.cs +++ b/GFramework.Core/Functional/ResultExtensions.cs @@ -177,7 +177,7 @@ public static class ResultExtensions return result.Match( succ: value => predicate(value) ? result - : Result.Fail(new ArgumentException(errorMessage)), + : Result.Fail(new ArgumentException(errorMessage, nameof(result))), fail: _ => result ); } diff --git a/GFramework.Core/Logging/AppenderConfiguration.cs b/GFramework.Core/Logging/AppenderConfiguration.cs index 5922ee8b..0528b111 100644 --- a/GFramework.Core/Logging/AppenderConfiguration.cs +++ b/GFramework.Core/Logging/AppenderConfiguration.cs @@ -1,5 +1,3 @@ -using GFramework.Core.Abstractions.Logging; - namespace GFramework.Core.Logging; /// diff --git a/GFramework.Core/Logging/ConfigurableLoggerFactory.cs b/GFramework.Core/Logging/ConfigurableLoggerFactory.cs index 9bea072f..18c3400c 100644 --- a/GFramework.Core/Logging/ConfigurableLoggerFactory.cs +++ b/GFramework.Core/Logging/ConfigurableLoggerFactory.cs @@ -10,7 +10,7 @@ internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable { private readonly ILogAppender[] _appenders; private readonly LoggingConfiguration _config; - private bool _disposed; + private int _disposed; /// /// 初始化一个基于日志配置创建输出管线的工厂实例。 @@ -27,7 +27,7 @@ internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable /// public void Dispose() { - if (_disposed) + if (Interlocked.Exchange(ref _disposed, 1) != 0) { return; } @@ -40,7 +40,6 @@ internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable } } - _disposed = true; } /// @@ -52,15 +51,24 @@ internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info) { var effectiveLevel = _config.MinLevel; + var bestMatchLength = -1; foreach (var kvp in _config.LoggerLevels) { - if (string.Equals(name, kvp.Key, StringComparison.Ordinal) || - name.StartsWith(kvp.Key + ".", StringComparison.Ordinal)) + var isExactMatch = string.Equals(name, kvp.Key, StringComparison.Ordinal); + if (isExactMatch) { effectiveLevel = kvp.Value; break; } + + var isPrefixMatch = name.StartsWith(kvp.Key + ".", StringComparison.Ordinal); + if (isPrefixMatch && kvp.Key.Length > bestMatchLength) + { + // 多个命名空间前缀都能命中时,最长前缀代表最具体的配置。 + bestMatchLength = kvp.Key.Length; + effectiveLevel = kvp.Value; + } } if (_appenders.Length == 0) diff --git a/GFramework.Core/Logging/Filters/SamplingFilter.cs b/GFramework.Core/Logging/Filters/SamplingFilter.cs index 0d170ef3..20f2965a 100644 --- a/GFramework.Core/Logging/Filters/SamplingFilter.cs +++ b/GFramework.Core/Logging/Filters/SamplingFilter.cs @@ -69,7 +69,10 @@ public sealed class SamplingFilter : ILogFilter foreach (var kvp in _samplingStates) { - if (string.Equals(kvp.Key, "*", StringComparison.Ordinal)) continue; // 不清理共享状态 + if (string.Equals(kvp.Key, "*", StringComparison.Ordinal)) + { + continue; // 不清理共享状态 + } if (kvp.Value.IsStale(now, staleThreshold)) { diff --git a/GFramework.Core/Resource/ResourceManager.cs b/GFramework.Core/Resource/ResourceManager.cs index ebd30bc7..52e3a253 100644 --- a/GFramework.Core/Resource/ResourceManager.cs +++ b/GFramework.Core/Resource/ResourceManager.cs @@ -82,8 +82,14 @@ public class ResourceManager : IResourceManager } /// - /// 异步加载资源 + /// 异步加载指定路径的资源,并在缓存中进行并发去重。 /// + /// 资源类型 + /// 资源路径,不能为空或空白。 + /// 加载成功返回资源实例;加载失败返回 + /// 为空或空白时抛出。 + /// 当未注册对应资源加载器时抛出。 + /// 内部使用 ConfigureAwait(false),后续延续不保证回到调用线程。 public async Task LoadAsync(string path) where T : class { if (string.IsNullOrWhiteSpace(path)) @@ -227,8 +233,14 @@ public class ResourceManager : IResourceManager } /// - /// 预加载资源 + /// 预加载资源到缓存中。 /// + /// 资源类型 + /// 资源路径,不能为空或空白。 + /// 表示预加载流程完成的任务。 + /// 为空或空白时抛出。 + /// 当未注册对应资源加载器时抛出。 + /// 内部委托给 ,同样不捕获同步上下文。 public async Task PreloadAsync(string path) where T : class { await LoadAsync(path).ConfigureAwait(false); diff --git a/GFramework.Cqrs/Internal/WeakKeyCache.cs b/GFramework.Cqrs/Internal/WeakKeyCache.cs index 2d97bd3d..54241dda 100644 --- a/GFramework.Cqrs/Internal/WeakKeyCache.cs +++ b/GFramework.Cqrs/Internal/WeakKeyCache.cs @@ -25,10 +25,8 @@ internal sealed class WeakKeyCache /// 缓存键。 /// 创建缓存值的工厂方法。 /// 已存在或新创建的缓存值。 - /// - /// 。 - /// 或 返回 。 - /// + /// + /// 返回 public TValue GetOrAdd(TKey key, Func valueFactory) { ArgumentNullException.ThrowIfNull(key); @@ -59,10 +57,8 @@ internal sealed class WeakKeyCache /// 创建缓存值时复用的附加状态。 /// 基于键与附加状态创建缓存值的工厂方法。 /// 已存在或新创建的缓存值。 - /// - /// 。 - /// 或 返回 。 - /// + /// + /// 返回 public TValue GetOrAdd(TKey key, TState state, Func valueFactory) { ArgumentNullException.ThrowIfNull(key); diff --git a/GFramework.Cqrs/Internal/WeakTypePairCache.cs b/GFramework.Cqrs/Internal/WeakTypePairCache.cs index 4742a908..10aff72b 100644 --- a/GFramework.Cqrs/Internal/WeakTypePairCache.cs +++ b/GFramework.Cqrs/Internal/WeakTypePairCache.cs @@ -8,6 +8,8 @@ namespace GFramework.Cqrs.Internal; /// /// 第一层和第二层键都使用弱键缓存,因此只要任一类型不再被外部引用, /// 对应条目都允许被 GC 清理,并在后续首次访问时重新建立。 +/// 线程安全:该类型支持并发访问,读写与清理由底层弱键缓存实现保证一致性。 +/// 失败模式:键被 GC 回收或调用 后,后续读取可能未命中并触发重建。 /// internal sealed class WeakTypePairCache where TValue : class @@ -25,6 +27,7 @@ internal sealed class WeakTypePairCache /// 或 /// 。 /// + /// 返回 public TValue GetOrAdd(Type primaryType, Type secondaryType, Func valueFactory) { ArgumentNullException.ThrowIfNull(primaryType); diff --git a/GFramework.Game.Abstractions/Setting/IApplyAbleSettings.cs b/GFramework.Game.Abstractions/Setting/IApplyAbleSettings.cs index bf0d43f0..2f5f199d 100644 --- a/GFramework.Game.Abstractions/Setting/IApplyAbleSettings.cs +++ b/GFramework.Game.Abstractions/Setting/IApplyAbleSettings.cs @@ -6,7 +6,11 @@ public interface IApplyAbleSettings : ISettingsSection { /// - /// 应用当前设置到系统中 + /// 异步应用当前设置到目标系统中。 /// - Task Apply(); -} \ No newline at end of file + /// + /// 表示应用流程完成的任务。 + /// 对于仅执行同步引擎调用的实现,可以返回已完成任务。 + /// + Task ApplyAsync(); +} diff --git a/GFramework.Game.Tests/Setting/GodotLocalizationSettingsTests.cs b/GFramework.Game.Tests/Setting/GodotLocalizationSettingsTests.cs index 2d651627..b431867b 100644 --- a/GFramework.Game.Tests/Setting/GodotLocalizationSettingsTests.cs +++ b/GFramework.Game.Tests/Setting/GodotLocalizationSettingsTests.cs @@ -13,7 +13,7 @@ namespace GFramework.Game.Tests.Setting; public sealed class GodotLocalizationSettingsTests { [Test] - public async Task Apply_ShouldSyncEnglishToGodotLocaleAndFrameworkLanguage() + public async Task ApplyAsync_ShouldSyncEnglishToGodotLocaleAndFrameworkLanguage() { var manager = new Mock(MockBehavior.Strict); manager.Setup(it => it.SetLanguage("eng")); @@ -21,14 +21,14 @@ public sealed class GodotLocalizationSettingsTests var applicator = CreateApplicator("English", manager.Object, locale => appliedLocale = locale); - await applicator.Apply(); + await applicator.ApplyAsync(); Assert.That(appliedLocale, Is.EqualTo("en")); manager.Verify(it => it.SetLanguage("eng"), Times.Once); } [Test] - public async Task Apply_ShouldSyncChineseToGodotLocaleAndFrameworkLanguage() + public async Task ApplyAsync_ShouldSyncChineseToGodotLocaleAndFrameworkLanguage() { var manager = new Mock(MockBehavior.Strict); manager.Setup(it => it.SetLanguage("zhs")); @@ -36,14 +36,14 @@ public sealed class GodotLocalizationSettingsTests var applicator = CreateApplicator("简体中文", manager.Object, locale => appliedLocale = locale); - await applicator.Apply(); + await applicator.ApplyAsync(); Assert.That(appliedLocale, Is.EqualTo("zh_CN")); manager.Verify(it => it.SetLanguage("zhs"), Times.Once); } [Test] - public async Task Apply_ShouldFallbackUnknownLanguageToEnglish() + public async Task ApplyAsync_ShouldFallbackUnknownLanguageToEnglish() { var manager = new Mock(MockBehavior.Strict); manager.Setup(it => it.SetLanguage("eng")); @@ -51,7 +51,7 @@ public sealed class GodotLocalizationSettingsTests var applicator = CreateApplicator("Esperanto", manager.Object, locale => appliedLocale = locale); - await applicator.Apply(); + await applicator.ApplyAsync(); Assert.That(appliedLocale, Is.EqualTo("en")); manager.Verify(it => it.SetLanguage("eng"), Times.Once); @@ -71,4 +71,4 @@ public sealed class GodotLocalizationSettingsTests return new GodotLocalizationSettings(settingsModel.Object, new LocalizationMap(), () => manager, applyGodotLocale); } -} \ No newline at end of file +} diff --git a/GFramework.Game.Tests/Setting/SettingsSystemTests.cs b/GFramework.Game.Tests/Setting/SettingsSystemTests.cs index 76fe04c5..372985e4 100644 --- a/GFramework.Game.Tests/Setting/SettingsSystemTests.cs +++ b/GFramework.Game.Tests/Setting/SettingsSystemTests.cs @@ -314,7 +314,7 @@ public sealed class SettingsSystemTests } /// - public async Task Apply() + public async Task ApplyAsync() { ApplyCount++; diff --git a/GFramework.Game/Setting/SettingsModel.cs b/GFramework.Game/Setting/SettingsModel.cs index 1c8f8915..3a187351 100644 --- a/GFramework.Game/Setting/SettingsModel.cs +++ b/GFramework.Game/Setting/SettingsModel.cs @@ -198,7 +198,7 @@ public class SettingsModel(IDataLocationProvider? locationProvider, foreach (var applicator in _applicators) try { - await applicator.Value.Apply(); + await applicator.Value.ApplyAsync(); } catch (Exception ex) { @@ -302,4 +302,4 @@ public class SettingsModel(IDataLocationProvider? locationProvider, return current; } -} \ No newline at end of file +} diff --git a/GFramework.Game/Setting/SettingsSystem.cs b/GFramework.Game/Setting/SettingsSystem.cs index 40515025..6b5b2aed 100644 --- a/GFramework.Game/Setting/SettingsSystem.cs +++ b/GFramework.Game/Setting/SettingsSystem.cs @@ -87,7 +87,7 @@ public class SettingsSystem : AbstractSystem, ISettingsSystem try { - await applyAbleSettings.Apply(); + await applyAbleSettings.ApplyAsync(); // 发送设置应用成功事件 this.SendEvent(new SettingsAppliedEvent(section, true)); } @@ -97,4 +97,4 @@ public class SettingsSystem : AbstractSystem, ISettingsSystem this.SendEvent(new SettingsAppliedEvent(section, false, ex)); } } -} \ No newline at end of file +} diff --git a/GFramework.Godot/Architectures/AbstractArchitecture.cs b/GFramework.Godot/Architectures/AbstractArchitecture.cs index aebb2df5..621e6a9a 100644 --- a/GFramework.Godot/Architectures/AbstractArchitecture.cs +++ b/GFramework.Godot/Architectures/AbstractArchitecture.cs @@ -85,7 +85,7 @@ public abstract class AbstractArchitecture( Name = _architectureAnchorName }; - _anchor.Bind(() => _ = DestroyAsync().AsTask()); + _anchor.Bind(ObserveDestroyAsync); tree.Root.CallDeferred(Node.MethodName.AddChild, _anchor); } @@ -97,16 +97,23 @@ public abstract class AbstractArchitecture( /// 模块类型,必须实现IGodotModule接口 /// 要安装的模块实例 /// 异步任务 + /// 时抛出。 + /// 当架构锚点尚未初始化时抛出。 + /// + /// 该方法会等待锚点进入场景树后再继续执行附加回调,避免模块在非主线程或未就绪状态下访问 Godot 节点 API。 + /// protected async Task InstallGodotModule(TModule module) where TModule : IGodotModule { + ArgumentNullException.ThrowIfNull(module); + module.Install(this); // 检查锚点是否已初始化,未初始化则抛出异常 if (_anchor == null) throw new InvalidOperationException("Anchor not initialized"); - // 等待锚点准备就绪 - await _anchor.WaitUntilReadyAsync().ConfigureAwait(false); + // 等待锚点准备就绪,并保持 Godot 同步上下文,以便后续附加逻辑安全访问节点 API。 + await _anchor.WaitUntilReadyAsync(); // 延迟调用将扩展节点添加为锚点的子节点 _anchor.CallDeferred(Node.MethodName.AddChild, module.Node); @@ -138,4 +145,32 @@ public abstract class AbstractArchitecture( await base.DestroyAsync().ConfigureAwait(false); } + + /// + /// 观察架构异步销毁流程,确保退出树时触发的 fire-and-forget 清理失败可见。 + /// + /// + /// Godot 的 回调是同步入口,无法直接等待异步销毁完成; + /// 因此这里显式附加错误观察器,把异常写入 Godot 错误输出,避免未观测任务异常被静默吞掉。 + /// + private void ObserveDestroyAsync() + { + _ = ObserveDestroyCoreAsync(); + } + + /// + /// 执行并观察异步销毁流程。 + /// + /// 表示观察任务本身完成的任务。 + private async Task ObserveDestroyCoreAsync() + { + try + { + await DestroyAsync(); + } + catch (Exception ex) + { + GD.PushError($"Architecture destruction failed: {ex}"); + } + } } diff --git a/GFramework.Godot/Config/GodotYamlConfigEnvironment.cs b/GFramework.Godot/Config/GodotYamlConfigEnvironment.cs index 79a40390..ec98e9b7 100644 --- a/GFramework.Godot/Config/GodotYamlConfigEnvironment.cs +++ b/GFramework.Godot/Config/GodotYamlConfigEnvironment.cs @@ -156,8 +156,28 @@ internal sealed class GodotYamlConfigEnvironment private static byte[] ReadAllBytesCore(string path) { - return path.IsGodotPath() - ? FileAccess.GetFileAsBytes(path) - : File.ReadAllBytes(path); + if (!path.IsGodotPath()) + { + return File.ReadAllBytes(path); + } + + var bytes = FileAccess.GetFileAsBytes(path); + var error = FileAccess.GetOpenError(); + if (error == Error.Ok) + { + return bytes; + } + + throw CreateReadException(path, error); + } + + private static Exception CreateReadException(string path, Error error) + { + return error switch + { + Error.FileNotFound => new FileNotFoundException($"Godot file not found: {path}", path), + Error.FileCantOpen => new IOException($"Godot could not open file '{path}'. Error: {error}"), + _ => new IOException($"Godot failed to read file '{path}'. Error: {error}") + }; } } diff --git a/GFramework.Godot/Scene/SceneBehaviorBase.cs b/GFramework.Godot/Scene/SceneBehaviorBase.cs index e3a0b083..bfee418f 100644 --- a/GFramework.Godot/Scene/SceneBehaviorBase.cs +++ b/GFramework.Godot/Scene/SceneBehaviorBase.cs @@ -144,7 +144,7 @@ public abstract class SceneBehaviorBase : ISceneBehavior public virtual async ValueTask OnPauseAsync() { if (_scene != null) - await _scene.OnPauseAsync().ConfigureAwait(false); + await _scene.OnPauseAsync(); // 暂停处理 Owner.SetProcess(false); @@ -165,7 +165,7 @@ public abstract class SceneBehaviorBase : ISceneBehavior return; if (_scene != null) - await _scene.OnResumeAsync().ConfigureAwait(false); + await _scene.OnResumeAsync(); // 恢复处理 Owner.SetProcess(true); @@ -198,7 +198,7 @@ public abstract class SceneBehaviorBase : ISceneBehavior public virtual async ValueTask OnUnloadAsync() { if (_scene != null) - await _scene.OnUnloadAsync().ConfigureAwait(false); + await _scene.OnUnloadAsync(); // 释放节点 Owner.QueueFreeX(); diff --git a/GFramework.Godot/Setting/GodotAudioSettings.cs b/GFramework.Godot/Setting/GodotAudioSettings.cs index 41abc764..7d7d1d7d 100644 --- a/GFramework.Godot/Setting/GodotAudioSettings.cs +++ b/GFramework.Godot/Setting/GodotAudioSettings.cs @@ -16,8 +16,8 @@ public class GodotAudioSettings(ISettingsModel model, AudioBusMap audioBusMap) /// /// 应用音频设置到Godot音频系统 /// - /// 表示异步操作的任务 - public Task Apply() + /// 已完成的任务;该实现只执行同步音频总线更新。 + public Task ApplyAsync() { var settings = model.GetData(); SetBus(audioBusMap.Master, settings.MasterVolume); @@ -67,4 +67,4 @@ public class GodotAudioSettings(ISettingsModel model, AudioBusMap audioBusMap) Mathf.LinearToDb(Mathf.Clamp(linear, 0.0001f, 1f)) ); } -} \ No newline at end of file +} diff --git a/GFramework.Godot/Setting/GodotGraphicsSettings.cs b/GFramework.Godot/Setting/GodotGraphicsSettings.cs index 6f2db267..6f0a3459 100644 --- a/GFramework.Godot/Setting/GodotGraphicsSettings.cs +++ b/GFramework.Godot/Setting/GodotGraphicsSettings.cs @@ -11,10 +11,10 @@ namespace GFramework.Godot.Setting; public class GodotGraphicsSettings(ISettingsModel model) : IResetApplyAbleSettings { /// - /// 应用图形设置到Godot引擎 + /// 将当前图形设置同步应用到 Godot 引擎。 /// - /// 异步任务 - public Task Apply() + /// 已完成的任务;该实现只执行同步引擎调用,不会启动后台异步工作。 + public Task ApplyAsync() { var settings = model.GetData(); // 创建分辨率向量 diff --git a/GFramework.Godot/Setting/GodotLocalizationSettings.cs b/GFramework.Godot/Setting/GodotLocalizationSettings.cs index ddcfd6fc..e34064d5 100644 --- a/GFramework.Godot/Setting/GodotLocalizationSettings.cs +++ b/GFramework.Godot/Setting/GodotLocalizationSettings.cs @@ -77,8 +77,8 @@ public class GodotLocalizationSettings : IResetApplyAbleSettings /// /// 应用本地化设置到 Godot 引擎与 GFramework 本地化管理器。 /// - /// 完成的任务 - public Task Apply() + /// 已完成的任务;该实现通过同步 API 推进语言切换。 + public Task ApplyAsync() { var settings = _model.GetData(); var locale = _localizationMap.ResolveGodotLocale(settings.Language); @@ -122,4 +122,4 @@ public class GodotLocalizationSettings : IResetApplyAbleSettings return null; } } -} \ No newline at end of file +} diff --git a/GFramework.Godot/UI/UiPageBehaviorFactory.cs b/GFramework.Godot/UI/UiPageBehaviorFactory.cs index 8b5f8319..3737e466 100644 --- a/GFramework.Godot/UI/UiPageBehaviorFactory.cs +++ b/GFramework.Godot/UI/UiPageBehaviorFactory.cs @@ -30,6 +30,7 @@ public static class UiPageBehaviorFactory /// UI 标识键 /// 目标层级 /// 对应层级的 IUiPageBehavior 实例 + /// 不是受支持的 UI 层级时抛出。 public static IUiPageBehavior Create(T owner, string key, UiLayer layer) where T : CanvasItem { diff --git a/docs/zh-CN/game/setting.md b/docs/zh-CN/game/setting.md index 33a4313d..619146db 100644 --- a/docs/zh-CN/game/setting.md +++ b/docs/zh-CN/game/setting.md @@ -72,7 +72,7 @@ public interface ISettingsModel : IModel - `RegisterApplicator()` 注册应用器,并把其 `Data` 纳入模型管理 - `InitializeAsync()` 从 `ISettingsDataRepository` 读取所有已注册设置,并在需要时执行迁移 - `SaveAllAsync()` 持久化当前所有设置数据 -- `ApplyAllAsync()` 依次调用所有 applicator 的 `Apply()` +- `ApplyAllAsync()` 依次调用所有 applicator 的 `ApplyAsync()` ## SettingsSystem @@ -139,7 +139,7 @@ public sealed class GameplaySettingsApplicator : IResetApplyAbleSettings Data.Reset(); } - public Task Apply() + public Task ApplyAsync() { var settings = (GameplaySettings)Data; TimeScale.Current = settings.GameSpeed; diff --git a/docs/zh-CN/godot/setting.md b/docs/zh-CN/godot/setting.md index 5a323626..f1c75ae5 100644 --- a/docs/zh-CN/godot/setting.md +++ b/docs/zh-CN/godot/setting.md @@ -44,7 +44,7 @@ GodotAudioSettings (Godot 特定实现) → IApplyAbleSettings (可应用设置 **功能:** - 接收 AudioSettings 配置对象和 AudioBusMap 总线映射 -- 实现 Apply() 方法,将音量设置应用到指定音频总线 +- 实现 `ApplyAsync()` 方法,将音量设置应用到指定音频总线 - 支持自定义音频总线映射 - 自动处理音量格式转换(线性值到分贝) @@ -106,7 +106,7 @@ graph TD F --> L[TranslationServer API] F --> M[ILocalizationManager] - N[SettingsSystem] --> O[Apply Method] + N[SettingsSystem] --> O[ApplyAsync Method] O --> B O --> D O --> F @@ -131,7 +131,7 @@ var settings = new AudioSettings var audioSettings = new GodotAudioSettings(settings, new AudioBusMap()); // 应用设置 -audioSettings.Apply(); +await audioSettings.ApplyAsync(); ``` #### 自定义音频总线映射 @@ -155,7 +155,7 @@ var settings = new AudioSettings // 使用自定义总线映射应用设置 var audioSettings = new GodotAudioSettings(settings, customBusMap); -await audioSettings.Apply(); +await audioSettings.ApplyAsync(); ``` #### 通过设置系统使用 @@ -170,7 +170,7 @@ audioSettingsData.SfxVolume = 0.9f; // 创建 Godot 音频设置应用器 var godotAudioSettings = new GodotAudioSettings(audioSettingsData, new AudioBusMap()); -await godotAudioSettings.Apply(); +await godotAudioSettings.ApplyAsync(); ``` ### 图形设置配置 @@ -187,7 +187,7 @@ var graphicsSettings = new GodotGraphicsSettings }; // 应用设置 -await graphicsSettings.Apply(); +await graphicsSettings.ApplyAsync(); ``` #### 窗口模式切换 @@ -205,7 +205,7 @@ public class DisplayManager : Node public async Task ToggleFullscreen() { _graphicsSettings.Fullscreen = !_graphicsSettings.Fullscreen; - await _graphicsSettings.Apply(); + await _graphicsSettings.ApplyAsync(); } public async Task SetResolution(int width, int height) @@ -213,7 +213,7 @@ public class DisplayManager : Node _graphicsSettings.ResolutionWidth = width; _graphicsSettings.ResolutionHeight = height; _graphicsSettings.Fullscreen = false; // 窗口化时自动关闭全屏 - await _graphicsSettings.Apply(); + await _graphicsSettings.ApplyAsync(); } } ``` @@ -237,7 +237,7 @@ public class ResolutionPresets settings.ResolutionWidth = width; settings.ResolutionHeight = height; settings.Fullscreen = false; - await settings.Apply(); + await settings.ApplyAsync(); } } ``` @@ -266,7 +266,7 @@ public sealed class AudioBusMap ```csharp public class GodotAudioSettings(AudioSettings settings, AudioBusMap busMap) : IApplyAbleSettings { - public Task Apply(); + public Task ApplyAsync(); } ``` @@ -278,7 +278,7 @@ public class GodotAudioSettings(AudioSettings settings, AudioBusMap busMap) : IA **Apply 方法实现:** ```csharp -public Task Apply() +public Task ApplyAsync() { SetBus(busMap.Master, settings.MasterVolume); SetBus(busMap.Bgm, settings.BgmVolume); @@ -292,7 +292,7 @@ public Task Apply() ```csharp public class GodotGraphicsSettings : GraphicsSettings, IApplyAbleSettings { - public Task Apply(); + public Task ApplyAsync(); } ``` @@ -382,7 +382,7 @@ public class AudioManager : Node { var settings = new AudioSettings { MasterVolume = linearVolume }; var audioSettings = new GodotAudioSettings(settings, new AudioBusMap()); - audioSettings.Apply(); + await audioSettings.ApplyAsync(); } } @@ -423,7 +423,7 @@ public class CustomAudioManager : Node { var audioSettingsData = new AudioSettings { MasterVolume = linearVolume }; var audioSettings = new GodotAudioSettings(audioSettingsData, _customBusMap); - audioSettings.Apply(); + await audioSettings.ApplyAsync(); } } ``` @@ -522,7 +522,7 @@ public class GraphicsSettingsManager : Node public async Task ApplyAndSave() { - await _settings.Apply(); + await _settings.ApplyAsync(); SaveSettings(); } } From 05de6d1e15d4453fd6f939a0548f0955309e3698 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sat, 18 Apr 2026 20:07:07 +0800 Subject: [PATCH 4/7] =?UTF-8?q?fix(review-followup):=20=E4=BF=AE=E5=A4=8DG?= =?UTF-8?q?odot=E5=AE=89=E8=A3=85=E9=A1=BA=E5=BA=8F=E4=B8=8E=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E5=B7=A5=E5=8E=82=E9=98=B2=E5=BE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 AbstractArchitecture 在锚点未初始化时先执行模块安装的顺序问题,并收紧 GodotYamlConfigEnvironment 的目录枚举异常处理 - 修复 ConfigurableLoggerFactory 对 null 集合、调用方 minLevel 与 AsyncLogAppender 释放路径的处理 - 补充 WeakTypePairCache 与 GodotLocalizationSettingsTests 的 XML 文档,并新增日志工厂回归测试 --- .../Logging/ConfigurableLoggerFactoryTests.cs | 117 ++++++++++++++++++ .../Logging/ConfigurableLoggerFactory.cs | 24 +++- GFramework.Cqrs/Internal/WeakTypePairCache.cs | 3 + .../Setting/GodotLocalizationSettingsTests.cs | 12 ++ .../Architectures/AbstractArchitecture.cs | 11 +- .../Config/GodotYamlConfigEnvironment.cs | 58 ++++++--- 6 files changed, 197 insertions(+), 28 deletions(-) create mode 100644 GFramework.Core.Tests/Logging/ConfigurableLoggerFactoryTests.cs diff --git a/GFramework.Core.Tests/Logging/ConfigurableLoggerFactoryTests.cs b/GFramework.Core.Tests/Logging/ConfigurableLoggerFactoryTests.cs new file mode 100644 index 00000000..d712cad4 --- /dev/null +++ b/GFramework.Core.Tests/Logging/ConfigurableLoggerFactoryTests.cs @@ -0,0 +1,117 @@ +using System.Reflection; +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Logging; +using GFramework.Core.Logging.Appenders; + +namespace GFramework.Core.Tests.Logging; + +/// +/// 验证可配置 Logger 工厂在配置归一化、级别合并与释放路径上的行为契约。 +/// +[TestFixture] +public sealed class ConfigurableLoggerFactoryTests +{ + /// + /// 验证当反序列化结果把集合字段写成 时,工厂会将其归一化为空集合而不是抛出空引用异常。 + /// + [Test] + public void CreateFactory_ShouldNormalizeNullCollectionsFromConfiguration() + { + var config = LoggingConfigurationLoader.LoadFromJsonString( + """ + { + "minLevel": "Warning", + "appenders": null, + "loggerLevels": null + } + """); + + var factory = LoggingConfigurationLoader.CreateFactory(config); + var logger = factory.GetLogger("TestLogger"); + + Assert.Multiple(() => + { + Assert.That(config.Appenders, Is.Not.Null); + Assert.That(config.LoggerLevels, Is.Not.Null); + Assert.That(logger.IsInfoEnabled(), Is.False); + Assert.That(logger.IsWarnEnabled(), Is.True); + }); + } + + /// + /// 验证调用方传入的默认最小级别会作为配置级别的下限参与最终 logger 级别计算。 + /// + [Test] + public void GetLogger_ShouldHonorStricterCallerMinLevelWhenNoOverrideMatches() + { + var config = LoggingConfigurationLoader.LoadFromJsonString( + """ + { + "minLevel": "Info", + "appenders": [ + { + "type": "Console", + "formatter": "Default", + "useColors": false + } + ] + } + """); + + var factory = LoggingConfigurationLoader.CreateFactory(config); + var logger = factory.GetLogger("TestLogger", LogLevel.Warning); + + Assert.Multiple(() => + { + Assert.That(logger.IsInfoEnabled(), Is.False); + Assert.That(logger.IsWarnEnabled(), Is.True); + }); + } + + /// + /// 验证工厂释放时会兼容释放未实现 。 + /// + [Test] + public void Dispose_ShouldDisposeAsyncLogAppenderCreatedFromConfiguration() + { + var config = LoggingConfigurationLoader.LoadFromJsonString( + """ + { + "appenders": [ + { + "type": "Async", + "bufferSize": 8, + "innerAppender": { + "type": "Console", + "formatter": "Default", + "useColors": false + } + } + ] + } + """); + + var factory = LoggingConfigurationLoader.CreateFactory(config); + var logger = factory.GetLogger("AsyncLogger"); + var asyncAppender = GetSingleAsyncAppender(factory); + + logger.Info("dispose-path"); + + ((IDisposable)factory).Dispose(); + + Assert.That(asyncAppender.IsCompleted, Is.True); + } + + private static AsyncLogAppender GetSingleAsyncAppender(ILoggerFactory factory) + { + var appendersField = factory.GetType().GetField("_appenders", BindingFlags.Instance | BindingFlags.NonPublic); + Assert.That(appendersField, Is.Not.Null); + + var appenders = appendersField!.GetValue(factory) as ILogAppender[]; + Assert.That(appenders, Is.Not.Null); + Assert.That(appenders, Has.Length.EqualTo(1)); + Assert.That(appenders![0], Is.TypeOf()); + + return (AsyncLogAppender)appenders[0]; + } +} diff --git a/GFramework.Core/Logging/ConfigurableLoggerFactory.cs b/GFramework.Core/Logging/ConfigurableLoggerFactory.cs index 18c3400c..e1c5ab6f 100644 --- a/GFramework.Core/Logging/ConfigurableLoggerFactory.cs +++ b/GFramework.Core/Logging/ConfigurableLoggerFactory.cs @@ -19,7 +19,11 @@ internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable public ConfigurableLoggerFactory(LoggingConfiguration config) { _config = config ?? throw new ArgumentNullException(nameof(config)); - _appenders = config.Appenders.Select(LoggingConfigurationLoader.CreateAppender).ToArray(); + + // 反序列化输入可能显式把集合写成 null,这里统一归一化为可安全枚举的空集合。 + _config.Appenders ??= []; + _config.LoggerLevels ??= new Dictionary(StringComparer.Ordinal); + _appenders = _config.Appenders.Select(LoggingConfigurationLoader.CreateAppender).ToArray(); } /// @@ -34,23 +38,31 @@ internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable foreach (var appender in _appenders) { - if (appender is IDisposable disposable) + switch (appender) { - disposable.Dispose(); + case AsyncLogAppender asyncLogAppender: + asyncLogAppender.Dispose(); + break; + case IDisposable disposable: + disposable.Dispose(); + break; } } - } /// /// 为指定名称创建日志记录器,并应用最匹配的命名空间级别配置。 /// /// 日志记录器名称。 - /// 调用方传入的默认最小级别。 + /// 调用方要求的最小日志级别下限;最终级别不会低于该值。 /// 可写入日志的记录器实例。 + /// + /// 当配置文件与调用方同时提供默认级别时,会取两者中更严格的那一个; + /// 若命中更具体的命名空间级别覆盖,则以该覆盖配置为准。 + /// public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info) { - var effectiveLevel = _config.MinLevel; + var effectiveLevel = _config.MinLevel > minLevel ? _config.MinLevel : minLevel; var bestMatchLength = -1; foreach (var kvp in _config.LoggerLevels) diff --git a/GFramework.Cqrs/Internal/WeakTypePairCache.cs b/GFramework.Cqrs/Internal/WeakTypePairCache.cs index 10aff72b..d3dd5501 100644 --- a/GFramework.Cqrs/Internal/WeakTypePairCache.cs +++ b/GFramework.Cqrs/Internal/WeakTypePairCache.cs @@ -81,6 +81,9 @@ internal sealed class WeakTypePairCache /// 第一段类型键。 /// 第二段类型键。 /// 当前缓存对象,或 + /// + /// 。 + /// /// /// 该入口仅用于测试通过反射观察缓存状态,不应用于运行时代码路径。 /// diff --git a/GFramework.Game.Tests/Setting/GodotLocalizationSettingsTests.cs b/GFramework.Game.Tests/Setting/GodotLocalizationSettingsTests.cs index b431867b..cb452c3d 100644 --- a/GFramework.Game.Tests/Setting/GodotLocalizationSettingsTests.cs +++ b/GFramework.Game.Tests/Setting/GodotLocalizationSettingsTests.cs @@ -12,6 +12,10 @@ namespace GFramework.Game.Tests.Setting; [TestFixture] public sealed class GodotLocalizationSettingsTests { + /// + /// 验证应用英文设置时,会同时同步 Godot locale 与框架语言管理器。 + /// + /// 表示异步断言完成的任务。 [Test] public async Task ApplyAsync_ShouldSyncEnglishToGodotLocaleAndFrameworkLanguage() { @@ -27,6 +31,10 @@ public sealed class GodotLocalizationSettingsTests manager.Verify(it => it.SetLanguage("eng"), Times.Once); } + /// + /// 验证应用简体中文设置时,会同时同步 Godot locale 与框架语言管理器。 + /// + /// 表示异步断言完成的任务。 [Test] public async Task ApplyAsync_ShouldSyncChineseToGodotLocaleAndFrameworkLanguage() { @@ -42,6 +50,10 @@ public sealed class GodotLocalizationSettingsTests manager.Verify(it => it.SetLanguage("zhs"), Times.Once); } + /// + /// 验证未知语言会回退到英文 locale,并同步默认框架语言代码。 + /// + /// 表示异步断言完成的任务。 [Test] public async Task ApplyAsync_ShouldFallbackUnknownLanguageToEnglish() { diff --git a/GFramework.Godot/Architectures/AbstractArchitecture.cs b/GFramework.Godot/Architectures/AbstractArchitecture.cs index 621e6a9a..d92ea1ef 100644 --- a/GFramework.Godot/Architectures/AbstractArchitecture.cs +++ b/GFramework.Godot/Architectures/AbstractArchitecture.cs @@ -106,17 +106,16 @@ public abstract class AbstractArchitecture( { ArgumentNullException.ThrowIfNull(module); + // 先确认锚点可用,避免模块安装产生副作用后再因架构未绑定场景树而失败。 + var anchor = _anchor ?? throw new InvalidOperationException("Anchor not initialized"); + module.Install(this); - // 检查锚点是否已初始化,未初始化则抛出异常 - if (_anchor == null) - throw new InvalidOperationException("Anchor not initialized"); - // 等待锚点准备就绪,并保持 Godot 同步上下文,以便后续附加逻辑安全访问节点 API。 - await _anchor.WaitUntilReadyAsync(); + await anchor.WaitUntilReadyAsync(); // 延迟调用将扩展节点添加为锚点的子节点 - _anchor.CallDeferred(Node.MethodName.AddChild, module.Node); + anchor.CallDeferred(Node.MethodName.AddChild, module.Node); // 调用扩展的附加回调方法 module.OnAttach(this); diff --git a/GFramework.Godot/Config/GodotYamlConfigEnvironment.cs b/GFramework.Godot/Config/GodotYamlConfigEnvironment.cs index ec98e9b7..e77d325e 100644 --- a/GFramework.Godot/Config/GodotYamlConfigEnvironment.cs +++ b/GFramework.Godot/Config/GodotYamlConfigEnvironment.cs @@ -106,17 +106,37 @@ internal sealed class GodotYamlConfigEnvironment { if (!path.IsGodotPath()) { - if (!Directory.Exists(path)) + try + { + if (!Directory.Exists(path)) + { + return null; + } + + return Directory + .EnumerateFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly) + .Select(static entryPath => new GodotYamlConfigDirectoryEntry( + Path.GetFileName(entryPath), + Directory.Exists(entryPath))) + .ToArray(); + } + catch (IOException) + { + // 非 Godot 路径分支与公开契约保持一致:宿主无法访问目录时返回 null,而不是泄漏底层异常。 + return null; + } + catch (UnauthorizedAccessException) + { + return null; + } + catch (ArgumentException) + { + return null; + } + catch (NotSupportedException) { return null; } - - return Directory - .EnumerateFileSystemEntries(path, "*", SearchOption.TopDirectoryOnly) - .Select(static entryPath => new GodotYamlConfigDirectoryEntry( - Path.GetFileName(entryPath), - Directory.Exists(entryPath))) - .ToArray(); } using var directory = DirAccess.Open(path); @@ -132,18 +152,24 @@ internal sealed class GodotYamlConfigEnvironment return null; } - while (true) + try { - var name = directory.GetNext(); - if (string.IsNullOrEmpty(name)) + while (true) { - break; + var name = directory.GetNext(); + if (string.IsNullOrEmpty(name)) + { + break; + } + + entries.Add(new GodotYamlConfigDirectoryEntry(name, directory.CurrentIsDir())); } - - entries.Add(new GodotYamlConfigDirectoryEntry(name, directory.CurrentIsDir())); } - - directory.ListDirEnd(); + finally + { + // 目录枚举句柄必须成对结束,避免未来循环体扩展后在异常路径上遗留引擎状态。 + directory.ListDirEnd(); + } return entries; } From 2c2df5de298e2eb6d6359a8fc196eab373fd5e14 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sat, 18 Apr 2026 20:45:37 +0800 Subject: [PATCH 5/7] =?UTF-8?q?fix(review-followup):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E8=B7=AF=E5=BE=84=E6=B8=85=E7=90=86=E4=B8=8E?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=A5=91=E7=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 Godot 模块在附加流程失败时的登记时机,确保后续销毁仍可感知半安装模块 - 更新 ConfigurableLoggerFactory 的 name 空值校验与 minLevel XML 契约,并用可观察行为替换脆弱的反射测试 - 补充 WeakTypePairCache 热路径注释,并新增 Godot 模块安装顺序回归测试 --- .../Logging/ConfigurableLoggerFactoryTests.cs | 66 +++++++++++++------ .../Logging/ConfigurableLoggerFactory.cs | 6 +- GFramework.Cqrs/Internal/WeakTypePairCache.cs | 2 + ...ractArchitectureModuleInstallationTests.cs | 64 ++++++++++++++++++ .../Architectures/AbstractArchitecture.cs | 6 +- 5 files changed, 120 insertions(+), 24 deletions(-) create mode 100644 GFramework.Godot.Tests/Architectures/AbstractArchitectureModuleInstallationTests.cs diff --git a/GFramework.Core.Tests/Logging/ConfigurableLoggerFactoryTests.cs b/GFramework.Core.Tests/Logging/ConfigurableLoggerFactoryTests.cs index d712cad4..074c9a02 100644 --- a/GFramework.Core.Tests/Logging/ConfigurableLoggerFactoryTests.cs +++ b/GFramework.Core.Tests/Logging/ConfigurableLoggerFactoryTests.cs @@ -1,7 +1,5 @@ -using System.Reflection; using GFramework.Core.Abstractions.Logging; using GFramework.Core.Logging; -using GFramework.Core.Logging.Appenders; namespace GFramework.Core.Tests.Logging; @@ -39,7 +37,7 @@ public sealed class ConfigurableLoggerFactoryTests } /// - /// 验证调用方传入的默认最小级别会作为配置级别的下限参与最终 logger 级别计算。 + /// 验证在未命中命名空间覆盖时,调用方传入的默认最小级别会作为最终 logger 级别的下限参与计算。 /// [Test] public void GetLogger_ShouldHonorStricterCallerMinLevelWhenNoOverrideMatches() @@ -69,7 +67,51 @@ public sealed class ConfigurableLoggerFactoryTests } /// - /// 验证工厂释放时会兼容释放未实现 。 + /// 验证命名空间覆盖级别会优先于调用方传入的默认最小级别,确保覆盖配置保持最高优先级。 + /// + [Test] + public void GetLogger_ShouldPreferNamespaceOverrideOverCallerMinLevel() + { + var config = LoggingConfigurationLoader.LoadFromJsonString( + """ + { + "minLevel": "Info", + "appenders": [ + { + "type": "Console", + "formatter": "Default", + "useColors": false + } + ], + "loggerLevels": { + "MyApp.Services": "Debug" + } + } + """); + + var factory = LoggingConfigurationLoader.CreateFactory(config); + var logger = factory.GetLogger("MyApp.Services.OrderService", LogLevel.Fatal); + + Assert.Multiple(() => + { + Assert.That(logger.IsDebugEnabled(), Is.True); + Assert.That(logger.IsTraceEnabled(), Is.False); + }); + } + + /// + /// 验证调用方传入空 logger 名称时,会得到显式的参数异常而不是后续字符串操作的空引用异常。 + /// + [Test] + public void GetLogger_WithNullName_ShouldThrowArgumentNullException() + { + var factory = LoggingConfigurationLoader.CreateFactory(new LoggingConfiguration()); + + Assert.Throws(() => factory.GetLogger(null!)); + } + + /// + /// 验证工厂释放时会兼容释放未实现 的异步 appender,并让既有 logger 观察到已释放状态。 /// [Test] public void Dispose_ShouldDisposeAsyncLogAppenderCreatedFromConfiguration() @@ -93,25 +135,11 @@ public sealed class ConfigurableLoggerFactoryTests var factory = LoggingConfigurationLoader.CreateFactory(config); var logger = factory.GetLogger("AsyncLogger"); - var asyncAppender = GetSingleAsyncAppender(factory); logger.Info("dispose-path"); ((IDisposable)factory).Dispose(); - Assert.That(asyncAppender.IsCompleted, Is.True); - } - - private static AsyncLogAppender GetSingleAsyncAppender(ILoggerFactory factory) - { - var appendersField = factory.GetType().GetField("_appenders", BindingFlags.Instance | BindingFlags.NonPublic); - Assert.That(appendersField, Is.Not.Null); - - var appenders = appendersField!.GetValue(factory) as ILogAppender[]; - Assert.That(appenders, Is.Not.Null); - Assert.That(appenders, Has.Length.EqualTo(1)); - Assert.That(appenders![0], Is.TypeOf()); - - return (AsyncLogAppender)appenders[0]; + Assert.Throws(() => logger.Info("after-dispose")); } } diff --git a/GFramework.Core/Logging/ConfigurableLoggerFactory.cs b/GFramework.Core/Logging/ConfigurableLoggerFactory.cs index e1c5ab6f..5e4b497e 100644 --- a/GFramework.Core/Logging/ConfigurableLoggerFactory.cs +++ b/GFramework.Core/Logging/ConfigurableLoggerFactory.cs @@ -54,14 +54,16 @@ internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable /// 为指定名称创建日志记录器,并应用最匹配的命名空间级别配置。 /// /// 日志记录器名称。 - /// 调用方要求的最小日志级别下限;最终级别不会低于该值。 + /// 调用方要求的最小日志级别下限;在未命中命名空间覆盖时生效。 /// 可写入日志的记录器实例。 /// /// 当配置文件与调用方同时提供默认级别时,会取两者中更严格的那一个; - /// 若命中更具体的命名空间级别覆盖,则以该覆盖配置为准。 + /// 若命中更具体的命名空间级别覆盖,则以该覆盖配置为准,即使其低于调用方传入的默认下限。 /// public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info) { + ArgumentNullException.ThrowIfNull(name); + var effectiveLevel = _config.MinLevel > minLevel ? _config.MinLevel : minLevel; var bestMatchLength = -1; diff --git a/GFramework.Cqrs/Internal/WeakTypePairCache.cs b/GFramework.Cqrs/Internal/WeakTypePairCache.cs index d3dd5501..f1870545 100644 --- a/GFramework.Cqrs/Internal/WeakTypePairCache.cs +++ b/GFramework.Cqrs/Internal/WeakTypePairCache.cs @@ -34,10 +34,12 @@ internal sealed class WeakTypePairCache ArgumentNullException.ThrowIfNull(secondaryType); ArgumentNullException.ThrowIfNull(valueFactory); + // 第一层按 primaryType 定位或创建二级缓存,避免每次命中都重新分配容器。 var secondaryEntries = _entries.GetOrAdd(primaryType, static _ => new WeakKeyCache()); return secondaryEntries.GetOrAdd( secondaryType, (PrimaryType: primaryType, Factory: valueFactory), + // 使用 static lambda + state 传参,避免热路径上的闭包捕获与额外分配。 static (cachedSecondaryType, state) => state.Factory(state.PrimaryType, cachedSecondaryType)); } diff --git a/GFramework.Godot.Tests/Architectures/AbstractArchitectureModuleInstallationTests.cs b/GFramework.Godot.Tests/Architectures/AbstractArchitectureModuleInstallationTests.cs new file mode 100644 index 00000000..8952a12c --- /dev/null +++ b/GFramework.Godot.Tests/Architectures/AbstractArchitectureModuleInstallationTests.cs @@ -0,0 +1,64 @@ +using GFramework.Core.Abstractions.Architectures; +using GFramework.Godot.Architectures; + +namespace GFramework.Godot.Tests.Architectures; + +/// +/// 验证 Godot 架构在模块安装前会先检查锚点状态,避免未绑定场景树时留下半安装副作用。 +/// +[TestFixture] +public sealed class AbstractArchitectureModuleInstallationTests +{ + /// + /// 验证当锚点尚未初始化时,安装流程会直接失败,且不会执行模块安装逻辑。 + /// + /// 表示异步断言完成的任务。 + [Test] + public async Task InstallGodotModuleAsync_ShouldThrowBeforeInvokingModuleInstall_WhenAnchorIsMissing() + { + var architecture = new TestArchitecture(); + var module = new RecordingGodotModule(); + + var exception = Assert.ThrowsAsync(async () => + await architecture.InstallGodotModuleForTestAsync(module)); + + Assert.Multiple(() => + { + Assert.That(exception, Is.Not.Null); + Assert.That(exception!.Message, Is.EqualTo("Anchor not initialized")); + Assert.That(module.InstallCalled, Is.False); + }); + } + + private sealed class TestArchitecture : AbstractArchitecture + { + protected override void InstallModules() + { + } + + public Task InstallGodotModuleForTestAsync(RecordingGodotModule module) + { + return InstallGodotModule(module); + } + } + + private sealed class RecordingGodotModule : IGodotModule + { + public bool InstallCalled { get; private set; } + + public global::Godot.Node Node => null!; + + public void Install(IArchitecture architecture) + { + InstallCalled = true; + } + + public void OnAttach(GFramework.Core.Architectures.Architecture architecture) + { + } + + public void OnDetach() + { + } + } +} diff --git a/GFramework.Godot/Architectures/AbstractArchitecture.cs b/GFramework.Godot/Architectures/AbstractArchitecture.cs index d92ea1ef..668c84ce 100644 --- a/GFramework.Godot/Architectures/AbstractArchitecture.cs +++ b/GFramework.Godot/Architectures/AbstractArchitecture.cs @@ -111,6 +111,9 @@ public abstract class AbstractArchitecture( module.Install(this); + // 在附加流程完成前先登记模块,保证后续任一步失败时仍能参与架构销毁阶段的清理。 + _extensions.Add(module); + // 等待锚点准备就绪,并保持 Godot 同步上下文,以便后续附加逻辑安全访问节点 API。 await anchor.WaitUntilReadyAsync(); @@ -119,9 +122,6 @@ public abstract class AbstractArchitecture( // 调用扩展的附加回调方法 module.OnAttach(this); - - // 将扩展添加到扩展集合中 - _extensions.Add(module); } From 5046c9752b9651103ee16ae9c7bbef2ae3d7f609 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:09:08 +0800 Subject: [PATCH 6/7] =?UTF-8?q?fix(review-followup):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E9=85=8D=E7=BD=AE=E7=A9=BA=E9=A1=B9=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=E4=B8=8E=E6=96=87=E6=A1=A3=E7=A4=BA=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 ConfigurableLoggerFactory 对 null Appender 配置项的显式校验与 XML 契约 - 补充日志工厂针对 appenders 空项输入的回归测试 - 更新 Godot setting 文档中的 async 示例签名以匹配 ApplyAsync 用法 - 修正 AbstractArchitecture 中 ObserveDestroyCoreAsync 方法里调用await DestroyAsync() 未配置ConfigureAwait(false)的问题 --- .../Logging/ConfigurableLoggerFactoryTests.cs | 18 ++++++++++++++++++ .../Logging/ConfigurableLoggerFactory.cs | 10 +++++++++- .../Architectures/AbstractArchitecture.cs | 4 +--- docs/zh-CN/godot/setting.md | 4 ++-- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/GFramework.Core.Tests/Logging/ConfigurableLoggerFactoryTests.cs b/GFramework.Core.Tests/Logging/ConfigurableLoggerFactoryTests.cs index 074c9a02..b20dd184 100644 --- a/GFramework.Core.Tests/Logging/ConfigurableLoggerFactoryTests.cs +++ b/GFramework.Core.Tests/Logging/ConfigurableLoggerFactoryTests.cs @@ -36,6 +36,24 @@ public sealed class ConfigurableLoggerFactoryTests }); } + /// + /// 验证当配置输入把 appenders 集合中的某个元素反序列化为 时,工厂会抛出可诊断异常。 + /// + [Test] + public void CreateFactory_ShouldThrowInvalidOperationException_WhenAppenderEntryIsNull() + { + var config = LoggingConfigurationLoader.LoadFromJsonString( + """ + { + "appenders": [ null ] + } + """); + + var exception = Assert.Throws(() => LoggingConfigurationLoader.CreateFactory(config)); + + Assert.That(exception!.Message, Is.EqualTo("Appender configuration cannot be null.")); + } + /// /// 验证在未命中命名空间覆盖时,调用方传入的默认最小级别会作为最终 logger 级别的下限参与计算。 /// diff --git a/GFramework.Core/Logging/ConfigurableLoggerFactory.cs b/GFramework.Core/Logging/ConfigurableLoggerFactory.cs index 5e4b497e..4d1fd064 100644 --- a/GFramework.Core/Logging/ConfigurableLoggerFactory.cs +++ b/GFramework.Core/Logging/ConfigurableLoggerFactory.cs @@ -16,6 +16,8 @@ internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable /// 初始化一个基于日志配置创建输出管线的工厂实例。 /// /// 日志配置。 + /// + /// 配置中的某个 Appender 项为 public ConfigurableLoggerFactory(LoggingConfiguration config) { _config = config ?? throw new ArgumentNullException(nameof(config)); @@ -23,7 +25,13 @@ internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable // 反序列化输入可能显式把集合写成 null,这里统一归一化为可安全枚举的空集合。 _config.Appenders ??= []; _config.LoggerLevels ??= new Dictionary(StringComparer.Ordinal); - _appenders = _config.Appenders.Select(LoggingConfigurationLoader.CreateAppender).ToArray(); + + // 外部配置可能把集合项反序列化为 null,这里先给出可诊断异常,避免后续工厂链路出现不清晰的空引用失败。 + _appenders = _config.Appenders + .Select(static appenderConfig => appenderConfig ?? + throw new InvalidOperationException("Appender configuration cannot be null.")) + .Select(LoggingConfigurationLoader.CreateAppender) + .ToArray(); } /// diff --git a/GFramework.Godot/Architectures/AbstractArchitecture.cs b/GFramework.Godot/Architectures/AbstractArchitecture.cs index 668c84ce..1beb8609 100644 --- a/GFramework.Godot/Architectures/AbstractArchitecture.cs +++ b/GFramework.Godot/Architectures/AbstractArchitecture.cs @@ -2,8 +2,6 @@ using GFramework.Core.Abstractions.Environment; using GFramework.Core.Architectures; using GFramework.Core.Constants; -using GFramework.Godot.Extensions; -using Godot; namespace GFramework.Godot.Architectures; @@ -165,7 +163,7 @@ public abstract class AbstractArchitecture( { try { - await DestroyAsync(); + await DestroyAsync().ConfigureAwait(false); } catch (Exception ex) { diff --git a/docs/zh-CN/godot/setting.md b/docs/zh-CN/godot/setting.md index f1c75ae5..8187f303 100644 --- a/docs/zh-CN/godot/setting.md +++ b/docs/zh-CN/godot/setting.md @@ -378,7 +378,7 @@ public class AudioManager : Node ); } - private void SetMasterVolume(float linearVolume) + private async void SetMasterVolume(float linearVolume) { var settings = new AudioSettings { MasterVolume = linearVolume }; var audioSettings = new GodotAudioSettings(settings, new AudioBusMap()); @@ -419,7 +419,7 @@ public class CustomAudioManager : Node ); } - private void SetMasterVolume(float linearVolume) + private async void SetMasterVolume(float linearVolume) { var audioSettingsData = new AudioSettings { MasterVolume = linearVolume }; var audioSettings = new GodotAudioSettings(audioSettingsData, _customBusMap); From adc38cc4f0953f5593c4c95681b73625bf15d66e Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Sat, 18 Apr 2026 21:39:19 +0800 Subject: [PATCH 7/7] =?UTF-8?q?docs(logging):=20=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=B7=A5=E5=8E=82=E6=96=B9=E6=B3=95=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为 ConfigurableLoggerFactory.GetLogger 方法添加 ArgumentNullException 异常说明 - 补充了当 name 参数为 null 时的异常抛出情况 - 完善了方法的 XML 注释文档 --- GFramework.Core/Logging/ConfigurableLoggerFactory.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/GFramework.Core/Logging/ConfigurableLoggerFactory.cs b/GFramework.Core/Logging/ConfigurableLoggerFactory.cs index 4d1fd064..c42b5e61 100644 --- a/GFramework.Core/Logging/ConfigurableLoggerFactory.cs +++ b/GFramework.Core/Logging/ConfigurableLoggerFactory.cs @@ -29,7 +29,8 @@ internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable // 外部配置可能把集合项反序列化为 null,这里先给出可诊断异常,避免后续工厂链路出现不清晰的空引用失败。 _appenders = _config.Appenders .Select(static appenderConfig => appenderConfig ?? - throw new InvalidOperationException("Appender configuration cannot be null.")) + throw new InvalidOperationException( + "Appender configuration cannot be null.")) .Select(LoggingConfigurationLoader.CreateAppender) .ToArray(); } @@ -64,6 +65,7 @@ internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable /// 日志记录器名称。 /// 调用方要求的最小日志级别下限;在未命中命名空间覆盖时生效。 /// 可写入日志的记录器实例。 + /// /// /// 当配置文件与调用方同时提供默认级别时,会取两者中更严格的那一个; /// 若命中更具体的命名空间级别覆盖,则以该覆盖配置为准,即使其低于调用方传入的默认下限。