fix(analyzers): 降低 Core、Cqrs、Godot 与生成器的构建警告

- 清理 GFramework.Core 与 GFramework.Cqrs 中的大量低风险 Meziantou 警告

- 修复 GFramework.Godot 运行时中的 ConfigureAwait、StringComparison 与参数校验告警

- 调整 Core SourceGenerators 中的字符串比较、文件命名与局部长方法问题

- 拆分部分配置与缓存辅助类型文件以消除 file/type mismatch 告警

- 更新 warning reduction 跟踪与执行记录,保留下一批结构性告警的恢复点
This commit is contained in:
GeWuYou 2026-04-18 16:47:44 +08:00
parent 22f271e709
commit 23489570bf
74 changed files with 781 additions and 685 deletions

View File

@ -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;

View File

@ -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;
// 检查是否为泛型方法

View File

@ -179,12 +179,7 @@ public sealed class AutoRegisterModuleGenerator : IIncrementalGenerator
{
var registrations = new List<RegistrationSpec>();
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<AttributeData> 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);

View File

@ -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();
}
/// <summary>
/// 读取枚举扩展生成选项,并在属性未显式指定时回退到契约默认值。
/// </summary>
/// <param name="attribute">待分析的特性数据。</param>
/// <returns>包含各个生成开关的选项元组。</returns>
private static (bool GenerateIsMethods, bool GenerateIsInMethod) GetGenerationOptions(AttributeData attribute)
{
return (
GetNamedBooleanArgument(
attribute,
nameof(GenerateEnumExtensionsAttribute.GenerateIsMethods),
true),
GetNamedBooleanArgument(
attribute,
nameof(GenerateEnumExtensionsAttribute.GenerateIsInMethod),
true));
}
/// <summary>
/// 获取生成文件的提示名称
/// </summary>
@ -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;

View File

@ -250,7 +250,7 @@ public class TestStateMachineSystemV5 : StateMachineSystem
/// 获取状态机内部的状态字典
/// </summary>
/// <returns>类型到状态实例的映射字典</returns>
public Dictionary<Type, IState> GetStates()
public IDictionary<Type, IState> GetStates()
{
return States;
}

View File

@ -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
/// <param name="asyncMode">是否启用异步模式</param>
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
/// </summary>
public virtual async ValueTask DestroyAsync()
{
await _lifecycle.DestroyAsync();
await _lifecycle.DestroyAsync().ConfigureAwait(false);
}
/// <summary>

View File

@ -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(
/// <param name="asyncMode">是否允许异步初始化服务模块。</param>
private async Task InitializeServiceModulesAsync(bool asyncMode)
{
await services.ModuleManager.InitializeAllAsync(asyncMode);
await services.ModuleManager.InitializeAllAsync(asyncMode).ConfigureAwait(false);
}
}

View File

@ -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);
}
/// <summary>
@ -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);
}
/// <summary>
@ -151,7 +151,7 @@ public class ArchitectureContext : IArchitectureContext
CancellationToken cancellationToken = default)
where TCommand : IRequest<Unit>
{
await SendRequestAsync(command, cancellationToken);
await SendRequestAsync(command, cancellationToken).ConfigureAwait(false);
}
/// <summary>
@ -162,7 +162,7 @@ public class ArchitectureContext : IArchitectureContext
IRequest<TResponse> 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<IAsyncQueryExecutor>();
if (asyncQueryBus == null) throw new InvalidOperationException("IAsyncQueryExecutor not registered");
return await asyncQueryBus.SendAsync(query);
return await asyncQueryBus.SendAsync(query).ConfigureAwait(false);
}
/// <summary>
@ -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);
}
/// <summary>
@ -368,7 +368,7 @@ public class ArchitectureContext : IArchitectureContext
ArgumentNullException.ThrowIfNull(command);
var commandBus = GetOrCache<ICommandExecutor>();
if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered");
await commandBus.SendAsync(command);
await commandBus.SendAsync(command).ConfigureAwait(false);
}
/// <summary>
@ -382,7 +382,7 @@ public class ArchitectureContext : IArchitectureContext
ArgumentNullException.ThrowIfNull(command);
var commandBus = GetOrCache<ICommandExecutor>();
if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered");
return await commandBus.SendAsync(command);
return await commandBus.SendAsync(command).ConfigureAwait(false);
}
/// <summary>

View File

@ -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();
}
}
}

View File

@ -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(
/// </summary>
public async ValueTask DestroyAsync()
{
await _disposer.DestroyAsync(CurrentPhase, EnterPhase);
await _disposer.DestroyAsync(CurrentPhase, EnterPhase).ConfigureAwait(false);
}
/// <summary>
@ -285,4 +285,4 @@ internal sealed class ArchitectureLifecycle(
public Task WaitUntilReadyAsync() => _readyTcs.Task;
#endregion
}
}

View File

@ -16,7 +16,7 @@ public abstract class AbstractAsyncCommand : ContextAwareBase, IAsyncCommand
/// <returns>表示异步操作的任务</returns>
async Task IAsyncCommand.ExecuteAsync()
{
await OnExecuteAsync();
await OnExecuteAsync().ConfigureAwait(false);
}
/// <summary>
@ -25,4 +25,4 @@ public abstract class AbstractAsyncCommand : ContextAwareBase, IAsyncCommand
/// </summary>
/// <returns>表示异步操作的任务</returns>
protected abstract Task OnExecuteAsync();
}
}

View File

@ -17,7 +17,7 @@ public abstract class AbstractAsyncCommand<TInput>(TInput input) : ContextAwareB
/// <returns>表示异步操作的任务</returns>
async Task IAsyncCommand.ExecuteAsync()
{
await OnExecuteAsync(input);
await OnExecuteAsync(input).ConfigureAwait(false);
}
/// <summary>

View File

@ -18,7 +18,7 @@ public abstract class AbstractAsyncCommand<TInput, TResult>(TInput input) : Cont
/// <returns>表示异步操作且包含结果的任务</returns>
async Task<TResult> IAsyncCommand<TResult>.ExecuteAsync()
{
return await OnExecuteAsync(input);
return await OnExecuteAsync(input).ConfigureAwait(false);
}
/// <summary>

View File

@ -22,7 +22,7 @@ namespace GFramework.Core.Concurrency;
public sealed class AsyncKeyLockManager : IAsyncKeyLockManager
{
private readonly Timer _cleanupTimer;
private readonly ConcurrentDictionary<string, LockEntry> _locks = new();
private readonly ConcurrentDictionary<string, LockEntry> _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);
}
/// <summary>
@ -187,4 +188,4 @@ public sealed class AsyncKeyLockManager : IAsyncKeyLockManager
Semaphore.Dispose();
}
}
}
}

View File

@ -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
/// <summary>
/// 配置存储字典(线程安全)
/// </summary>
private readonly ConcurrentDictionary<string, object> _configs = new();
private readonly ConcurrentDictionary<string, object> _configs = new(StringComparer.Ordinal);
private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(ConfigurationManager));
@ -45,7 +46,7 @@ public class ConfigurationManager : IConfigurationManager
/// 配置监听器字典(线程安全)
/// 键:配置键,值:监听器列表
/// </summary>
private readonly ConcurrentDictionary<string, List<Delegate>> _watchers = new();
private readonly ConcurrentDictionary<string, List<Delegate>> _watchers = new(StringComparer.Ordinal);
/// <summary>
/// 获取配置数量
@ -200,7 +201,7 @@ public class ConfigurationManager : IConfigurationManager
/// </summary>
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!;
}
}
}
}

View File

@ -85,6 +85,15 @@ public readonly struct CoroutineHandle : IEquatable<CoroutineHandle>
return _id;
}
/// <summary>
/// 返回协程句柄的稳定字符串表示,用于日志和诊断输出。
/// </summary>
/// <returns>包含内部标识符与键值的诊断字符串。</returns>
public override string ToString()
{
return $"CoroutineHandle(Id={_id}, Key={Key})";
}
/// <summary>
/// 比较两个协程句柄是否相等
/// </summary>
@ -106,4 +115,4 @@ public readonly struct CoroutineHandle : IEquatable<CoroutineHandle>
{
return a._id != b._id;
}
}
}

View File

@ -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
}
}

View File

@ -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<CoroutinePriority, int> _countByPriority = new();
private readonly Dictionary<string, int> _countByTag = new();
private readonly Dictionary<string, int> _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
}
}
}
}
}

View File

@ -24,7 +24,7 @@ public sealed class WaitForTask<T> : IYieldInstruction
_done = true;
else
// 注册完成回调
_task.ContinueWith(_ => { _done = true; }, TaskContinuationOptions.ExecuteSynchronously);
_ = _task.ContinueWith(_ => { _done = true; }, TaskContinuationOptions.ExecuteSynchronously);
}
/// <summary>
@ -50,4 +50,4 @@ public sealed class WaitForTask<T> : IYieldInstruction
/// 获取等待是否已完成
/// </summary>
public bool IsDone => _done;
}
}

View File

@ -26,7 +26,7 @@ public sealed class WaitForTask : IYieldInstruction
_done = true;
else
// 注册完成回调
_task.ContinueWith(_ => { _done = true; }, TaskContinuationOptions.ExecuteSynchronously);
_ = _task.ContinueWith(_ => { _done = true; }, TaskContinuationOptions.ExecuteSynchronously);
}
/// <summary>
@ -47,4 +47,4 @@ public sealed class WaitForTask : IYieldInstruction
/// 获取等待是否已完成
/// </summary>
public bool IsDone => _done;
}
}

View File

@ -11,7 +11,7 @@ public abstract class EnvironmentBase : ContextAwareBase, IEnvironment
/// <summary>
/// 存储环境值的字典,键为字符串,值为对象类型
/// </summary>
protected readonly Dictionary<string, object> Values = new();
protected readonly IDictionary<string, object> Values = new Dictionary<string, object>(StringComparer.Ordinal);
/// <summary>
/// 获取环境名称的抽象属性
@ -84,4 +84,4 @@ public abstract class EnvironmentBase : ContextAwareBase, IEnvironment
// 将键值对添加到Values字典中
Values[key] = value;
}
}
}

View File

@ -9,9 +9,9 @@ namespace GFramework.Core.Events;
/// </summary>
public sealed class EventStatistics : IEventStatistics
{
private readonly Dictionary<string, int> _listenerCountByType = new();
private readonly Dictionary<string, int> _listenerCountByType = new(StringComparer.Ordinal);
private readonly object _lock = new();
private readonly Dictionary<string, long> _publishCountByType = new();
private readonly Dictionary<string, long> _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
}
}
}
}
}

View File

@ -118,11 +118,11 @@ public static class AsyncExtensions
try
{
return await task;
return await task.ConfigureAwait(false);
}
catch (Exception ex)
{
return fallback(ex);
}
}
}
}

View File

@ -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);
}
/// <summary>
@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -26,7 +26,7 @@ public static class NumericExtensions
public static bool Between<T>(this T value, T min, T max, bool inclusive = true) where T : IComparable<T>
{
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);
}
}
}

View File

@ -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<T>(result);
}
catch (Exception ex)
@ -107,4 +107,4 @@ public static class AsyncFunctionalExtensions
return new Result<T>(ex);
}
}
}
}

View File

@ -201,7 +201,7 @@ public readonly struct Result<A> : IEquatable<Result<A>>, IComparable<Result<A>>
try
{
return new Result<B>(await f(_value!));
return new Result<B>(await f(_value!).ConfigureAwait(false));
}
catch (Exception ex)
{
@ -307,7 +307,7 @@ public readonly struct Result<A> : IEquatable<Result<A>>, IComparable<Result<A>>
if (IsSuccess) return EqualityComparer<A>.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<A> : IEquatable<Result<A>>, IComparable<Result<A>>
ResultState.Faulted => $"Fail({Exception.Message})",
_ => "(Bottom)"
};
}
}

View File

@ -126,7 +126,7 @@ public readonly struct Result : IEquatable<Result>
return true;
return _exception!.GetType() == other._exception!.GetType() &&
_exception.Message == other._exception.Message;
string.Equals(_exception.Message, other._exception.Message, StringComparison.Ordinal);
}
/// <summary>
@ -216,4 +216,4 @@ public readonly struct Result : IEquatable<Result>
ArgumentNullException.ThrowIfNull(func);
return IsSuccess ? func() : Result<B>.Failure(_exception!);
}
}
}

View File

@ -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<TResult>.Fail(result.Exception);
}
@ -176,7 +177,7 @@ public static class ResultExtensions
return result.Match(
succ: value => predicate(value)
? result
: Result<T>.Fail(new ArgumentException(errorMessage)),
: Result<T>.Fail(new ArgumentException(errorMessage, nameof(errorMessage))),
fail: _ => result
);
}
@ -233,7 +234,7 @@ public static class ResultExtensions
ArgumentNullException.ThrowIfNull(func);
try
{
return Result<T>.Succeed(await func());
return Result<T>.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<T>.Succeed(value)
: Result<T>.Fail(new ArgumentNullException(errorMessage));
: Result<T>.Fail(new ArgumentNullException(nameof(value), errorMessage));
/// <summary>
/// 将可空值类型转换为 Result
@ -271,7 +272,7 @@ public static class ResultExtensions
string errorMessage = "Value is null") where T : struct =>
value.HasValue
? Result<T>.Succeed(value.Value)
: Result<T>.Fail(new ArgumentNullException(errorMessage));
: Result<T>.Fail(new ArgumentNullException(nameof(value), errorMessage));
#endregion
}
}

View File

@ -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();

View File

@ -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
};
}
}
}

View File

@ -27,8 +27,8 @@ public class LocalizationManager : AbstractSystem, ILocalizationManager
public LocalizationManager(LocalizationConfig? config = null)
{
_config = config ?? new LocalizationConfig();
_tables = new Dictionary<string, Dictionary<string, ILocalizationTable>>();
_formatters = new Dictionary<string, ILocalizationFormatter>();
_tables = new Dictionary<string, Dictionary<string, ILocalizationTable>>(StringComparer.Ordinal);
_formatters = new Dictionary<string, ILocalizationFormatter>(StringComparer.Ordinal);
_languageChangeCallbacks = new List<Action<string>>();
_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<string, ILocalizationTable>();
var languageTables = new Dictionary<string, ILocalizationTable>(StringComparer.Ordinal);
// 加载回退语言(如果不是默认语言)
Dictionary<string, ILocalizationTable>? 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<Dictionary<string, string>>(json);
return data ?? new Dictionary<string, string>();
return data ?? new Dictionary<string, string>(StringComparer.Ordinal);
}
/// <summary>
@ -314,4 +314,4 @@ public class LocalizationManager : AbstractSystem, ILocalizationManager
}
}
}
}
}

View File

@ -18,7 +18,10 @@ public class LocalizationString : ILocalizationString
/// 预编译的静态正则表达式,用于格式化字符串中的变量替换
/// </summary>
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<string, object> _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<string, object>();
_variables = new Dictionary<string, object>(StringComparer.Ordinal);
}
/// <inheritdoc/>
@ -234,4 +237,4 @@ public class LocalizationString : ILocalizationString
{
return manager.GetFormatter(name);
}
}
}

View File

@ -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<string, string>(data);
_overrides = new Dictionary<string, string>();
_data = new Dictionary<string, string>(data, StringComparer.Ordinal);
_overrides = new Dictionary<string, string>(StringComparer.Ordinal);
Fallback = fallback;
}
@ -105,7 +105,7 @@ public class LocalizationTable : ILocalizationTable
/// <returns>包含所有键的可枚举集合</returns>
public IEnumerable<string> GetKeys()
{
var keys = new HashSet<string>(_data.Keys);
var keys = new HashSet<string>(_data.Keys, StringComparer.Ordinal);
keys.UnionWith(_overrides.Keys);
if (Fallback != null)
@ -133,4 +133,4 @@ public class LocalizationTable : ILocalizationTable
_overrides[key] = value;
}
}
}
}

View File

@ -0,0 +1,54 @@
using GFramework.Core.Abstractions.Logging;
namespace GFramework.Core.Logging;
/// <summary>
/// Appender 配置。
/// </summary>
public sealed class AppenderConfiguration
{
/// <summary>
/// Appender 类型Console, File, RollingFile, Async
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// 格式化器类型Default, Json
/// </summary>
public string Formatter { get; set; } = "Default";
/// <summary>
/// 文件路径(仅用于 File 和 RollingFile
/// </summary>
public string? FilePath { get; set; }
/// <summary>
/// 是否使用颜色(仅用于 Console
/// </summary>
public bool UseColors { get; set; } = true;
/// <summary>
/// 缓冲区大小(仅用于 Async
/// </summary>
public int BufferSize { get; set; } = 10000;
/// <summary>
/// 最大文件大小(仅用于 RollingFile字节
/// </summary>
public long MaxFileSize { get; set; } = 10 * 1024 * 1024;
/// <summary>
/// 最大文件数量(仅用于 RollingFile
/// </summary>
public int MaxFileCount { get; set; } = 5;
/// <summary>
/// 过滤器配置。
/// </summary>
public FilterConfiguration? Filter { get; set; }
/// <summary>
/// 内部 Appender 配置(仅用于 Async
/// </summary>
public AppenderConfiguration? InnerAppender { get; set; }
}

View File

@ -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
// 错误观察者只用于诊断,绝不能反向影响日志处理线程的生命周期。
}
}
}
}

View File

@ -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<LogLevel, long> _levelCounts = new();
private readonly ConcurrentDictionary<string, long> _loggerCounts = new();
private readonly ConcurrentDictionary<string, long> _loggerCounts = new(StringComparer.Ordinal);
private readonly ITimeProvider _timeProvider;
private long _errorCount;
private long _startTimeTicks;
@ -127,7 +128,7 @@ public sealed class StatisticsAppender : ILogAppender
/// </summary>
public IReadOnlyDictionary<string, long> GetLoggerCounts()
{
return new Dictionary<string, long>(_loggerCounts);
return new Dictionary<string, long>(_loggerCounts, StringComparer.Ordinal);
}
/// <summary>
@ -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<LogLevel>())
{
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();
}
}
}

View File

@ -8,7 +8,7 @@ namespace GFramework.Core.Logging;
/// </summary>
public sealed class CachedLoggerFactory : ILoggerFactory
{
private readonly ConcurrentDictionary<string, ILogger> _cache = new();
private readonly ConcurrentDictionary<string, ILogger> _cache = new(StringComparer.Ordinal);
private readonly ILoggerFactory _innerFactory;
/// <summary>
@ -31,4 +31,4 @@ public sealed class CachedLoggerFactory : ILoggerFactory
var cacheKey = $"{name}:{minLevel}";
return _cache.GetOrAdd(cacheKey, _ => _innerFactory.GetLogger(name, minLevel));
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -0,0 +1,78 @@
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Logging.Appenders;
namespace GFramework.Core.Logging;
/// <summary>
/// 可配置的 Logger 工厂。
/// </summary>
internal sealed class ConfigurableLoggerFactory : ILoggerFactory, IDisposable
{
private readonly ILogAppender[] _appenders;
private readonly LoggingConfiguration _config;
private bool _disposed;
/// <summary>
/// 初始化一个基于日志配置创建输出管线的工厂实例。
/// </summary>
/// <param name="config">日志配置。</param>
public ConfigurableLoggerFactory(LoggingConfiguration config)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
_appenders = config.Appenders.Select(LoggingConfigurationLoader.CreateAppender).ToArray();
}
/// <summary>
/// 释放内部 Appender 持有的资源。
/// </summary>
public void Dispose()
{
if (_disposed)
{
return;
}
foreach (var appender in _appenders)
{
if (appender is IDisposable disposable)
{
disposable.Dispose();
}
}
_disposed = true;
}
/// <summary>
/// 为指定名称创建日志记录器,并应用最匹配的命名空间级别配置。
/// </summary>
/// <param name="name">日志记录器名称。</param>
/// <param name="minLevel">调用方传入的默认最小级别。</param>
/// <returns>可写入日志的记录器实例。</returns>
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);
}
}

View File

@ -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(
/// <param name="exception">异常信息,可为空</param>
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
}
}

View File

@ -0,0 +1,29 @@
using GFramework.Core.Abstractions.Logging;
namespace GFramework.Core.Logging;
/// <summary>
/// 过滤器配置。
/// </summary>
public sealed class FilterConfiguration
{
/// <summary>
/// 过滤器类型LogLevel, Namespace, Composite
/// </summary>
public string Type { get; set; } = "LogLevel";
/// <summary>
/// 最小日志级别(用于 LogLevel 过滤器)。
/// </summary>
public LogLevel? MinLevel { get; set; }
/// <summary>
/// 命名空间前缀列表(用于 Namespace 过滤器)。
/// </summary>
public List<string>? Namespaces { get; set; }
/// <summary>
/// 子过滤器列表(用于 Composite 过滤器)。
/// </summary>
public List<FilterConfiguration>? Filters { get; set; }
}

View File

@ -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<string, SamplingState> _samplingStates = new();
private readonly ConcurrentDictionary<string, SamplingState> _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;
}
}
}
}

View File

@ -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
/// <returns>格式化后的日志字符串</returns>
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();
}
}
}

View File

@ -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
/// <returns>JSON 格式的日志字符串</returns>
public string Format(LogEntry entry)
{
var logObject = new Dictionary<string, object?>
var logObject = new Dictionary<string, object?>(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);
}
}
}

View File

@ -20,82 +20,5 @@ public sealed class LoggingConfiguration
/// <summary>
/// 特定 Logger 的日志级别配置
/// </summary>
public Dictionary<string, LogLevel> LoggerLevels { get; set; } = new();
public Dictionary<string, LogLevel> LoggerLevels { get; set; } = new(StringComparer.Ordinal);
}
/// <summary>
/// Appender 配置
/// </summary>
public sealed class AppenderConfiguration
{
/// <summary>
/// Appender 类型Console, File, RollingFile, Async
/// </summary>
public string Type { get; set; } = string.Empty;
/// <summary>
/// 格式化器类型Default, Json
/// </summary>
public string Formatter { get; set; } = "Default";
/// <summary>
/// 文件路径(仅用于 File 和 RollingFile
/// </summary>
public string? FilePath { get; set; }
/// <summary>
/// 是否使用颜色(仅用于 Console
/// </summary>
public bool UseColors { get; set; } = true;
/// <summary>
/// 缓冲区大小(仅用于 Async
/// </summary>
public int BufferSize { get; set; } = 10000;
/// <summary>
/// 最大文件大小(仅用于 RollingFile字节
/// </summary>
public long MaxFileSize { get; set; } = 10 * 1024 * 1024; // 10MB
/// <summary>
/// 最大文件数量(仅用于 RollingFile
/// </summary>
public int MaxFileCount { get; set; } = 5;
/// <summary>
/// 过滤器配置
/// </summary>
public FilterConfiguration? Filter { get; set; }
/// <summary>
/// 内部 Appender 配置(仅用于 Async
/// </summary>
public AppenderConfiguration? InnerAppender { get; set; }
}
/// <summary>
/// 过滤器配置
/// </summary>
public sealed class FilterConfiguration
{
/// <summary>
/// 过滤器类型LogLevel, Namespace, Composite
/// </summary>
public string Type { get; set; } = "LogLevel";
/// <summary>
/// 最小日志级别(用于 LogLevel 过滤器)
/// </summary>
public LogLevel? MinLevel { get; set; }
/// <summary>
/// 命名空间前缀列表(用于 Namespace 过滤器)
/// </summary>
public List<string>? Namespaces { get; set; }
/// <summary>
/// 子过滤器列表(用于 Composite 过滤器)
/// </summary>
public List<FilterConfiguration>? Filters { get; set; }
}

View File

@ -127,66 +127,3 @@ public static class LoggingConfigurationLoader
};
}
}
/// <summary>
/// 可配置的 Logger 工厂
/// </summary>
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);
}
}

View File

@ -15,7 +15,7 @@ public abstract class AbstractObjectPoolSystem<TKey, TObject>
/// <summary>
/// 存储对象池的字典,键为池标识,值为池信息
/// </summary>
protected readonly Dictionary<TKey, PoolInfo> Pools = new();
protected readonly IDictionary<TKey, PoolInfo> Pools = new Dictionary<TKey, PoolInfo>();
/// <summary>
/// 获取对象池中的对象,如果池中没有可用对象则创建新的对象
@ -254,4 +254,4 @@ public abstract class AbstractObjectPoolSystem<TKey, TObject>
/// </summary>
public int ActiveCount { get; set; }
}
}
}

View File

@ -3,19 +3,8 @@ using System.Collections.Concurrent;
namespace GFramework.Core.Resource;
/// <summary>
/// 资源缓存条目
/// </summary>
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;
}
/// <summary>
/// 资源缓存系统,管理已加载资源的缓存和引用计数
/// 线程安全:所有公共方法都是线程安全的
/// 资源缓存系统,管理已加载资源的缓存和引用计数。
/// 线程安全:所有公共方法都是线程安全的。
/// </summary>
internal sealed class ResourceCache
{
@ -24,7 +13,7 @@ internal sealed class ResourceCache
/// </summary>
private const string PathCannotBeNullOrEmptyMessage = "Path cannot be null or whitespace.";
private readonly ConcurrentDictionary<string, ResourceCacheEntry> _cache = new();
private readonly ConcurrentDictionary<string, ResourceCacheEntry> _cache = new(StringComparer.Ordinal);
private readonly object _lock = new();
/// <summary>
@ -208,4 +197,4 @@ internal sealed class ResourceCache
return unreferencedPaths;
}
}
}

View File

@ -0,0 +1,27 @@
namespace GFramework.Core.Resource;
/// <summary>
/// 资源缓存条目。
/// </summary>
internal sealed class ResourceCacheEntry(object resource, Type resourceType)
{
/// <summary>
/// 获取缓存中的资源实例。
/// </summary>
public object Resource { get; } = resource ?? throw new ArgumentNullException(nameof(resource));
/// <summary>
/// 获取资源的运行时类型。
/// </summary>
public Type ResourceType { get; } = resourceType ?? throw new ArgumentNullException(nameof(resourceType));
/// <summary>
/// 获取或设置当前引用计数。
/// </summary>
public int ReferenceCount { get; set; }
/// <summary>
/// 获取或设置最近访问时间。
/// </summary>
public DateTime LastAccessTime { get; set; } = DateTime.UtcNow;
}

View File

@ -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
/// </summary>
public async Task PreloadAsync<T>(string path) where T : class
{
await LoadAsync<T>(path);
await LoadAsync<T>(path).ConfigureAwait(false);
}
/// <summary>
@ -324,4 +324,4 @@ public class ResourceManager : IResourceManager
}
}
}
}
}

View File

@ -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)
{

View File

@ -17,7 +17,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine
/// <summary>
/// 存储所有已注册状态的字典,键为状态类型,值为状态实例
/// </summary>
protected readonly Dictionary<Type, IState> States = new();
protected readonly IDictionary<Type, IState> States = new Dictionary<Type, IState>();
/// <summary>
/// 获取当前激活的状态
@ -45,7 +45,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine
/// <typeparam name="T">要注销的状态类型</typeparam>
public async Task<IStateMachine> UnregisterAsync<T>() where T : IState
{
await _transitionLock.WaitAsync();
await _transitionLock.WaitAsync().ConfigureAwait(false);
try
{
var stateToUnregister = PrepareUnregister<T>(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
/// <returns>如果可以切换则返回true否则返回false</returns>
public async Task<bool> CanChangeToAsync<T>() 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
/// <exception cref="InvalidOperationException">当目标状态未注册时抛出</exception>
public async Task<bool> ChangeToAsync<T>() 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
/// <returns>如果成功回退则返回true否则返回false</returns>
public async Task<bool> 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);
}
/// <summary>
@ -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);
}
/// <summary>
@ -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<bool> 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;
}
}
}

View File

@ -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
});
}
}
}

View File

@ -50,7 +50,7 @@ public sealed class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRe
try
{
var response = await next(message, cancellationToken);
var response = await next(message, cancellationToken).ConfigureAwait(false);
var elapsed = Stopwatch.GetElapsedTime(start);
_logger.Debug($"Handled {requestName} successfully in {elapsed.TotalMilliseconds} ms");

View File

@ -48,7 +48,7 @@ public sealed class PerformanceBehavior<TRequest, TResponse> : IPipelineBehavior
try
{
return await next(message, cancellationToken);
return await next(message, cancellationToken).ConfigureAwait(false);
}
finally
{

View File

@ -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);
}
/// <summary>

View File

@ -44,8 +44,8 @@ internal sealed class WeakKeyCache<TKey, TValue>
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<TKey, TValue>
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<TKey, TValue>
return TryGetValue(key, out var value) ? value : null;
}
}
/// <summary>
/// 提供以两段 <see cref="Type" /> 为键的弱引用缓存。
/// 适用于请求/响应或流请求/响应这类组合类型元数据的复用场景。
/// </summary>
/// <typeparam name="TValue">缓存值类型。</typeparam>
/// <remarks>
/// 第一层和第二层键都使用弱键缓存,因此只要任一类型不再被外部引用,
/// 对应条目都允许被 GC 清理,并在后续首次访问时重新建立。
/// </remarks>
internal sealed class WeakTypePairCache<TValue>
where TValue : class
{
private readonly WeakKeyCache<Type, WeakKeyCache<Type, TValue>> _entries = new();
/// <summary>
/// 获取指定类型对对应的缓存值;若未命中则创建并写入。
/// </summary>
/// <param name="primaryType">第一段类型键。</param>
/// <param name="secondaryType">第二段类型键。</param>
/// <param name="valueFactory">创建缓存值的工厂方法。</param>
/// <returns>已存在或新创建的缓存值。</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="primaryType" />、<paramref name="secondaryType" /> 或
/// <paramref name="valueFactory" /> 为 <see langword="null" />。
/// </exception>
public TValue GetOrAdd(Type primaryType, Type secondaryType, Func<Type, Type, TValue> valueFactory)
{
ArgumentNullException.ThrowIfNull(primaryType);
ArgumentNullException.ThrowIfNull(secondaryType);
ArgumentNullException.ThrowIfNull(valueFactory);
var secondaryEntries = _entries.GetOrAdd(primaryType, static _ => new WeakKeyCache<Type, TValue>());
return secondaryEntries.GetOrAdd(
secondaryType,
(PrimaryType: primaryType, Factory: valueFactory),
static (cachedSecondaryType, state) => state.Factory(state.PrimaryType, cachedSecondaryType));
}
/// <summary>
/// 尝试读取指定类型对的缓存值,而不触发新的创建逻辑。
/// </summary>
/// <param name="primaryType">第一段类型键。</param>
/// <param name="secondaryType">第二段类型键。</param>
/// <param name="value">命中时返回的缓存值。</param>
/// <returns>若命中当前缓存则为 <see langword="true" />;否则为 <see langword="false" />。</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="primaryType" /> 或 <paramref name="secondaryType" /> 为 <see langword="null" />。
/// </exception>
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;
}
/// <summary>
/// 清空当前缓存实例。
/// </summary>
/// <remarks>
/// 该方法主要服务于测试,避免同一进程里的静态缓存污染后续断言。
/// </remarks>
public void Clear()
{
_entries.Clear();
}
/// <summary>
/// 返回指定类型对当前命中的缓存对象;若未命中则返回 <see langword="null" />。
/// </summary>
/// <param name="primaryType">第一段类型键。</param>
/// <param name="secondaryType">第二段类型键。</param>
/// <returns>当前缓存对象,或 <see langword="null" />。</returns>
/// <remarks>
/// 该入口仅用于测试通过反射观察缓存状态,不应用于运行时代码路径。
/// </remarks>
public TValue? GetValueOrDefaultForTesting(Type primaryType, Type secondaryType)
{
return TryGetValue(primaryType, secondaryType, out var value) ? value : null;
}
}

View File

@ -0,0 +1,88 @@
namespace GFramework.Cqrs.Internal;
/// <summary>
/// 提供以两段 <see cref="Type" /> 为键的弱引用缓存。
/// 适用于请求/响应或流请求/响应这类组合类型元数据的复用场景。
/// </summary>
/// <typeparam name="TValue">缓存值类型。</typeparam>
/// <remarks>
/// 第一层和第二层键都使用弱键缓存,因此只要任一类型不再被外部引用,
/// 对应条目都允许被 GC 清理,并在后续首次访问时重新建立。
/// </remarks>
internal sealed class WeakTypePairCache<TValue>
where TValue : class
{
private readonly WeakKeyCache<Type, WeakKeyCache<Type, TValue>> _entries = new();
/// <summary>
/// 获取指定类型对对应的缓存值;若未命中则创建并写入。
/// </summary>
/// <param name="primaryType">第一段类型键。</param>
/// <param name="secondaryType">第二段类型键。</param>
/// <param name="valueFactory">创建缓存值的工厂方法。</param>
/// <returns>已存在或新创建的缓存值。</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="primaryType" />、<paramref name="secondaryType" /> 或
/// <paramref name="valueFactory" /> 为 <see langword="null" />。
/// </exception>
public TValue GetOrAdd(Type primaryType, Type secondaryType, Func<Type, Type, TValue> valueFactory)
{
ArgumentNullException.ThrowIfNull(primaryType);
ArgumentNullException.ThrowIfNull(secondaryType);
ArgumentNullException.ThrowIfNull(valueFactory);
var secondaryEntries = _entries.GetOrAdd(primaryType, static _ => new WeakKeyCache<Type, TValue>());
return secondaryEntries.GetOrAdd(
secondaryType,
(PrimaryType: primaryType, Factory: valueFactory),
static (cachedSecondaryType, state) => state.Factory(state.PrimaryType, cachedSecondaryType));
}
/// <summary>
/// 尝试读取指定类型对的缓存值,而不触发新的创建逻辑。
/// </summary>
/// <param name="primaryType">第一段类型键。</param>
/// <param name="secondaryType">第二段类型键。</param>
/// <param name="value">命中时返回的缓存值。</param>
/// <returns>若命中当前缓存则为 <see langword="true" />;否则为 <see langword="false" />。</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="primaryType" /> 或 <paramref name="secondaryType" /> 为 <see langword="null" />。
/// </exception>
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;
}
/// <summary>
/// 清空当前缓存实例。
/// </summary>
/// <remarks>
/// 该方法主要服务于测试,避免同一进程里的静态缓存污染后续断言。
/// </remarks>
public void Clear()
{
_entries.Clear();
}
/// <summary>
/// 返回指定类型对当前命中的缓存对象;若未命中则返回 <see langword="null" />。
/// </summary>
/// <param name="primaryType">第一段类型键。</param>
/// <param name="secondaryType">第二段类型键。</param>
/// <returns>当前缓存对象,或 <see langword="null" />。</returns>
/// <remarks>
/// 该入口仅用于测试通过反射观察缓存状态,不应用于运行时代码路径。
/// </remarks>
public TValue? GetValueOrDefaultForTesting(Type primaryType, Type secondaryType)
{
return TryGetValue(primaryType, secondaryType, out var value) ? value : null;
}
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,33 @@
namespace GFramework.Godot.Config;
/// <summary>
/// 描述一次目录枚举返回的单个子项。
/// </summary>
/// <remarks>
/// 该结构只承载目录扫描阶段需要的最小信息。
/// <see cref="Name" /> 必须是单个目录项名称,而不是包含父目录的完整路径;
/// 对于 Godot 路径和普通路径都遵循相同约定,便于加载器统一做后续拼接与过滤。
/// </remarks>
internal readonly record struct GodotYamlConfigDirectoryEntry
{
/// <summary>
/// 初始化一个目录枚举结果项。
/// </summary>
/// <param name="name">当前目录项的名称,不包含父目录路径。</param>
/// <param name="isDirectory">指示该目录项是否为子目录。</param>
public GodotYamlConfigDirectoryEntry(string name, bool isDirectory)
{
Name = name;
IsDirectory = isDirectory;
}
/// <summary>
/// 获取当前目录项的名称,不包含父目录路径。
/// </summary>
public string Name { get; }
/// <summary>
/// 获取一个值,指示当前目录项是否为子目录。
/// </summary>
public bool IsDirectory { get; }
}

View File

@ -0,0 +1,163 @@
using System.IO;
using FileAccess = Godot.FileAccess;
namespace GFramework.Godot.Config;
/// <summary>
/// 抽象 <see cref="GodotYamlConfigLoader" /> 与具体宿主环境之间的 Godot 路径和文件访问边界。
/// </summary>
/// <remarks>
/// 该抽象存在的原因,是编辑器态与导出态对 <c>res://</c>、<c>user://</c> 的访问方式不同:
/// 编辑器态通常可以把 Godot 特殊路径全局化后直接落到普通文件系统,而导出态往往只能通过 Godot API 读取原始文本资源,
/// 再把它们复制到运行时缓存目录。<see cref="EnumerateDirectory" /> 在目录不存在或当前环境无法枚举时必须返回
/// <see langword="null" />,用来表达“不可访问”而不是抛出未找到异常;<see cref="ReadAllBytes" /> 则应保留底层读取失败异常,
/// 交由加载器包装成配置诊断。对于普通文件系统路径,应遵循 <see cref="Directory" /> / <see cref="File" /> 语义;
/// 对于 Godot 特殊路径,则应使用引擎提供的路径解析和读取能力。
/// </remarks>
internal sealed class GodotYamlConfigEnvironment
{
/// <summary>
/// 初始化一个可替换的 Godot YAML 配置宿主环境抽象。
/// </summary>
/// <param name="isEditor">返回当前进程是否处于 Godot 编辑器态的委托。</param>
/// <param name="globalizePath">
/// 把 Godot 特殊路径转换为普通绝对路径的委托。
/// 当前加载器仅会在输入为 <c>res://</c> 或 <c>user://</c> 时调用它,返回值必须为非空绝对路径。
/// </param>
/// <param name="enumerateDirectory">
/// 枚举指定目录直接子项的委托。
/// 当目录不存在、无法访问或当前环境无法枚举该路径时,必须返回 <see langword="null" />。
/// </param>
/// <param name="fileExists">
/// 检查指定路径上的文件是否存在的委托。
/// 输入既可能是 Godot 特殊路径,也可能是普通绝对路径。
/// </param>
/// <param name="readAllBytes">
/// 读取指定文件完整字节内容的委托。
/// 当文件缺失或读取失败时,应抛出底层异常,由加载器统一包装为配置加载诊断。
/// </param>
/// <exception cref="ArgumentNullException">任一委托参数为 <see langword="null" /> 时抛出。</exception>
public GodotYamlConfigEnvironment(
Func<bool> isEditor,
Func<string, string> globalizePath,
Func<string, IReadOnlyList<GodotYamlConfigDirectoryEntry>?> enumerateDirectory,
Func<string, bool> fileExists,
Func<string, byte[]> 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));
}
/// <summary>
/// 获取默认的 Godot 运行时环境实现。
/// </summary>
/// <remarks>
/// 默认实现使用 <see cref="OS.HasFeature(string)" /> 检测编辑器态,
/// 使用 <see cref="ProjectSettings.GlobalizePath(string)" /> 处理 Godot 特殊路径,
/// 并在 Godot 路径与普通路径之间切换对应的枚举和读取 API。
/// </remarks>
public static GodotYamlConfigEnvironment Default { get; } = new(
static () => OS.HasFeature("editor"),
static path => ProjectSettings.GlobalizePath(path),
EnumerateDirectoryCore,
FileExistsCore,
ReadAllBytesCore);
/// <summary>
/// 获取用于判断当前进程是否处于编辑器态的委托。
/// </summary>
public Func<bool> IsEditor { get; }
/// <summary>
/// 获取把 Godot 特殊路径转换为普通绝对路径的委托。
/// </summary>
/// <remarks>
/// 当前加载器只会对 <c>res://</c> 和 <c>user://</c> 路径调用该委托。
/// 返回空字符串会被视为无效环境实现,并在后续路径解析阶段触发异常。
/// </remarks>
public Func<string, string> GlobalizePath { get; }
/// <summary>
/// 获取用于枚举目录直接子项的委托。
/// </summary>
/// <remarks>
/// 当目录不存在、无法访问,或当前环境无法枚举给定路径时,该委托必须返回 <see langword="null" />。
/// 返回的集合只应包含当前目录下的直接子项,调用方会自行过滤隐藏项、子目录与非 YAML 文件。
/// </remarks>
public Func<string, IReadOnlyList<GodotYamlConfigDirectoryEntry>?> EnumerateDirectory { get; }
/// <summary>
/// 获取用于检查文件是否存在的委托。
/// </summary>
public Func<string, bool> FileExists { get; }
/// <summary>
/// 获取用于读取文件完整字节内容的委托。
/// </summary>
/// <remarks>
/// 该委托在路径不存在、权限不足或 I/O 失败时应抛出底层异常,以便加载器保留失败原因并生成诊断信息。
/// </remarks>
public Func<string, byte[]> ReadAllBytes { get; }
private static IReadOnlyList<GodotYamlConfigDirectoryEntry>? 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<GodotYamlConfigDirectoryEntry>();
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);
}
}

View File

@ -153,7 +153,7 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
SynchronizeRuntimeCache(cancellationToken);
}
await _loader.LoadAsync(registry, cancellationToken);
await _loader.LoadAsync(registry, cancellationToken).ConfigureAwait(false);
}
/// <summary>
@ -511,194 +511,3 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
innerException);
}
}
/// <summary>
/// 抽象 <see cref="GodotYamlConfigLoader" /> 与具体宿主环境之间的 Godot 路径和文件访问边界。
/// </summary>
/// <remarks>
/// 该抽象存在的原因,是编辑器态与导出态对 <c>res://</c>、<c>user://</c> 的访问方式不同:
/// 编辑器态通常可以把 Godot 特殊路径全局化后直接落到普通文件系统,而导出态往往只能通过 Godot API 读取原始文本资源,
/// 再把它们复制到运行时缓存目录。<see cref="EnumerateDirectory" /> 在目录不存在或当前环境无法枚举时必须返回
/// <see langword="null" />,用来表达“不可访问”而不是抛出未找到异常;<see cref="ReadAllBytes" /> 则应保留底层读取失败异常,
/// 交由加载器包装成配置诊断。对于普通文件系统路径,应遵循 <see cref="Directory" /> / <see cref="File" /> 语义;
/// 对于 Godot 特殊路径,则应使用引擎提供的路径解析和读取能力。
/// </remarks>
internal sealed class GodotYamlConfigEnvironment
{
/// <summary>
/// 初始化一个可替换的 Godot YAML 配置宿主环境抽象。
/// </summary>
/// <param name="isEditor">返回当前进程是否处于 Godot 编辑器态的委托。</param>
/// <param name="globalizePath">
/// 把 Godot 特殊路径转换为普通绝对路径的委托。
/// 当前加载器仅会在输入为 <c>res://</c> 或 <c>user://</c> 时调用它,返回值必须为非空绝对路径。
/// </param>
/// <param name="enumerateDirectory">
/// 枚举指定目录直接子项的委托。
/// 当目录不存在、无法访问或当前环境无法枚举该路径时,必须返回 <see langword="null" />。
/// </param>
/// <param name="fileExists">
/// 检查指定路径上的文件是否存在的委托。
/// 输入既可能是 Godot 特殊路径,也可能是普通绝对路径。
/// </param>
/// <param name="readAllBytes">
/// 读取指定文件完整字节内容的委托。
/// 当文件缺失或读取失败时,应抛出底层异常,由加载器统一包装为配置加载诊断。
/// </param>
/// <exception cref="ArgumentNullException">任一委托参数为 <see langword="null" /> 时抛出。</exception>
public GodotYamlConfigEnvironment(
Func<bool> isEditor,
Func<string, string> globalizePath,
Func<string, IReadOnlyList<GodotYamlConfigDirectoryEntry>?> enumerateDirectory,
Func<string, bool> fileExists,
Func<string, byte[]> 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));
}
/// <summary>
/// 获取默认的 Godot 运行时环境实现。
/// </summary>
/// <remarks>
/// 默认实现使用 <see cref="OS.HasFeature(string)" /> 检测编辑器态,
/// 使用 <see cref="ProjectSettings.GlobalizePath(string)" /> 处理 Godot 特殊路径,
/// 并在 Godot 路径与普通路径之间切换对应的枚举和读取 API。
/// </remarks>
public static GodotYamlConfigEnvironment Default { get; } = new(
static () => OS.HasFeature("editor"),
static path => ProjectSettings.GlobalizePath(path),
EnumerateDirectoryCore,
FileExistsCore,
ReadAllBytesCore);
/// <summary>
/// 获取用于判断当前进程是否处于编辑器态的委托。
/// </summary>
public Func<bool> IsEditor { get; }
/// <summary>
/// 获取把 Godot 特殊路径转换为普通绝对路径的委托。
/// </summary>
/// <remarks>
/// 当前加载器只会对 <c>res://</c> 和 <c>user://</c> 路径调用该委托。
/// 返回空字符串会被视为无效环境实现,并在后续路径解析阶段触发异常。
/// </remarks>
public Func<string, string> GlobalizePath { get; }
/// <summary>
/// 获取用于枚举目录直接子项的委托。
/// </summary>
/// <remarks>
/// 当目录不存在、无法访问,或当前环境无法枚举给定路径时,该委托必须返回 <see langword="null" />。
/// 返回的集合只应包含当前目录下的直接子项,调用方会自行过滤隐藏项、子目录与非 YAML 文件。
/// </remarks>
public Func<string, IReadOnlyList<GodotYamlConfigDirectoryEntry>?> EnumerateDirectory { get; }
/// <summary>
/// 获取用于检查文件是否存在的委托。
/// </summary>
public Func<string, bool> FileExists { get; }
/// <summary>
/// 获取用于读取文件完整字节内容的委托。
/// </summary>
/// <remarks>
/// 该委托在路径不存在、权限不足或 I/O 失败时应抛出底层异常,以便加载器保留失败原因并生成诊断信息。
/// </remarks>
public Func<string, byte[]> ReadAllBytes { get; }
private static IReadOnlyList<GodotYamlConfigDirectoryEntry>? 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<GodotYamlConfigDirectoryEntry>();
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);
}
}
/// <summary>
/// 描述一次目录枚举返回的单个子项。
/// </summary>
/// <remarks>
/// 该结构只承载目录扫描阶段需要的最小信息。
/// <see cref="Name" /> 必须是单个目录项名称,而不是包含父目录的完整路径;
/// 对于 Godot 路径和普通路径都遵循相同约定,便于加载器统一做后续拼接与过滤。
/// </remarks>
internal readonly record struct GodotYamlConfigDirectoryEntry
{
/// <summary>
/// 初始化一个目录枚举结果项。
/// </summary>
/// <param name="name">当前目录项的名称,不包含父目录路径。</param>
/// <param name="isDirectory">指示该目录项是否为子目录。</param>
public GodotYamlConfigDirectoryEntry(string name, bool isDirectory)
{
Name = name;
IsDirectory = isDirectory;
}
/// <summary>
/// 获取当前目录项的名称,不包含父目录路径。
/// </summary>
public string Name { get; }
/// <summary>
/// 获取一个值,指示当前目录项是否为子目录。
/// </summary>
public bool IsDirectory { get; }
}

View File

@ -209,7 +209,8 @@ public class GodotResourceRepository<TKey, TResource>
}
// 只处理.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<TKey, TResource>
if (!_storage.TryAdd(resource.Key, resource))
Log.Warn($"Duplicate key detected: {resource.Key}");
}
}
}

View File

@ -13,7 +13,7 @@ public static class GodotPathExtensions
/// <returns>如果路径以 "user://" 开头且不为空,则返回 true否则返回 false。</returns>
public static bool IsUserPath(this string path)
{
return !string.IsNullOrEmpty(path) && path.StartsWith("user://");
return !string.IsNullOrEmpty(path) && path.StartsWith("user://", StringComparison.Ordinal);
}
/// <summary>
@ -23,7 +23,7 @@ public static class GodotPathExtensions
/// <returns>如果路径以 "res://" 开头且不为空,则返回 true否则返回 false。</returns>
public static bool IsResPath(this string path)
{
return !string.IsNullOrEmpty(path) && path.StartsWith("res://");
return !string.IsNullOrEmpty(path) && path.StartsWith("res://", StringComparison.Ordinal);
}
/// <summary>
@ -35,4 +35,4 @@ public static class GodotPathExtensions
{
return path.IsUserPath() || path.IsResPath();
}
}
}

View File

@ -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);
}
/// <summary>
@ -285,4 +285,4 @@ public static class NodeExtensions
return t;
throw new InvalidCastException($"Cannot cast {node} to {typeof(T)}");
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -114,7 +114,7 @@ public abstract class SceneBehaviorBase<T> : 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<T> : 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<T> : 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<T> : ISceneBehavior
return;
if (_scene != null)
await _scene.OnResumeAsync();
await _scene.OnResumeAsync().ConfigureAwait(false);
// 恢复处理
Owner.SetProcess(true);
@ -185,7 +185,7 @@ public abstract class SceneBehaviorBase<T> : ISceneBehavior
_isTransitioning = true;
if (_scene != null)
await _scene.OnExitAsync();
await _scene.OnExitAsync().ConfigureAwait(false);
_isActive = false;
}
@ -198,7 +198,7 @@ public abstract class SceneBehaviorBase<T> : 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<T> : ISceneBehavior
}
#endregion
}
}

View File

@ -24,7 +24,7 @@ public class LocalizationMap
/// <summary>
/// 用户语言 -> Godot locale 映射表。
/// </summary>
public Dictionary<string, string> LanguageMap { get; set; } = new()
public Dictionary<string, string> LanguageMap { get; set; } = new(StringComparer.Ordinal)
{
{ "简体中文", "zh_CN" },
{ "English", "en" }
@ -33,7 +33,7 @@ public class LocalizationMap
/// <summary>
/// 用户语言 -> GFramework 本地化语言码映射表。
/// </summary>
public Dictionary<string, string> FrameworkLanguageMap { get; set; } = new()
public Dictionary<string, string> FrameworkLanguageMap { get; set; } = new(StringComparer.Ordinal)
{
{ "简体中文", "zhs" },
{ "English", "eng" }
@ -68,4 +68,4 @@ public class LocalizationMap
return FrameworkLanguageMap.GetValueOrDefault(storedLanguage, DefaultFrameworkLanguage);
}
}
}

View File

@ -14,7 +14,7 @@ public class GodotGraphicsSettings(ISettingsModel model) : IResetApplyAbleSettin
/// 应用图形设置到Godot引擎
/// </summary>
/// <returns>异步任务</returns>
public async Task Apply()
public Task Apply()
{
var settings = model.GetData<GraphicsSettings>();
// 创建分辨率向量
@ -40,7 +40,7 @@ public class GodotGraphicsSettings(ISettingsModel model) : IResetApplyAbleSettin
DisplayServer.WindowSetPosition(pos);
}
await Task.CompletedTask;
return Task.CompletedTask;
}
/// <summary>
@ -62,4 +62,4 @@ public class GodotGraphicsSettings(ISettingsModel model) : IResetApplyAbleSettin
/// 该属性返回图形设置数据的具体类型信息。
/// </summary>
public Type DataType { get; } = typeof(GraphicsSettings);
}
}

View File

@ -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<string>)result;
});
}).ConfigureAwait(false);
}
/// <summary>
@ -319,7 +322,7 @@ public sealed class GodotFileStorage : IStorage, IDisposable
dir.ListDirEnd();
return (IReadOnlyList<string>)result;
});
}).ConfigureAwait(false);
}
/// <summary>
@ -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
}
}

View File

@ -40,7 +40,7 @@ public static class UiPageBehaviorFactory
UiLayer.Modal => new ModalLayerUiPageBehavior<T>(owner, key),
UiLayer.Toast => new ToastLayerUiPageBehavior<T>(owner, key),
UiLayer.Topmost => new TopmostLayerUiPageBehavior<T>(owner, key),
_ => throw new ArgumentException($"Unsupported UI layer: {layer}")
_ => throw new ArgumentException($"Unsupported UI layer: {layer}", nameof(layer))
};
}
}
}