mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-13 06:04:30 +08:00
Merge pull request #252 from GeWuYou/fix/analyzer-warning-reduction-batch-1
fix(analyzers): 降低 Core、Cqrs、Godot 与生成器的构建警告
This commit is contained in:
commit
c0ef3b5c00
18
AGENTS.md
18
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
|
- 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.
|
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: `<type>(<scope>): <summary>`.
|
||||||
|
- 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 `<type>/<topic-or-scope>`, where `<type>` should match the intended
|
||||||
|
Conventional Commit category as closely as practical.
|
||||||
|
|
||||||
## Subagent Usage Rules
|
## 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
|
- Use subagents only when the task is complex, the context is likely to grow too large, or the work can be split into
|
||||||
|
|||||||
@ -22,11 +22,14 @@ public interface IResourceManager : IUtility
|
|||||||
T? Load<T>(string path) where T : class;
|
T? Load<T>(string path) where T : class;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 异步加载资源
|
/// 异步加载指定路径的资源,并在缓存中对并发加载进行去重。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">资源类型</typeparam>
|
/// <typeparam name="T">资源类型</typeparam>
|
||||||
/// <param name="path">资源路径</param>
|
/// <param name="path">资源路径,不能为空或空白。</param>
|
||||||
/// <returns>资源实例,如果加载失败返回 null</returns>
|
/// <returns>加载成功返回资源实例;加载失败返回 <see langword="null"/>。</returns>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="path"/> 为空或空白时抛出。</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">当未注册对应资源加载器时抛出。</exception>
|
||||||
|
/// <remarks>实现内部可能使用 <c>ConfigureAwait(false)</c>,异步延续不保证回到调用线程。</remarks>
|
||||||
Task<T?> LoadAsync<T>(string path) where T : class;
|
Task<T?> LoadAsync<T>(string path) where T : class;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -70,10 +73,14 @@ public interface IResourceManager : IUtility
|
|||||||
void UnregisterLoader<T>() where T : class;
|
void UnregisterLoader<T>() where T : class;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 预加载资源(加载但不返回)
|
/// 预加载资源到缓存中。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">资源类型</typeparam>
|
/// <typeparam name="T">资源类型</typeparam>
|
||||||
/// <param name="path">资源路径</param>
|
/// <param name="path">资源路径,不能为空或空白。</param>
|
||||||
|
/// <returns>表示预加载流程完成的任务。</returns>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="path"/> 为空或空白时抛出。</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">当未注册对应资源加载器时抛出。</exception>
|
||||||
|
/// <remarks>内部委托给 <see cref="LoadAsync{T}(string)"/>,同样不捕获同步上下文。</remarks>
|
||||||
Task PreloadAsync<T>(string path) where T : class;
|
Task PreloadAsync<T>(string path) where T : class;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -86,4 +93,4 @@ public interface IResourceManager : IUtility
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="strategy">资源释放策略</param>
|
/// <param name="strategy">资源释放策略</param>
|
||||||
void SetReleaseStrategy(IResourceReleaseStrategy strategy);
|
void SetReleaseStrategy(IResourceReleaseStrategy strategy);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -635,7 +635,7 @@ public sealed class ContextRegistrationAnalyzer : DiagnosticAnalyzer
|
|||||||
if (targetMethod.TypeArguments[0] is not INamedTypeSymbol namedType)
|
if (targetMethod.TypeArguments[0] is not INamedTypeSymbol namedType)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (targetMethod.Name == "RegisterModel" &&
|
if (string.Equals(targetMethod.Name, "RegisterModel", StringComparison.Ordinal) &&
|
||||||
SymbolHelpers.IsAssignableTo(targetMethod.ContainingType, symbols.IArchitectureType))
|
SymbolHelpers.IsAssignableTo(targetMethod.ContainingType, symbols.IArchitectureType))
|
||||||
{
|
{
|
||||||
componentKind = ComponentKind.Model;
|
componentKind = ComponentKind.Model;
|
||||||
@ -643,7 +643,7 @@ public sealed class ContextRegistrationAnalyzer : DiagnosticAnalyzer
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetMethod.Name == "RegisterSystem" &&
|
if (string.Equals(targetMethod.Name, "RegisterSystem", StringComparison.Ordinal) &&
|
||||||
SymbolHelpers.IsAssignableTo(targetMethod.ContainingType, symbols.IArchitectureType))
|
SymbolHelpers.IsAssignableTo(targetMethod.ContainingType, symbols.IArchitectureType))
|
||||||
{
|
{
|
||||||
componentKind = ComponentKind.System;
|
componentKind = ComponentKind.System;
|
||||||
@ -651,7 +651,7 @@ public sealed class ContextRegistrationAnalyzer : DiagnosticAnalyzer
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetMethod.Name == "RegisterUtility" &&
|
if (string.Equals(targetMethod.Name, "RegisterUtility", StringComparison.Ordinal) &&
|
||||||
SymbolHelpers.IsAssignableTo(targetMethod.ContainingType, symbols.IArchitectureType))
|
SymbolHelpers.IsAssignableTo(targetMethod.ContainingType, symbols.IArchitectureType))
|
||||||
{
|
{
|
||||||
componentKind = ComponentKind.Utility;
|
componentKind = ComponentKind.Utility;
|
||||||
|
|||||||
@ -62,7 +62,7 @@ public sealed class PriorityUsageAnalyzer : DiagnosticAnalyzer
|
|||||||
var method = invocation.TargetMethod;
|
var method = invocation.TargetMethod;
|
||||||
|
|
||||||
// 检查方法名是否为 GetAll
|
// 检查方法名是否为 GetAll
|
||||||
if (method.Name != "GetAll")
|
if (!string.Equals(method.Name, "GetAll", StringComparison.Ordinal))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// 检查是否为泛型方法
|
// 检查是否为泛型方法
|
||||||
|
|||||||
@ -179,12 +179,7 @@ public sealed class AutoRegisterModuleGenerator : IIncrementalGenerator
|
|||||||
{
|
{
|
||||||
var registrations = new List<RegistrationSpec>();
|
var registrations = new List<RegistrationSpec>();
|
||||||
|
|
||||||
foreach (var attribute in typeSymbol.GetAttributes()
|
foreach (var attribute in GetOrderedRegistrationAttributes(typeSymbol))
|
||||||
// Roslyn 会把 partial 类型上的属性合并到同一个集合中。
|
|
||||||
// 先按语法树标识排序,才能让每个文件内的 Span.Start 成为可比较的稳定顺序键。
|
|
||||||
.OrderBy(GetAttributeSyntaxTreeOrderKey, StringComparer.Ordinal)
|
|
||||||
.ThenBy(GetAttributeOrder)
|
|
||||||
.ThenBy(GetAttributeTypeOrderKey, StringComparer.Ordinal))
|
|
||||||
{
|
{
|
||||||
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, registerModelAttribute))
|
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, registerModelAttribute))
|
||||||
{
|
{
|
||||||
@ -239,6 +234,16 @@ public sealed class AutoRegisterModuleGenerator : IIncrementalGenerator
|
|||||||
return registrations;
|
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(
|
private static bool TryCreateRegistration(
|
||||||
SourceProductionContext context,
|
SourceProductionContext context,
|
||||||
INamedTypeSymbol ownerType,
|
INamedTypeSymbol ownerType,
|
||||||
@ -323,7 +328,8 @@ public sealed class AutoRegisterModuleGenerator : IIncrementalGenerator
|
|||||||
RegistrationKind.Model => "RegisterModel",
|
RegistrationKind.Model => "RegisterModel",
|
||||||
RegistrationKind.System => "RegisterSystem",
|
RegistrationKind.System => "RegisterSystem",
|
||||||
RegistrationKind.Utility => "RegisterUtility",
|
RegistrationKind.Utility => "RegisterUtility",
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(registration.Kind))
|
_ => throw new InvalidOperationException(
|
||||||
|
$"Unsupported registration kind '{registration.Kind}'.")
|
||||||
});
|
});
|
||||||
builder.Append("(new ");
|
builder.Append("(new ");
|
||||||
builder.Append(registration.ComponentTypeDisplayName);
|
builder.Append(registration.ComponentTypeDisplayName);
|
||||||
|
|||||||
@ -72,14 +72,7 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase
|
|||||||
? null
|
? null
|
||||||
: symbol.ContainingNamespace.ToDisplayString();
|
: symbol.ContainingNamespace.ToDisplayString();
|
||||||
|
|
||||||
var generateIsMethods = GetNamedBooleanArgument(
|
var generationOptions = GetGenerationOptions(attr);
|
||||||
attr,
|
|
||||||
nameof(GenerateEnumExtensionsAttribute.GenerateIsMethods),
|
|
||||||
true);
|
|
||||||
var generateIsInMethod = GetNamedBooleanArgument(
|
|
||||||
attr,
|
|
||||||
nameof(GenerateEnumExtensionsAttribute.GenerateIsInMethod),
|
|
||||||
true);
|
|
||||||
var enumName = symbol.Name;
|
var enumName = symbol.Name;
|
||||||
var fullEnumName = symbol.ToDisplayString();
|
var fullEnumName = symbol.ToDisplayString();
|
||||||
var members = symbol.GetMembers()
|
var members = symbol.GetMembers()
|
||||||
@ -104,7 +97,7 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase
|
|||||||
// 两个生成开关是彼此独立的契约,需要分别控制输出,并保持空行布局稳定,便于快照精确回归。
|
// 两个生成开关是彼此独立的契约,需要分别控制输出,并保持空行布局稳定,便于快照精确回归。
|
||||||
var hasGeneratedMembers = false;
|
var hasGeneratedMembers = false;
|
||||||
|
|
||||||
if (generateIsMethods)
|
if (generationOptions.GenerateIsMethods)
|
||||||
{
|
{
|
||||||
hasGeneratedMembers = AppendIsMethods(
|
hasGeneratedMembers = AppendIsMethods(
|
||||||
sb,
|
sb,
|
||||||
@ -112,7 +105,7 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase
|
|||||||
fullEnumName);
|
fullEnumName);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (generateIsInMethod)
|
if (generationOptions.GenerateIsInMethod)
|
||||||
{
|
{
|
||||||
if (hasGeneratedMembers)
|
if (hasGeneratedMembers)
|
||||||
{
|
{
|
||||||
@ -130,6 +123,24 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase
|
|||||||
return sb.ToString();
|
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>
|
||||||
/// 获取生成文件的提示名称
|
/// 获取生成文件的提示名称
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -151,7 +162,7 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase
|
|||||||
{
|
{
|
||||||
foreach (var namedArgument in attribute.NamedArguments)
|
foreach (var namedArgument in attribute.NamedArguments)
|
||||||
{
|
{
|
||||||
if (namedArgument.Key == argumentName &&
|
if (string.Equals(namedArgument.Key, argumentName, StringComparison.Ordinal) &&
|
||||||
namedArgument.Value.Value is bool value)
|
namedArgument.Value.Value is bool value)
|
||||||
{
|
{
|
||||||
return value;
|
return value;
|
||||||
|
|||||||
@ -118,6 +118,24 @@ public class NumericExtensionsTests
|
|||||||
Assert.Throws<ArgumentException>(() => value.Between(100, 0));
|
Assert.Throws<ArgumentException>(() => value.Between(100, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试Between方法在引用类型参数为null时抛出ArgumentNullException
|
||||||
|
/// </summary>
|
||||||
|
[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<ArgumentNullException>(() => NumericExtensions.Between(value!, null!, max!));
|
||||||
|
Assert.Throws<ArgumentNullException>(() => NumericExtensions.Between(value!, min!, null!));
|
||||||
|
Assert.Throws<ArgumentNullException>(() => NumericExtensions.Between(null!, min!, max!));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 测试Lerp方法在t为0时返回起始值
|
/// 测试Lerp方法在t为0时返回起始值
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -205,4 +223,4 @@ public class NumericExtensionsTests
|
|||||||
// Arrange & Act & Assert
|
// Arrange & Act & Assert
|
||||||
Assert.Throws<DivideByZeroException>(() => 50f.InverseLerp(100f, 100f));
|
Assert.Throws<DivideByZeroException>(() => 50f.InverseLerp(100f, 100f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -319,7 +319,12 @@ public class ResultExtensionsTests
|
|||||||
{
|
{
|
||||||
var result = Result<int>.Succeed(-1);
|
var result = Result<int>.Succeed(-1);
|
||||||
var ensured = result.Ensure(x => x > 0, "Value must be positive");
|
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<ArgumentException>());
|
||||||
|
Assert.That(((ArgumentException)ensured.Exception).ParamName, Is.EqualTo("result"));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -635,4 +640,4 @@ public class ResultExtensionsTests
|
|||||||
|
|
||||||
Assert.That(result.IsSuccess, Is.True);
|
Assert.That(result.IsSuccess, Is.True);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -429,4 +429,20 @@ public class ResultTests
|
|||||||
// Assert
|
// Assert
|
||||||
Assert.That(str, Is.EqualTo("Fail(Test error)"));
|
Assert.That(str, Is.EqualTo("Fail(Test error)"));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试default(Result)不会在相等性比较、哈希计算和字符串格式化中触发空引用异常
|
||||||
|
/// </summary>
|
||||||
|
[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)"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
163
GFramework.Core.Tests/Logging/ConfigurableLoggerFactoryTests.cs
Normal file
163
GFramework.Core.Tests/Logging/ConfigurableLoggerFactoryTests.cs
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
using GFramework.Core.Abstractions.Logging;
|
||||||
|
using GFramework.Core.Logging;
|
||||||
|
|
||||||
|
namespace GFramework.Core.Tests.Logging;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证可配置 Logger 工厂在配置归一化、级别合并与释放路径上的行为契约。
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture]
|
||||||
|
public sealed class ConfigurableLoggerFactoryTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 验证当反序列化结果把集合字段写成 <see langword="null" /> 时,工厂会将其归一化为空集合而不是抛出空引用异常。
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证当配置输入把 appenders 集合中的某个元素反序列化为 <see langword="null" /> 时,工厂会抛出可诊断异常。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void CreateFactory_ShouldThrowInvalidOperationException_WhenAppenderEntryIsNull()
|
||||||
|
{
|
||||||
|
var config = LoggingConfigurationLoader.LoadFromJsonString(
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"appenders": [ null ]
|
||||||
|
}
|
||||||
|
""");
|
||||||
|
|
||||||
|
var exception = Assert.Throws<InvalidOperationException>(() => LoggingConfigurationLoader.CreateFactory(config));
|
||||||
|
|
||||||
|
Assert.That(exception!.Message, Is.EqualTo("Appender configuration cannot be null."));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证在未命中命名空间覆盖时,调用方传入的默认最小级别会作为最终 logger 级别的下限参与计算。
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证命名空间覆盖级别会优先于调用方传入的默认最小级别,确保覆盖配置保持最高优先级。
|
||||||
|
/// </summary>
|
||||||
|
[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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证调用方传入空 logger 名称时,会得到显式的参数异常而不是后续字符串操作的空引用异常。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void GetLogger_WithNullName_ShouldThrowArgumentNullException()
|
||||||
|
{
|
||||||
|
var factory = LoggingConfigurationLoader.CreateFactory(new LoggingConfiguration());
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentNullException>(() => factory.GetLogger(null!));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证工厂释放时会兼容释放未实现 <see cref="IDisposable" /> 的异步 appender,并让既有 logger 观察到已释放状态。
|
||||||
|
/// </summary>
|
||||||
|
[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");
|
||||||
|
|
||||||
|
logger.Info("dispose-path");
|
||||||
|
|
||||||
|
((IDisposable)factory).Dispose();
|
||||||
|
|
||||||
|
Assert.Throws<ObjectDisposedException>(() => logger.Info("after-dispose"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -143,6 +143,35 @@ public class LoggingConfigurationTests
|
|||||||
Assert.That(logger3.IsInfoEnabled(), Is.True);
|
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]
|
[Test]
|
||||||
public void CreateFactory_WithInvalidAppenderType_ShouldThrowException()
|
public void CreateFactory_WithInvalidAppenderType_ShouldThrowException()
|
||||||
{
|
{
|
||||||
@ -308,4 +337,4 @@ public class LoggingConfigurationTests
|
|||||||
Assert.That(config.Appenders[2].MaxFileSize, Is.EqualTo(10485760));
|
Assert.That(config.Appenders[2].MaxFileSize, Is.EqualTo(10485760));
|
||||||
Assert.That(config.LoggerLevels.Count, Is.EqualTo(3));
|
Assert.That(config.LoggerLevels.Count, Is.EqualTo(3));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -250,7 +250,7 @@ public class TestStateMachineSystemV5 : StateMachineSystem
|
|||||||
/// 获取状态机内部的状态字典
|
/// 获取状态机内部的状态字典
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>类型到状态实例的映射字典</returns>
|
/// <returns>类型到状态实例的映射字典</returns>
|
||||||
public Dictionary<Type, IState> GetStates()
|
public IDictionary<Type, IState> GetStates()
|
||||||
{
|
{
|
||||||
return States;
|
return States;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -288,7 +288,7 @@ public abstract class Architecture : IArchitecture
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await InitializeInternalAsync(true);
|
await InitializeInternalAsync(true).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -304,7 +304,8 @@ public abstract class Architecture : IArchitecture
|
|||||||
/// <param name="asyncMode">是否启用异步模式</param>
|
/// <param name="asyncMode">是否启用异步模式</param>
|
||||||
private async Task InitializeInternalAsync(bool asyncMode)
|
private async Task InitializeInternalAsync(bool asyncMode)
|
||||||
{
|
{
|
||||||
_context = await _bootstrapper.PrepareForInitializationAsync(_context, Configurator, asyncMode);
|
_context = await _bootstrapper.PrepareForInitializationAsync(_context, Configurator, asyncMode)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
// === 用户 OnInitialize ===
|
// === 用户 OnInitialize ===
|
||||||
_logger.Debug("Calling user OnInitialize()");
|
_logger.Debug("Calling user OnInitialize()");
|
||||||
@ -312,7 +313,7 @@ public abstract class Architecture : IArchitecture
|
|||||||
_logger.Debug("User OnInitialize() completed");
|
_logger.Debug("User OnInitialize() completed");
|
||||||
|
|
||||||
// === 组件初始化阶段 ===
|
// === 组件初始化阶段 ===
|
||||||
await _lifecycle.InitializeAllComponentsAsync(asyncMode);
|
await _lifecycle.InitializeAllComponentsAsync(asyncMode).ConfigureAwait(false);
|
||||||
|
|
||||||
// === 初始化完成阶段 ===
|
// === 初始化完成阶段 ===
|
||||||
_bootstrapper.CompleteInitialization();
|
_bootstrapper.CompleteInitialization();
|
||||||
@ -337,7 +338,7 @@ public abstract class Architecture : IArchitecture
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async ValueTask DestroyAsync()
|
public virtual async ValueTask DestroyAsync()
|
||||||
{
|
{
|
||||||
await _lifecycle.DestroyAsync();
|
await _lifecycle.DestroyAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -35,7 +35,7 @@ internal sealed class ArchitectureBootstrapper(
|
|||||||
|
|
||||||
var context = EnsureContext(existingContext);
|
var context = EnsureContext(existingContext);
|
||||||
ConfigureServices(context, configurator);
|
ConfigureServices(context, configurator);
|
||||||
await InitializeServiceModulesAsync(asyncMode);
|
await InitializeServiceModulesAsync(asyncMode).ConfigureAwait(false);
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,6 +117,6 @@ internal sealed class ArchitectureBootstrapper(
|
|||||||
/// <param name="asyncMode">是否允许异步初始化服务模块。</param>
|
/// <param name="asyncMode">是否允许异步初始化服务模块。</param>
|
||||||
private async Task InitializeServiceModulesAsync(bool asyncMode)
|
private async Task InitializeServiceModulesAsync(bool asyncMode)
|
||||||
{
|
{
|
||||||
await services.ModuleManager.InitializeAllAsync(asyncMode);
|
await services.ModuleManager.InitializeAllAsync(asyncMode).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -97,7 +97,7 @@ public class ArchitectureContext : IArchitectureContext
|
|||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(request);
|
ArgumentNullException.ThrowIfNull(request);
|
||||||
return await CqrsRuntime.SendAsync(this, request, cancellationToken);
|
return await CqrsRuntime.SendAsync(this, request, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -124,7 +124,7 @@ public class ArchitectureContext : IArchitectureContext
|
|||||||
where TNotification : INotification
|
where TNotification : INotification
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(notification);
|
ArgumentNullException.ThrowIfNull(notification);
|
||||||
await CqrsRuntime.PublishAsync(this, notification, cancellationToken);
|
await CqrsRuntime.PublishAsync(this, notification, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -151,7 +151,7 @@ public class ArchitectureContext : IArchitectureContext
|
|||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
where TCommand : IRequest<Unit>
|
where TCommand : IRequest<Unit>
|
||||||
{
|
{
|
||||||
await SendRequestAsync(command, cancellationToken);
|
await SendRequestAsync(command, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -162,7 +162,7 @@ public class ArchitectureContext : IArchitectureContext
|
|||||||
IRequest<TResponse> command,
|
IRequest<TResponse> command,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return await SendRequestAsync(command, cancellationToken);
|
return await SendRequestAsync(command, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -205,7 +205,7 @@ public class ArchitectureContext : IArchitectureContext
|
|||||||
if (query == null) throw new ArgumentNullException(nameof(query));
|
if (query == null) throw new ArgumentNullException(nameof(query));
|
||||||
var asyncQueryBus = GetOrCache<IAsyncQueryExecutor>();
|
var asyncQueryBus = GetOrCache<IAsyncQueryExecutor>();
|
||||||
if (asyncQueryBus == null) throw new InvalidOperationException("IAsyncQueryExecutor not registered");
|
if (asyncQueryBus == null) throw new InvalidOperationException("IAsyncQueryExecutor not registered");
|
||||||
return await asyncQueryBus.SendAsync(query);
|
return await asyncQueryBus.SendAsync(query).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -219,7 +219,7 @@ public class ArchitectureContext : IArchitectureContext
|
|||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(query);
|
ArgumentNullException.ThrowIfNull(query);
|
||||||
return await SendRequestAsync(query, cancellationToken);
|
return await SendRequestAsync(query, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -356,7 +356,7 @@ public class ArchitectureContext : IArchitectureContext
|
|||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(command);
|
ArgumentNullException.ThrowIfNull(command);
|
||||||
return await SendRequestAsync(command, cancellationToken);
|
return await SendRequestAsync(command, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -368,7 +368,7 @@ public class ArchitectureContext : IArchitectureContext
|
|||||||
ArgumentNullException.ThrowIfNull(command);
|
ArgumentNullException.ThrowIfNull(command);
|
||||||
var commandBus = GetOrCache<ICommandExecutor>();
|
var commandBus = GetOrCache<ICommandExecutor>();
|
||||||
if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered");
|
if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered");
|
||||||
await commandBus.SendAsync(command);
|
await commandBus.SendAsync(command).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -382,7 +382,7 @@ public class ArchitectureContext : IArchitectureContext
|
|||||||
ArgumentNullException.ThrowIfNull(command);
|
ArgumentNullException.ThrowIfNull(command);
|
||||||
var commandBus = GetOrCache<ICommandExecutor>();
|
var commandBus = GetOrCache<ICommandExecutor>();
|
||||||
if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered");
|
if (commandBus == null) throw new InvalidOperationException("ICommandExecutor not registered");
|
||||||
return await commandBus.SendAsync(command);
|
return await commandBus.SendAsync(command).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -59,15 +59,15 @@ internal sealed class ArchitectureDisposer(
|
|||||||
if (currentPhase == ArchitecturePhase.None)
|
if (currentPhase == ArchitecturePhase.None)
|
||||||
{
|
{
|
||||||
logger.Debug("Architecture destroy called but never initialized, cleaning up registered components");
|
logger.Debug("Architecture destroy called but never initialized, cleaning up registered components");
|
||||||
await CleanupComponentsAsync();
|
await CleanupComponentsAsync().ConfigureAwait(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("Starting architecture destruction");
|
logger.Info("Starting architecture destruction");
|
||||||
enterPhase(ArchitecturePhase.Destroying);
|
enterPhase(ArchitecturePhase.Destroying);
|
||||||
|
|
||||||
await CleanupComponentsAsync();
|
await CleanupComponentsAsync().ConfigureAwait(false);
|
||||||
await services.ModuleManager.DestroyAllAsync();
|
await services.ModuleManager.DestroyAllAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
// Destroyed 广播依赖容器中的阶段监听器,必须在清空容器前完成。
|
// Destroyed 广播依赖容器中的阶段监听器,必须在清空容器前完成。
|
||||||
enterPhase(ArchitecturePhase.Destroyed);
|
enterPhase(ArchitecturePhase.Destroyed);
|
||||||
@ -93,7 +93,7 @@ internal sealed class ArchitectureDisposer(
|
|||||||
|
|
||||||
if (component is IAsyncDestroyable asyncDestroyable)
|
if (component is IAsyncDestroyable asyncDestroyable)
|
||||||
{
|
{
|
||||||
await asyncDestroyable.DestroyAsync();
|
await asyncDestroyable.DestroyAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else if (component is IDestroyable destroyable)
|
else if (component is IDestroyable destroyable)
|
||||||
{
|
{
|
||||||
@ -109,4 +109,4 @@ internal sealed class ArchitectureDisposer(
|
|||||||
_disposables.Clear();
|
_disposables.Clear();
|
||||||
_disposableSet.Clear();
|
_disposableSet.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -160,7 +160,7 @@ internal sealed class ArchitectureLifecycle(
|
|||||||
foreach (var utility in utilities)
|
foreach (var utility in utilities)
|
||||||
{
|
{
|
||||||
logger.Debug($"Initializing utility: {utility.GetType().Name}");
|
logger.Debug($"Initializing utility: {utility.GetType().Name}");
|
||||||
await InitializeComponentAsync(utility, asyncMode);
|
await InitializeComponentAsync(utility, asyncMode).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("All context utilities initialized");
|
logger.Info("All context utilities initialized");
|
||||||
@ -178,7 +178,7 @@ internal sealed class ArchitectureLifecycle(
|
|||||||
foreach (var model in models)
|
foreach (var model in models)
|
||||||
{
|
{
|
||||||
logger.Debug($"Initializing model: {model.GetType().Name}");
|
logger.Debug($"Initializing model: {model.GetType().Name}");
|
||||||
await InitializeComponentAsync(model, asyncMode);
|
await InitializeComponentAsync(model, asyncMode).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("All models initialized");
|
logger.Info("All models initialized");
|
||||||
@ -196,7 +196,7 @@ internal sealed class ArchitectureLifecycle(
|
|||||||
foreach (var system in systems)
|
foreach (var system in systems)
|
||||||
{
|
{
|
||||||
logger.Debug($"Initializing system: {system.GetType().Name}");
|
logger.Debug($"Initializing system: {system.GetType().Name}");
|
||||||
await InitializeComponentAsync(system, asyncMode);
|
await InitializeComponentAsync(system, asyncMode).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("All systems initialized");
|
logger.Info("All systems initialized");
|
||||||
@ -218,7 +218,7 @@ internal sealed class ArchitectureLifecycle(
|
|||||||
private static async Task InitializeComponentAsync(IInitializable component, bool asyncMode)
|
private static async Task InitializeComponentAsync(IInitializable component, bool asyncMode)
|
||||||
{
|
{
|
||||||
if (asyncMode && component is IAsyncInitializable asyncInit)
|
if (asyncMode && component is IAsyncInitializable asyncInit)
|
||||||
await asyncInit.InitializeAsync();
|
await asyncInit.InitializeAsync().ConfigureAwait(false);
|
||||||
else
|
else
|
||||||
component.Initialize();
|
component.Initialize();
|
||||||
}
|
}
|
||||||
@ -244,7 +244,7 @@ internal sealed class ArchitectureLifecycle(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async ValueTask DestroyAsync()
|
public async ValueTask DestroyAsync()
|
||||||
{
|
{
|
||||||
await _disposer.DestroyAsync(CurrentPhase, EnterPhase);
|
await _disposer.DestroyAsync(CurrentPhase, EnterPhase).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -285,4 +285,4 @@ internal sealed class ArchitectureLifecycle(
|
|||||||
public Task WaitUntilReadyAsync() => _readyTcs.Task;
|
public Task WaitUntilReadyAsync() => _readyTcs.Task;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ public abstract class AbstractAsyncCommand : ContextAwareBase, IAsyncCommand
|
|||||||
/// <returns>表示异步操作的任务</returns>
|
/// <returns>表示异步操作的任务</returns>
|
||||||
async Task IAsyncCommand.ExecuteAsync()
|
async Task IAsyncCommand.ExecuteAsync()
|
||||||
{
|
{
|
||||||
await OnExecuteAsync();
|
await OnExecuteAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -25,4 +25,4 @@ public abstract class AbstractAsyncCommand : ContextAwareBase, IAsyncCommand
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>表示异步操作的任务</returns>
|
/// <returns>表示异步操作的任务</returns>
|
||||||
protected abstract Task OnExecuteAsync();
|
protected abstract Task OnExecuteAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ public abstract class AbstractAsyncCommand<TInput>(TInput input) : ContextAwareB
|
|||||||
/// <returns>表示异步操作的任务</returns>
|
/// <returns>表示异步操作的任务</returns>
|
||||||
async Task IAsyncCommand.ExecuteAsync()
|
async Task IAsyncCommand.ExecuteAsync()
|
||||||
{
|
{
|
||||||
await OnExecuteAsync(input);
|
await OnExecuteAsync(input).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -18,7 +18,7 @@ public abstract class AbstractAsyncCommand<TInput, TResult>(TInput input) : Cont
|
|||||||
/// <returns>表示异步操作且包含结果的任务</returns>
|
/// <returns>表示异步操作且包含结果的任务</returns>
|
||||||
async Task<TResult> IAsyncCommand<TResult>.ExecuteAsync()
|
async Task<TResult> IAsyncCommand<TResult>.ExecuteAsync()
|
||||||
{
|
{
|
||||||
return await OnExecuteAsync(input);
|
return await OnExecuteAsync(input).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -22,7 +22,7 @@ namespace GFramework.Core.Concurrency;
|
|||||||
public sealed class AsyncKeyLockManager : IAsyncKeyLockManager
|
public sealed class AsyncKeyLockManager : IAsyncKeyLockManager
|
||||||
{
|
{
|
||||||
private readonly Timer _cleanupTimer;
|
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 readonly long _lockTimeoutMs;
|
||||||
private volatile bool _disposed;
|
private volatile bool _disposed;
|
||||||
|
|
||||||
@ -119,7 +119,8 @@ public sealed class AsyncKeyLockManager : IAsyncKeyLockManager
|
|||||||
LastAccessTicks = kvp.Value.LastAccessTicks,
|
LastAccessTicks = kvp.Value.LastAccessTicks,
|
||||||
// CurrentCount == 0 表示锁被持有,可能有等待者(近似值)
|
// CurrentCount == 0 表示锁被持有,可能有等待者(近似值)
|
||||||
WaitingCount = kvp.Value.Semaphore.CurrentCount == 0 ? 1 : 0
|
WaitingCount = kvp.Value.Semaphore.CurrentCount == 0 ? 1 : 0
|
||||||
});
|
},
|
||||||
|
StringComparer.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -187,4 +188,4 @@ public sealed class AsyncKeyLockManager : IAsyncKeyLockManager
|
|||||||
Semaphore.Dispose();
|
Semaphore.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using GFramework.Core.Abstractions.Configuration;
|
using GFramework.Core.Abstractions.Configuration;
|
||||||
@ -32,7 +33,7 @@ public class ConfigurationManager : IConfigurationManager
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 配置存储字典(线程安全)
|
/// 配置存储字典(线程安全)
|
||||||
/// </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));
|
private readonly ILogger _logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(ConfigurationManager));
|
||||||
|
|
||||||
@ -45,7 +46,7 @@ public class ConfigurationManager : IConfigurationManager
|
|||||||
/// 配置监听器字典(线程安全)
|
/// 配置监听器字典(线程安全)
|
||||||
/// 键:配置键,值:监听器列表
|
/// 键:配置键,值:监听器列表
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly ConcurrentDictionary<string, List<Delegate>> _watchers = new();
|
private readonly ConcurrentDictionary<string, List<Delegate>> _watchers = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取配置数量
|
/// 获取配置数量
|
||||||
@ -200,7 +201,7 @@ public class ConfigurationManager : IConfigurationManager
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string SaveToJson()
|
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
|
return JsonSerializer.Serialize(dict, new JsonSerializerOptions
|
||||||
{
|
{
|
||||||
WriteIndented = true
|
WriteIndented = true
|
||||||
@ -321,11 +322,11 @@ public class ConfigurationManager : IConfigurationManager
|
|||||||
// 尝试类型转换
|
// 尝试类型转换
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return (T)Convert.ChangeType(value, typeof(T));
|
return (T)Convert.ChangeType(value, typeof(T), CultureInfo.InvariantCulture);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return default!;
|
return default!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -85,6 +85,15 @@ public readonly struct CoroutineHandle : IEquatable<CoroutineHandle>
|
|||||||
return _id;
|
return _id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回协程句柄的稳定字符串表示,用于日志和诊断输出。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>包含内部标识符与键值的诊断字符串。</returns>
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"CoroutineHandle(Id={_id}, Key={Key})";
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 比较两个协程句柄是否相等
|
/// 比较两个协程句柄是否相等
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -106,4 +115,4 @@ public readonly struct CoroutineHandle : IEquatable<CoroutineHandle>
|
|||||||
{
|
{
|
||||||
return a._id != b._id;
|
return a._id != b._id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -742,7 +742,7 @@ public sealed class CoroutineScheduler(
|
|||||||
var handler = OnCoroutineException;
|
var handler = OnCoroutineException;
|
||||||
if (handler != null)
|
if (handler != null)
|
||||||
{
|
{
|
||||||
Task.Run(() =>
|
_ = Task.Run(() =>
|
||||||
{
|
{
|
||||||
try
|
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);
|
FinalizeCoroutine(slotIndex, CoroutineCompletionStatus.Faulted, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1003,4 +1003,4 @@ public sealed class CoroutineScheduler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Globalization;
|
||||||
using GFramework.Core.Abstractions.Coroutine;
|
using GFramework.Core.Abstractions.Coroutine;
|
||||||
|
|
||||||
namespace GFramework.Core.Coroutine;
|
namespace GFramework.Core.Coroutine;
|
||||||
@ -10,7 +11,7 @@ namespace GFramework.Core.Coroutine;
|
|||||||
internal sealed class CoroutineStatistics : ICoroutineStatistics
|
internal sealed class CoroutineStatistics : ICoroutineStatistics
|
||||||
{
|
{
|
||||||
private readonly Dictionary<CoroutinePriority, int> _countByPriority = new();
|
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 readonly object _lock = new();
|
||||||
private int _activeCount;
|
private int _activeCount;
|
||||||
private double _maxExecutionTimeMs;
|
private double _maxExecutionTimeMs;
|
||||||
@ -109,29 +110,31 @@ internal sealed class CoroutineStatistics : ICoroutineStatistics
|
|||||||
public string GenerateReport()
|
public string GenerateReport()
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine("=== 协程统计报告 ===");
|
sb.AppendLine(FormattableString.Invariant($"=== 协程统计报告 ==="));
|
||||||
sb.AppendLine($"总启动数: {TotalStarted}");
|
sb.AppendLine(FormattableString.Invariant($"总启动数: {TotalStarted}"));
|
||||||
sb.AppendLine($"总完成数: {TotalCompleted}");
|
sb.AppendLine(FormattableString.Invariant($"总完成数: {TotalCompleted}"));
|
||||||
sb.AppendLine($"总失败数: {TotalFailed}");
|
sb.AppendLine(FormattableString.Invariant($"总失败数: {TotalFailed}"));
|
||||||
sb.AppendLine($"当前活跃: {ActiveCount}");
|
sb.AppendLine(FormattableString.Invariant($"当前活跃: {ActiveCount}"));
|
||||||
sb.AppendLine($"当前暂停: {PausedCount}");
|
sb.AppendLine(FormattableString.Invariant($"当前暂停: {PausedCount}"));
|
||||||
sb.AppendLine($"平均执行时间: {AverageExecutionTimeMs:F2} ms");
|
sb.AppendLine(FormattableString.Invariant(
|
||||||
sb.AppendLine($"最大执行时间: {MaxExecutionTimeMs:F2} ms");
|
$"平均执行时间: {AverageExecutionTimeMs.ToString("F2", CultureInfo.InvariantCulture)} ms"));
|
||||||
|
sb.AppendLine(FormattableString.Invariant(
|
||||||
|
$"最大执行时间: {MaxExecutionTimeMs.ToString("F2", CultureInfo.InvariantCulture)} ms"));
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
if (_countByPriority.Count > 0)
|
if (_countByPriority.Count > 0)
|
||||||
{
|
{
|
||||||
sb.AppendLine("\n按优先级统计:");
|
sb.AppendLine(FormattableString.Invariant($"\n按优先级统计:"));
|
||||||
foreach (var kvp in _countByPriority.OrderByDescending(x => x.Key))
|
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)
|
if (_countByTag.Count > 0)
|
||||||
{
|
{
|
||||||
sb.AppendLine("\n按标签统计:");
|
sb.AppendLine(FormattableString.Invariant($"\n按标签统计:"));
|
||||||
foreach (var kvp in _countByTag.OrderByDescending(x => x.Value))
|
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
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ public sealed class WaitForTask<T> : IYieldInstruction
|
|||||||
_done = true;
|
_done = true;
|
||||||
else
|
else
|
||||||
// 注册完成回调
|
// 注册完成回调
|
||||||
_task.ContinueWith(_ => { _done = true; }, TaskContinuationOptions.ExecuteSynchronously);
|
_ = _task.ContinueWith(_ => { _done = true; }, TaskContinuationOptions.ExecuteSynchronously);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -50,4 +50,4 @@ public sealed class WaitForTask<T> : IYieldInstruction
|
|||||||
/// 获取等待是否已完成
|
/// 获取等待是否已完成
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsDone => _done;
|
public bool IsDone => _done;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,9 +2,6 @@ using GFramework.Core.Abstractions.Coroutine;
|
|||||||
|
|
||||||
namespace GFramework.Core.Coroutine.Instructions;
|
namespace GFramework.Core.Coroutine.Instructions;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 等待Task完成的等待指令
|
|
||||||
/// </summary>
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 等待Task完成的等待指令
|
/// 等待Task完成的等待指令
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -26,7 +23,7 @@ public sealed class WaitForTask : IYieldInstruction
|
|||||||
_done = true;
|
_done = true;
|
||||||
else
|
else
|
||||||
// 注册完成回调
|
// 注册完成回调
|
||||||
_task.ContinueWith(_ => { _done = true; }, TaskContinuationOptions.ExecuteSynchronously);
|
_ = _task.ContinueWith(_ => { _done = true; }, TaskContinuationOptions.ExecuteSynchronously);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -47,4 +44,4 @@ public sealed class WaitForTask : IYieldInstruction
|
|||||||
/// 获取等待是否已完成
|
/// 获取等待是否已完成
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsDone => _done;
|
public bool IsDone => _done;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ public abstract class EnvironmentBase : ContextAwareBase, IEnvironment
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 存储环境值的字典,键为字符串,值为对象类型
|
/// 存储环境值的字典,键为字符串,值为对象类型
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly Dictionary<string, object> Values = new();
|
protected readonly IDictionary<string, object> Values = new Dictionary<string, object>(StringComparer.Ordinal);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取环境名称的抽象属性
|
/// 获取环境名称的抽象属性
|
||||||
@ -84,4 +84,4 @@ public abstract class EnvironmentBase : ContextAwareBase, IEnvironment
|
|||||||
// 将键值对添加到Values字典中
|
// 将键值对添加到Values字典中
|
||||||
Values[key] = value;
|
Values[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,9 +9,9 @@ namespace GFramework.Core.Events;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class EventStatistics : IEventStatistics
|
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 object _lock = new();
|
||||||
private readonly Dictionary<string, long> _publishCountByType = new();
|
private readonly Dictionary<string, long> _publishCountByType = new(StringComparer.Ordinal);
|
||||||
private long _totalFailed;
|
private long _totalFailed;
|
||||||
private long _totalHandled;
|
private long _totalHandled;
|
||||||
private long _totalPublished;
|
private long _totalPublished;
|
||||||
@ -85,27 +85,27 @@ public sealed class EventStatistics : IEventStatistics
|
|||||||
public string GenerateReport()
|
public string GenerateReport()
|
||||||
{
|
{
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.AppendLine("=== 事件统计报告 ===");
|
sb.AppendLine(FormattableString.Invariant($"=== 事件统计报告 ==="));
|
||||||
sb.AppendLine($"总发布数: {TotalPublished}");
|
sb.AppendLine(FormattableString.Invariant($"总发布数: {TotalPublished}"));
|
||||||
sb.AppendLine($"总处理数: {TotalHandled}");
|
sb.AppendLine(FormattableString.Invariant($"总处理数: {TotalHandled}"));
|
||||||
sb.AppendLine($"总失败数: {TotalFailed}");
|
sb.AppendLine(FormattableString.Invariant($"总失败数: {TotalFailed}"));
|
||||||
sb.AppendLine($"活跃事件类型: {ActiveEventTypes}");
|
sb.AppendLine(FormattableString.Invariant($"活跃事件类型: {ActiveEventTypes}"));
|
||||||
sb.AppendLine($"活跃监听器: {ActiveListeners}");
|
sb.AppendLine(FormattableString.Invariant($"活跃监听器: {ActiveListeners}"));
|
||||||
|
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
if (_publishCountByType.Count > 0)
|
if (_publishCountByType.Count > 0)
|
||||||
{
|
{
|
||||||
sb.AppendLine("\n按事件类型统计(发布次数):");
|
sb.AppendLine(FormattableString.Invariant($"\n按事件类型统计(发布次数):"));
|
||||||
foreach (var kvp in _publishCountByType.OrderByDescending(x => x.Value))
|
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)
|
if (_listenerCountByType.Count > 0)
|
||||||
{
|
{
|
||||||
sb.AppendLine("\n按事件类型统计(监听器数量):");
|
sb.AppendLine(FormattableString.Invariant($"\n按事件类型统计(监听器数量):"));
|
||||||
foreach (var kvp in _listenerCountByType.OrderByDescending(x => x.Value))
|
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
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -118,11 +118,11 @@ public static class AsyncExtensions
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await task;
|
return await task.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return fallback(ex);
|
return fallback(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,7 +54,7 @@ public static class ContextAwareCommandExtensions
|
|||||||
ArgumentNullException.ThrowIfNull(command);
|
ArgumentNullException.ThrowIfNull(command);
|
||||||
|
|
||||||
var context = contextAware.GetContext();
|
var context = contextAware.GetContext();
|
||||||
await context.SendCommandAsync(command);
|
await context.SendCommandAsync(command).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -72,6 +72,6 @@ public static class ContextAwareCommandExtensions
|
|||||||
ArgumentNullException.ThrowIfNull(command);
|
ArgumentNullException.ThrowIfNull(command);
|
||||||
|
|
||||||
var context = contextAware.GetContext();
|
var context = contextAware.GetContext();
|
||||||
return await context.SendCommandAsync(command);
|
return await context.SendCommandAsync(command).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,6 +41,6 @@ public static class ContextAwareQueryExtensions
|
|||||||
ArgumentNullException.ThrowIfNull(query);
|
ArgumentNullException.ThrowIfNull(query);
|
||||||
|
|
||||||
var context = contextAware.GetContext();
|
var context = contextAware.GetContext();
|
||||||
return await context.SendQueryAsync(query);
|
return await context.SendQueryAsync(query).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,8 +25,12 @@ public static class NumericExtensions
|
|||||||
/// </example>
|
/// </example>
|
||||||
public static bool Between<T>(this T value, T min, T max, bool inclusive = true) where T : IComparable<T>
|
public static bool Between<T>(this T value, T min, T max, bool inclusive = true) where T : IComparable<T>
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(value);
|
||||||
|
ArgumentNullException.ThrowIfNull(min);
|
||||||
|
ArgumentNullException.ThrowIfNull(max);
|
||||||
|
|
||||||
if (min.CompareTo(max) > 0)
|
if (min.CompareTo(max) > 0)
|
||||||
throw new ArgumentException($"最小值 ({min}) 不能大于最大值 ({max})");
|
throw new ArgumentException($"最小值 ({min}) 不能大于最大值 ({max})", nameof(min));
|
||||||
|
|
||||||
if (inclusive)
|
if (inclusive)
|
||||||
return value.CompareTo(min) >= 0 && value.CompareTo(max) <= 0;
|
return value.CompareTo(min) >= 0 && value.CompareTo(max) <= 0;
|
||||||
@ -71,4 +75,4 @@ public static class NumericExtensions
|
|||||||
|
|
||||||
return (value - from) / (to - from);
|
return (value - from) / (to - from);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,14 +54,14 @@ public static class AsyncFunctionalExtensions
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await taskFactory();
|
return await taskFactory().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// 若还有重试机会且允许重试,则等待后继续;否则统一包装为 AggregateException 抛出
|
// 若还有重试机会且允许重试,则等待后继续;否则统一包装为 AggregateException 抛出
|
||||||
if (attempt < maxRetries && shouldRetry(ex))
|
if (attempt < maxRetries && shouldRetry(ex))
|
||||||
{
|
{
|
||||||
await Task.Delay(delay);
|
await Task.Delay(delay).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -99,7 +99,7 @@ public static class AsyncFunctionalExtensions
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await func();
|
var result = await func().ConfigureAwait(false);
|
||||||
return new Result<T>(result);
|
return new Result<T>(result);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -107,4 +107,4 @@ public static class AsyncFunctionalExtensions
|
|||||||
return new Result<T>(ex);
|
return new Result<T>(ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -201,7 +201,7 @@ public readonly struct Result<A> : IEquatable<Result<A>>, IComparable<Result<A>>
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return new Result<B>(await f(_value!));
|
return new Result<B>(await f(_value!).ConfigureAwait(false));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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 (IsSuccess) return EqualityComparer<A>.Default.Equals(_value, other._value);
|
||||||
if (IsFaulted)
|
if (IsFaulted)
|
||||||
return Exception.GetType() == other.Exception.GetType()
|
return Exception.GetType() == other.Exception.GetType()
|
||||||
&& Exception.Message == other.Exception.Message;
|
&& string.Equals(Exception.Message, other.Exception.Message, StringComparison.Ordinal);
|
||||||
return true; // both Bottom
|
return true; // both Bottom
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,4 +418,4 @@ public readonly struct Result<A> : IEquatable<Result<A>>, IComparable<Result<A>>
|
|||||||
ResultState.Faulted => $"Fail({Exception.Message})",
|
ResultState.Faulted => $"Fail({Exception.Message})",
|
||||||
_ => "(Bottom)"
|
_ => "(Bottom)"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -125,8 +125,11 @@ public readonly struct Result : IEquatable<Result>
|
|||||||
if (_isSuccess)
|
if (_isSuccess)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return _exception!.GetType() == other._exception!.GetType() &&
|
if (_exception is null || other._exception is null)
|
||||||
_exception.Message == other._exception.Message;
|
return _exception is null && other._exception is null;
|
||||||
|
|
||||||
|
return _exception.GetType() == other._exception.GetType() &&
|
||||||
|
string.Equals(_exception.Message, other._exception.Message, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -144,7 +147,12 @@ public readonly struct Result : IEquatable<Result>
|
|||||||
[Pure]
|
[Pure]
|
||||||
public override int GetHashCode()
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -171,7 +179,7 @@ public readonly struct Result : IEquatable<Result>
|
|||||||
/// <returns>Result 的字符串表示</returns>
|
/// <returns>Result 的字符串表示</returns>
|
||||||
[Pure]
|
[Pure]
|
||||||
public override string ToString() =>
|
public override string ToString() =>
|
||||||
_isSuccess ? "Success" : $"Fail({_exception!.Message})";
|
_isSuccess ? "Success" : (_exception != null ? $"Fail({_exception.Message})" : "Fail(null)");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 尝试执行一个无返回值的操作,并根据执行结果返回成功或失败的 Result
|
/// 尝试执行一个无返回值的操作,并根据执行结果返回成功或失败的 Result
|
||||||
@ -216,4 +224,4 @@ public readonly struct Result : IEquatable<Result>
|
|||||||
ArgumentNullException.ThrowIfNull(func);
|
ArgumentNullException.ThrowIfNull(func);
|
||||||
return IsSuccess ? func() : Result<B>.Failure(_exception!);
|
return IsSuccess ? func() : Result<B>.Failure(_exception!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -111,6 +111,7 @@ public static class ResultExtensions
|
|||||||
ArgumentNullException.ThrowIfNull(binder);
|
ArgumentNullException.ThrowIfNull(binder);
|
||||||
return result.IsSuccess
|
return result.IsSuccess
|
||||||
? await binder(result.Match(succ: v => v, fail: _ => throw new InvalidOperationException()))
|
? await binder(result.Match(succ: v => v, fail: _ => throw new InvalidOperationException()))
|
||||||
|
.ConfigureAwait(false)
|
||||||
: Result<TResult>.Fail(result.Exception);
|
: Result<TResult>.Fail(result.Exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,7 +177,7 @@ public static class ResultExtensions
|
|||||||
return result.Match(
|
return result.Match(
|
||||||
succ: value => predicate(value)
|
succ: value => predicate(value)
|
||||||
? result
|
? result
|
||||||
: Result<T>.Fail(new ArgumentException(errorMessage)),
|
: Result<T>.Fail(new ArgumentException(errorMessage, nameof(result))),
|
||||||
fail: _ => result
|
fail: _ => result
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -233,7 +234,7 @@ public static class ResultExtensions
|
|||||||
ArgumentNullException.ThrowIfNull(func);
|
ArgumentNullException.ThrowIfNull(func);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return Result<T>.Succeed(await func());
|
return Result<T>.Succeed(await func().ConfigureAwait(false));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -261,7 +262,7 @@ public static class ResultExtensions
|
|||||||
string errorMessage = "Value is null") where T : class =>
|
string errorMessage = "Value is null") where T : class =>
|
||||||
value is not null
|
value is not null
|
||||||
? Result<T>.Succeed(value)
|
? Result<T>.Succeed(value)
|
||||||
: Result<T>.Fail(new ArgumentNullException(errorMessage));
|
: Result<T>.Fail(new ArgumentNullException(nameof(value), errorMessage));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将可空值类型转换为 Result
|
/// 将可空值类型转换为 Result
|
||||||
@ -271,7 +272,7 @@ public static class ResultExtensions
|
|||||||
string errorMessage = "Value is null") where T : struct =>
|
string errorMessage = "Value is null") where T : struct =>
|
||||||
value.HasValue
|
value.HasValue
|
||||||
? Result<T>.Succeed(value.Value)
|
? Result<T>.Succeed(value.Value)
|
||||||
: Result<T>.Fail(new ArgumentNullException(errorMessage));
|
: Result<T>.Fail(new ArgumentNullException(nameof(value), errorMessage));
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -392,7 +392,10 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
|||||||
var assemblyArray = assemblies.ToArray();
|
var assemblyArray = assemblies.ToArray();
|
||||||
foreach (var assembly in assemblyArray)
|
foreach (var assembly in assemblyArray)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(assembly);
|
if (assembly is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(assemblies), "Assemblies collection cannot contain null items.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_lock.EnterWriteLock();
|
_lock.EnterWriteLock();
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
using System.Globalization;
|
||||||
using GFramework.Core.Abstractions.Localization;
|
using GFramework.Core.Abstractions.Localization;
|
||||||
using GFramework.Core.Abstractions.Utility.Numeric;
|
using GFramework.Core.Abstractions.Utility.Numeric;
|
||||||
using GFramework.Core.Utility.Numeric;
|
using GFramework.Core.Utility.Numeric;
|
||||||
@ -155,13 +156,14 @@ public sealed class CompactNumberLocalizationFormatter : ILocalizationFormatter
|
|||||||
ref bool trimTrailingZeros,
|
ref bool trimTrailingZeros,
|
||||||
ref bool useGroupingBelowThreshold)
|
ref bool useGroupingBelowThreshold)
|
||||||
{
|
{
|
||||||
|
var formatProvider = CultureInfo.InvariantCulture;
|
||||||
return key switch
|
return key switch
|
||||||
{
|
{
|
||||||
"maxDecimals" => int.TryParse(value, out maxDecimalPlaces),
|
"maxDecimals" => int.TryParse(value, NumberStyles.Integer, formatProvider, out maxDecimalPlaces),
|
||||||
"minDecimals" => int.TryParse(value, out minDecimalPlaces),
|
"minDecimals" => int.TryParse(value, NumberStyles.Integer, formatProvider, out minDecimalPlaces),
|
||||||
"trimZeros" => bool.TryParse(value, out trimTrailingZeros),
|
"trimZeros" => bool.TryParse(value, out trimTrailingZeros),
|
||||||
"grouping" => bool.TryParse(value, out useGroupingBelowThreshold),
|
"grouping" => bool.TryParse(value, out useGroupingBelowThreshold),
|
||||||
_ => true
|
_ => true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,8 +27,8 @@ public class LocalizationManager : AbstractSystem, ILocalizationManager
|
|||||||
public LocalizationManager(LocalizationConfig? config = null)
|
public LocalizationManager(LocalizationConfig? config = null)
|
||||||
{
|
{
|
||||||
_config = config ?? new LocalizationConfig();
|
_config = config ?? new LocalizationConfig();
|
||||||
_tables = new Dictionary<string, Dictionary<string, ILocalizationTable>>();
|
_tables = new Dictionary<string, Dictionary<string, ILocalizationTable>>(StringComparer.Ordinal);
|
||||||
_formatters = new Dictionary<string, ILocalizationFormatter>();
|
_formatters = new Dictionary<string, ILocalizationFormatter>(StringComparer.Ordinal);
|
||||||
_languageChangeCallbacks = new List<Action<string>>();
|
_languageChangeCallbacks = new List<Action<string>>();
|
||||||
_currentLanguage = _config.DefaultLanguage;
|
_currentLanguage = _config.DefaultLanguage;
|
||||||
_currentCulture = GetCultureInfo(_currentLanguage);
|
_currentCulture = GetCultureInfo(_currentLanguage);
|
||||||
@ -53,7 +53,7 @@ public class LocalizationManager : AbstractSystem, ILocalizationManager
|
|||||||
throw new ArgumentNullException(nameof(languageCode));
|
throw new ArgumentNullException(nameof(languageCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_currentLanguage == languageCode)
|
if (string.Equals(_currentLanguage, languageCode, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -227,11 +227,11 @@ public class LocalizationManager : AbstractSystem, ILocalizationManager
|
|||||||
return; // 已加载
|
return; // 已加载
|
||||||
}
|
}
|
||||||
|
|
||||||
var languageTables = new Dictionary<string, ILocalizationTable>();
|
var languageTables = new Dictionary<string, ILocalizationTable>(StringComparer.Ordinal);
|
||||||
|
|
||||||
// 加载回退语言(如果不是默认语言)
|
// 加载回退语言(如果不是默认语言)
|
||||||
Dictionary<string, ILocalizationTable>? fallbackTables = null;
|
Dictionary<string, ILocalizationTable>? fallbackTables = null;
|
||||||
if (languageCode != _config.FallbackLanguage)
|
if (!string.Equals(languageCode, _config.FallbackLanguage, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
LoadLanguage(_config.FallbackLanguage);
|
LoadLanguage(_config.FallbackLanguage);
|
||||||
_tables.TryGetValue(_config.FallbackLanguage, out fallbackTables);
|
_tables.TryGetValue(_config.FallbackLanguage, out fallbackTables);
|
||||||
@ -264,7 +264,7 @@ public class LocalizationManager : AbstractSystem, ILocalizationManager
|
|||||||
{
|
{
|
||||||
var json = File.ReadAllText(filePath);
|
var json = File.ReadAllText(filePath);
|
||||||
var data = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
var data = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||||
return data ?? new Dictionary<string, string>();
|
return data ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -314,4 +314,4 @@ public class LocalizationManager : AbstractSystem, ILocalizationManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,17 +8,35 @@ namespace GFramework.Core.Localization;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class LocalizationString : ILocalizationString
|
public class LocalizationString : ILocalizationString
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 正则分组名:变量名。
|
||||||
|
/// </summary>
|
||||||
|
private const string VariableGroupName = "variable";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 正则分组名:格式化器名。
|
||||||
|
/// </summary>
|
||||||
|
private const string FormatterGroupName = "formatter";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 正则分组名:格式化器参数。
|
||||||
|
/// </summary>
|
||||||
|
private const string FormatterArgsGroupName = "args";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 匹配 {variableName} 或 {variableName:formatter:args} 的正则表达式模式
|
/// 匹配 {variableName} 或 {variableName:formatter:args} 的正则表达式模式
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const string FormatVariablePattern =
|
private const string FormatVariablePattern =
|
||||||
@"\{([a-zA-Z_][a-zA-Z0-9_]*)(?::([a-zA-Z_][a-zA-Z0-9_]*)(?::([^}]+))?)?\}";
|
@"\{(?<variable>[a-zA-Z_][a-zA-Z0-9_]*)(?::(?<formatter>[a-zA-Z_][a-zA-Z0-9_]*)(?::(?<args>[^}]+))?)?\}";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 预编译的静态正则表达式,用于格式化字符串中的变量替换
|
/// 预编译的静态正则表达式,用于格式化字符串中的变量替换
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly Regex FormatVariableRegex =
|
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 ILocalizationManager _manager;
|
||||||
private readonly Dictionary<string, object> _variables;
|
private readonly Dictionary<string, object> _variables;
|
||||||
@ -35,7 +53,7 @@ public class LocalizationString : ILocalizationString
|
|||||||
_manager = manager ?? throw new ArgumentNullException(nameof(manager));
|
_manager = manager ?? throw new ArgumentNullException(nameof(manager));
|
||||||
Table = table ?? throw new ArgumentNullException(nameof(table));
|
Table = table ?? throw new ArgumentNullException(nameof(table));
|
||||||
Key = key ?? throw new ArgumentNullException(nameof(key));
|
Key = key ?? throw new ArgumentNullException(nameof(key));
|
||||||
_variables = new Dictionary<string, object>();
|
_variables = new Dictionary<string, object>(StringComparer.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
@ -154,13 +172,13 @@ public class LocalizationString : ILocalizationString
|
|||||||
Dictionary<string, object> variables,
|
Dictionary<string, object> variables,
|
||||||
ILocalizationManager manager)
|
ILocalizationManager manager)
|
||||||
{
|
{
|
||||||
var variableName = match.Groups[1].Value;
|
var variableName = match.Groups[VariableGroupName].Value;
|
||||||
if (!variables.TryGetValue(variableName, out var value))
|
if (!variables.TryGetValue(variableName, out var value))
|
||||||
{
|
{
|
||||||
return match.Value;
|
return match.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
var formatterName = GetOptionalGroupValue(match, 2);
|
var formatterName = GetOptionalGroupValue(match, FormatterGroupName);
|
||||||
if (string.IsNullOrEmpty(formatterName))
|
if (string.IsNullOrEmpty(formatterName))
|
||||||
{
|
{
|
||||||
return FormatValue(value, manager);
|
return FormatValue(value, manager);
|
||||||
@ -187,7 +205,7 @@ public class LocalizationString : ILocalizationString
|
|||||||
ILocalizationManager manager,
|
ILocalizationManager manager,
|
||||||
out string result)
|
out string result)
|
||||||
{
|
{
|
||||||
var formatterArgs = GetOptionalGroupValue(match, 3) ?? string.Empty;
|
var formatterArgs = GetOptionalGroupValue(match, FormatterArgsGroupName) ?? string.Empty;
|
||||||
if (GetFormatter(manager, formatterName) is { } formatter &&
|
if (GetFormatter(manager, formatterName) is { } formatter &&
|
||||||
formatter.TryFormat(formatterArgs, value, manager.CurrentCulture, out result))
|
formatter.TryFormat(formatterArgs, value, manager.CurrentCulture, out result))
|
||||||
{
|
{
|
||||||
@ -217,11 +235,11 @@ public class LocalizationString : ILocalizationString
|
|||||||
/// 获取正则表达式匹配组中的可选值
|
/// 获取正则表达式匹配组中的可选值
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="match">正则表达式匹配结果</param>
|
/// <param name="match">正则表达式匹配结果</param>
|
||||||
/// <param name="groupIndex">要获取的组索引</param>
|
/// <param name="groupName">要获取的命名组</param>
|
||||||
/// <returns>如果该组匹配成功则返回其值;否则返回 null</returns>
|
/// <returns>如果该组匹配成功则返回其值;否则返回 null</returns>
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -234,4 +252,4 @@ public class LocalizationString : ILocalizationString
|
|||||||
{
|
{
|
||||||
return manager.GetFormatter(name);
|
return manager.GetFormatter(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,8 +32,8 @@ public class LocalizationTable : ILocalizationTable
|
|||||||
{
|
{
|
||||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
Language = language ?? throw new ArgumentNullException(nameof(language));
|
Language = language ?? throw new ArgumentNullException(nameof(language));
|
||||||
_data = new Dictionary<string, string>(data);
|
_data = new Dictionary<string, string>(data, StringComparer.Ordinal);
|
||||||
_overrides = new Dictionary<string, string>();
|
_overrides = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
Fallback = fallback;
|
Fallback = fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +105,7 @@ public class LocalizationTable : ILocalizationTable
|
|||||||
/// <returns>包含所有键的可枚举集合</returns>
|
/// <returns>包含所有键的可枚举集合</returns>
|
||||||
public IEnumerable<string> GetKeys()
|
public IEnumerable<string> GetKeys()
|
||||||
{
|
{
|
||||||
var keys = new HashSet<string>(_data.Keys);
|
var keys = new HashSet<string>(_data.Keys, StringComparer.Ordinal);
|
||||||
keys.UnionWith(_overrides.Keys);
|
keys.UnionWith(_overrides.Keys);
|
||||||
|
|
||||||
if (Fallback != null)
|
if (Fallback != null)
|
||||||
@ -133,4 +133,4 @@ public class LocalizationTable : ILocalizationTable
|
|||||||
_overrides[key] = value;
|
_overrides[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
GFramework.Core/Logging/AppenderConfiguration.cs
Normal file
52
GFramework.Core/Logging/AppenderConfiguration.cs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
@ -162,7 +162,7 @@ public sealed class AsyncLogAppender : ILogAppender
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await foreach (var entry in _channel.Reader.ReadAllAsync(cancellationToken))
|
await foreach (var entry in _channel.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -235,4 +235,4 @@ public sealed class AsyncLogAppender : ILogAppender
|
|||||||
// 错误观察者只用于诊断,绝不能反向影响日志处理线程的生命周期。
|
// 错误观察者只用于诊断,绝不能反向影响日志处理线程的生命周期。
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Globalization;
|
||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
using GFramework.Core.Abstractions.Time;
|
using GFramework.Core.Abstractions.Time;
|
||||||
using GFramework.Core.Time;
|
using GFramework.Core.Time;
|
||||||
@ -13,7 +14,7 @@ namespace GFramework.Core.Logging.Appenders;
|
|||||||
public sealed class StatisticsAppender : ILogAppender
|
public sealed class StatisticsAppender : ILogAppender
|
||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<LogLevel, long> _levelCounts = new();
|
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 readonly ITimeProvider _timeProvider;
|
||||||
private long _errorCount;
|
private long _errorCount;
|
||||||
private long _startTimeTicks;
|
private long _startTimeTicks;
|
||||||
@ -127,7 +128,7 @@ public sealed class StatisticsAppender : ILogAppender
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IReadOnlyDictionary<string, long> GetLoggerCounts()
|
public IReadOnlyDictionary<string, long> GetLoggerCounts()
|
||||||
{
|
{
|
||||||
return new Dictionary<string, long>(_loggerCounts);
|
return new Dictionary<string, long>(_loggerCounts, StringComparer.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -151,27 +152,28 @@ public sealed class StatisticsAppender : ILogAppender
|
|||||||
var startTime = StartTime;
|
var startTime = StartTime;
|
||||||
var now = _timeProvider.UtcNow;
|
var now = _timeProvider.UtcNow;
|
||||||
|
|
||||||
report.AppendLine("=== 日志统计报告 ===");
|
report.AppendLine(FormattableString.Invariant($"=== 日志统计报告 ==="));
|
||||||
report.AppendLine($"统计时间: {startTime:yyyy-MM-dd HH:mm:ss} - {now:yyyy-MM-dd HH:mm:ss}");
|
report.AppendLine(FormattableString.Invariant(
|
||||||
report.AppendLine($"运行时长: {Uptime}");
|
$"统计时间: {startTime.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)} - {now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)}"));
|
||||||
report.AppendLine($"总日志数: {TotalCount}");
|
report.AppendLine(FormattableString.Invariant($"运行时长: {Uptime}"));
|
||||||
report.AppendLine($"错误日志数: {ErrorCount}");
|
report.AppendLine(FormattableString.Invariant($"总日志数: {TotalCount}"));
|
||||||
report.AppendLine($"错误率: {ErrorRate:P2}");
|
report.AppendLine(FormattableString.Invariant($"错误日志数: {ErrorCount}"));
|
||||||
|
report.AppendLine(FormattableString.Invariant($"错误率: {ErrorRate:P2}"));
|
||||||
report.AppendLine();
|
report.AppendLine();
|
||||||
|
|
||||||
report.AppendLine("按级别统计:");
|
report.AppendLine(FormattableString.Invariant($"按级别统计:"));
|
||||||
foreach (var level in Enum.GetValues<LogLevel>())
|
foreach (var level in Enum.GetValues<LogLevel>())
|
||||||
{
|
{
|
||||||
var count = GetCountByLevel(level);
|
var count = GetCountByLevel(level);
|
||||||
if (count > 0)
|
if (count > 0)
|
||||||
{
|
{
|
||||||
var percentage = (double)count / TotalCount;
|
var percentage = (double)count / TotalCount;
|
||||||
report.AppendLine($" {level}: {count} ({percentage:P2})");
|
report.AppendLine(FormattableString.Invariant($" {level}: {count} ({percentage:P2})"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
report.AppendLine();
|
report.AppendLine();
|
||||||
report.AppendLine("按日志记录器统计 (Top 10):");
|
report.AppendLine(FormattableString.Invariant($"按日志记录器统计 (Top 10):"));
|
||||||
var topLoggers = _loggerCounts
|
var topLoggers = _loggerCounts
|
||||||
.OrderByDescending(kvp => kvp.Value)
|
.OrderByDescending(kvp => kvp.Value)
|
||||||
.Take(10);
|
.Take(10);
|
||||||
@ -179,9 +181,9 @@ public sealed class StatisticsAppender : ILogAppender
|
|||||||
foreach (var (logger, count) in topLoggers)
|
foreach (var (logger, count) in topLoggers)
|
||||||
{
|
{
|
||||||
var percentage = (double)count / TotalCount;
|
var percentage = (double)count / TotalCount;
|
||||||
report.AppendLine($" {logger}: {count} ({percentage:P2})");
|
report.AppendLine(FormattableString.Invariant($" {logger}: {count} ({percentage:P2})"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return report.ToString();
|
return report.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ namespace GFramework.Core.Logging;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class CachedLoggerFactory : ILoggerFactory
|
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;
|
private readonly ILoggerFactory _innerFactory;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -31,4 +31,4 @@ public sealed class CachedLoggerFactory : ILoggerFactory
|
|||||||
var cacheKey = $"{name}:{minLevel}";
|
var cacheKey = $"{name}:{minLevel}";
|
||||||
return _cache.GetOrAdd(cacheKey, _ => _innerFactory.GetLogger(name, minLevel));
|
return _cache.GetOrAdd(cacheKey, _ => _innerFactory.GetLogger(name, minLevel));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,7 +74,7 @@ public sealed class CompositeLogger : AbstractLogger, IDisposable
|
|||||||
if (!IsEnabled(level)) return;
|
if (!IsEnabled(level)) return;
|
||||||
|
|
||||||
var propsDict = properties.Length > 0
|
var propsDict = properties.Length > 0
|
||||||
? properties.ToDictionary(p => p.Key, p => p.Value)
|
? properties.ToDictionary(p => p.Key, p => p.Value, StringComparer.Ordinal)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var entry = new LogEntry(
|
var entry = new LogEntry(
|
||||||
@ -104,7 +104,7 @@ public sealed class CompositeLogger : AbstractLogger, IDisposable
|
|||||||
if (!IsEnabled(level)) return;
|
if (!IsEnabled(level)) return;
|
||||||
|
|
||||||
var propsDict = properties.Length > 0
|
var propsDict = properties.Length > 0
|
||||||
? properties.ToDictionary(p => p.Key, p => p.Value)
|
? properties.ToDictionary(p => p.Key, p => p.Value, StringComparer.Ordinal)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var entry = new LogEntry(
|
var entry = new LogEntry(
|
||||||
@ -131,4 +131,4 @@ public sealed class CompositeLogger : AbstractLogger, IDisposable
|
|||||||
appender.Flush();
|
appender.Flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
110
GFramework.Core/Logging/ConfigurableLoggerFactory.cs
Normal file
110
GFramework.Core/Logging/ConfigurableLoggerFactory.cs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
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 int _disposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化一个基于日志配置创建输出管线的工厂实例。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="config">日志配置。</param>
|
||||||
|
/// <exception cref="ArgumentNullException"><paramref name="config"/> 为 <see langword="null" />。</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">配置中的某个 Appender 项为 <see langword="null" />。</exception>
|
||||||
|
public ConfigurableLoggerFactory(LoggingConfiguration config)
|
||||||
|
{
|
||||||
|
_config = config ?? throw new ArgumentNullException(nameof(config));
|
||||||
|
|
||||||
|
// 反序列化输入可能显式把集合写成 null,这里统一归一化为可安全枚举的空集合。
|
||||||
|
_config.Appenders ??= [];
|
||||||
|
_config.LoggerLevels ??= new Dictionary<string, LogLevel>(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
// 外部配置可能把集合项反序列化为 null,这里先给出可诊断异常,避免后续工厂链路出现不清晰的空引用失败。
|
||||||
|
_appenders = _config.Appenders
|
||||||
|
.Select(static appenderConfig => appenderConfig ??
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Appender configuration cannot be null."))
|
||||||
|
.Select(LoggingConfigurationLoader.CreateAppender)
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 释放内部 Appender 持有的资源。
|
||||||
|
/// </summary>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _disposed, 1) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var appender in _appenders)
|
||||||
|
{
|
||||||
|
switch (appender)
|
||||||
|
{
|
||||||
|
case AsyncLogAppender asyncLogAppender:
|
||||||
|
asyncLogAppender.Dispose();
|
||||||
|
break;
|
||||||
|
case IDisposable disposable:
|
||||||
|
disposable.Dispose();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为指定名称创建日志记录器,并应用最匹配的命名空间级别配置。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">日志记录器名称。</param>
|
||||||
|
/// <param name="minLevel">调用方要求的最小日志级别下限;在未命中命名空间覆盖时生效。</param>
|
||||||
|
/// <returns>可写入日志的记录器实例。</returns>
|
||||||
|
/// <exception cref="ArgumentNullException"><paramref name="name"/> 为 <see langword="null" />。</exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// 当配置文件与调用方同时提供默认级别时,会取两者中更严格的那一个;
|
||||||
|
/// 若命中更具体的命名空间级别覆盖,则以该覆盖配置为准,即使其低于调用方传入的默认下限。
|
||||||
|
/// </remarks>
|
||||||
|
public ILogger GetLogger(string name, LogLevel minLevel = LogLevel.Info)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(name);
|
||||||
|
|
||||||
|
var effectiveLevel = _config.MinLevel > minLevel ? _config.MinLevel : minLevel;
|
||||||
|
var bestMatchLength = -1;
|
||||||
|
|
||||||
|
foreach (var kvp in _config.LoggerLevels)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
return new ConsoleLogger(name, effectiveLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_appenders.Length == 1 && _appenders[0] is ConsoleAppender)
|
||||||
|
{
|
||||||
|
return new ConsoleLogger(name, effectiveLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CompositeLogger(name, effectiveLevel, _appenders);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
|
|
||||||
@ -35,7 +36,7 @@ public sealed class ConsoleLogger(
|
|||||||
/// <param name="exception">异常信息,可为空</param>
|
/// <param name="exception">异常信息,可为空</param>
|
||||||
protected override void Write(LogLevel level, string message, Exception? exception)
|
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 levelStr = LevelStrings[(int)level];
|
||||||
var log = $"[{timestamp}] {levelStr} [{Name()}] {message}";
|
var log = $"[{timestamp}] {levelStr} [{Name()}] {message}";
|
||||||
|
|
||||||
@ -81,4 +82,4 @@ public sealed class ConsoleLogger(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
29
GFramework.Core/Logging/FilterConfiguration.cs
Normal file
29
GFramework.Core/Logging/FilterConfiguration.cs
Normal 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; }
|
||||||
|
}
|
||||||
@ -14,7 +14,7 @@ public sealed class SamplingFilter : ILogFilter
|
|||||||
private const int DefaultMaxLoggers = 1000;
|
private const int DefaultMaxLoggers = 1000;
|
||||||
private readonly int _maxLoggers;
|
private readonly int _maxLoggers;
|
||||||
private readonly int _sampleRate;
|
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 ITimeProvider _timeProvider;
|
||||||
private readonly TimeSpan _timeWindow;
|
private readonly TimeSpan _timeWindow;
|
||||||
|
|
||||||
@ -69,7 +69,10 @@ public sealed class SamplingFilter : ILogFilter
|
|||||||
|
|
||||||
foreach (var kvp in _samplingStates)
|
foreach (var kvp in _samplingStates)
|
||||||
{
|
{
|
||||||
if (kvp.Key == "*") continue; // 不清理共享状态
|
if (string.Equals(kvp.Key, "*", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue; // 不清理共享状态
|
||||||
|
}
|
||||||
|
|
||||||
if (kvp.Value.IsStale(now, staleThreshold))
|
if (kvp.Value.IsStale(now, staleThreshold))
|
||||||
{
|
{
|
||||||
@ -132,4 +135,4 @@ public sealed class SamplingFilter : ILogFilter
|
|||||||
return now - lastAccess > staleThreshold;
|
return now - lastAccess > staleThreshold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Globalization;
|
||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
|
|
||||||
namespace GFramework.Core.Logging.Formatters;
|
namespace GFramework.Core.Logging.Formatters;
|
||||||
@ -26,7 +27,7 @@ public sealed class DefaultLogFormatter : ILogFormatter
|
|||||||
/// <returns>格式化后的日志字符串</returns>
|
/// <returns>格式化后的日志字符串</returns>
|
||||||
public string Format(LogEntry entry)
|
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 levelStr = LevelStrings[(int)entry.Level];
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
|
|
||||||
@ -54,4 +55,4 @@ public sealed class DefaultLogFormatter : ILogFormatter
|
|||||||
|
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using System.Globalization;
|
||||||
using GFramework.Core.Abstractions.Logging;
|
using GFramework.Core.Abstractions.Logging;
|
||||||
|
|
||||||
namespace GFramework.Core.Logging.Formatters;
|
namespace GFramework.Core.Logging.Formatters;
|
||||||
@ -21,9 +22,9 @@ public sealed class JsonLogFormatter : ILogFormatter
|
|||||||
/// <returns>JSON 格式的日志字符串</returns>
|
/// <returns>JSON 格式的日志字符串</returns>
|
||||||
public string Format(LogEntry entry)
|
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(),
|
["level"] = entry.Level.ToString().ToUpperInvariant(),
|
||||||
["logger"] = entry.LoggerName,
|
["logger"] = entry.LoggerName,
|
||||||
["message"] = entry.Message
|
["message"] = entry.Message
|
||||||
@ -49,4 +50,4 @@ public sealed class JsonLogFormatter : ILogFormatter
|
|||||||
|
|
||||||
return JsonSerializer.Serialize(logObject, JsonOptions);
|
return JsonSerializer.Serialize(logObject, JsonOptions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,82 +20,5 @@ public sealed class LoggingConfiguration
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 特定 Logger 的日志级别配置
|
/// 特定 Logger 的日志级别配置
|
||||||
/// </summary>
|
/// </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; }
|
|
||||||
}
|
|
||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -15,7 +15,7 @@ public abstract class AbstractObjectPoolSystem<TKey, TObject>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 存储对象池的字典,键为池标识,值为池信息
|
/// 存储对象池的字典,键为池标识,值为池信息
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly Dictionary<TKey, PoolInfo> Pools = new();
|
protected readonly IDictionary<TKey, PoolInfo> Pools = new Dictionary<TKey, PoolInfo>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取对象池中的对象,如果池中没有可用对象则创建新的对象
|
/// 获取对象池中的对象,如果池中没有可用对象则创建新的对象
|
||||||
@ -254,4 +254,4 @@ public abstract class AbstractObjectPoolSystem<TKey, TObject>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int ActiveCount { get; set; }
|
public int ActiveCount { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,19 +3,8 @@ using System.Collections.Concurrent;
|
|||||||
namespace GFramework.Core.Resource;
|
namespace GFramework.Core.Resource;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
internal sealed class ResourceCache
|
internal sealed class ResourceCache
|
||||||
{
|
{
|
||||||
@ -24,7 +13,7 @@ internal sealed class ResourceCache
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private const string PathCannotBeNullOrEmptyMessage = "Path cannot be null or whitespace.";
|
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();
|
private readonly object _lock = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -208,4 +197,4 @@ internal sealed class ResourceCache
|
|||||||
|
|
||||||
return unreferencedPaths;
|
return unreferencedPaths;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
27
GFramework.Core/Resource/ResourceCacheEntry.cs
Normal file
27
GFramework.Core/Resource/ResourceCacheEntry.cs
Normal 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;
|
||||||
|
}
|
||||||
@ -82,8 +82,14 @@ public class ResourceManager : IResourceManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 异步加载资源
|
/// 异步加载指定路径的资源,并在缓存中进行并发去重。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <typeparam name="T">资源类型</typeparam>
|
||||||
|
/// <param name="path">资源路径,不能为空或空白。</param>
|
||||||
|
/// <returns>加载成功返回资源实例;加载失败返回 <see langword="null"/>。</returns>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="path"/> 为空或空白时抛出。</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">当未注册对应资源加载器时抛出。</exception>
|
||||||
|
/// <remarks>内部使用 <c>ConfigureAwait(false)</c>,后续延续不保证回到调用线程。</remarks>
|
||||||
public async Task<T?> LoadAsync<T>(string path) where T : class
|
public async Task<T?> LoadAsync<T>(string path) where T : class
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(path))
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
@ -104,7 +110,7 @@ public class ResourceManager : IResourceManager
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var resource = await loader.LoadAsync(path);
|
var resource = await loader.LoadAsync(path).ConfigureAwait(false);
|
||||||
lock (_loadLock)
|
lock (_loadLock)
|
||||||
{
|
{
|
||||||
// 双重检查
|
// 双重检查
|
||||||
@ -227,11 +233,17 @@ public class ResourceManager : IResourceManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 预加载资源
|
/// 预加载资源到缓存中。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <typeparam name="T">资源类型</typeparam>
|
||||||
|
/// <param name="path">资源路径,不能为空或空白。</param>
|
||||||
|
/// <returns>表示预加载流程完成的任务。</returns>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="path"/> 为空或空白时抛出。</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">当未注册对应资源加载器时抛出。</exception>
|
||||||
|
/// <remarks>内部委托给 <see cref="LoadAsync{T}(string)"/>,同样不捕获同步上下文。</remarks>
|
||||||
public async Task PreloadAsync<T>(string path) where T : class
|
public async Task PreloadAsync<T>(string path) where T : class
|
||||||
{
|
{
|
||||||
await LoadAsync<T>(path);
|
await LoadAsync<T>(path).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -324,4 +336,4 @@ public class ResourceManager : IResourceManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,7 @@ public sealed class ServiceModuleManager : IServiceModuleManager
|
|||||||
return;
|
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");
|
_logger.Warn($"Module {module.ModuleName} already registered");
|
||||||
return;
|
return;
|
||||||
@ -109,7 +109,7 @@ public sealed class ServiceModuleManager : IServiceModuleManager
|
|||||||
|
|
||||||
if (asyncMode && module is IAsyncInitializable asyncInitializable)
|
if (asyncMode && module is IAsyncInitializable asyncInitializable)
|
||||||
{
|
{
|
||||||
await asyncInitializable.InitializeAsync();
|
await asyncInitializable.InitializeAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -137,7 +137,7 @@ public sealed class ServiceModuleManager : IServiceModuleManager
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.Debug($"Destroying module: {module.ModuleName}");
|
_logger.Debug($"Destroying module: {module.ModuleName}");
|
||||||
await module.DestroyAsync();
|
await module.DestroyAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -17,7 +17,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 存储所有已注册状态的字典,键为状态类型,值为状态实例
|
/// 存储所有已注册状态的字典,键为状态类型,值为状态实例
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected readonly Dictionary<Type, IState> States = new();
|
protected readonly IDictionary<Type, IState> States = new Dictionary<Type, IState>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取当前激活的状态
|
/// 获取当前激活的状态
|
||||||
@ -45,7 +45,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine
|
|||||||
/// <typeparam name="T">要注销的状态类型</typeparam>
|
/// <typeparam name="T">要注销的状态类型</typeparam>
|
||||||
public async Task<IStateMachine> UnregisterAsync<T>() where T : IState
|
public async Task<IStateMachine> UnregisterAsync<T>() where T : IState
|
||||||
{
|
{
|
||||||
await _transitionLock.WaitAsync();
|
await _transitionLock.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var stateToUnregister = PrepareUnregister<T>(out var isCurrentState);
|
var stateToUnregister = PrepareUnregister<T>(out var isCurrentState);
|
||||||
@ -53,7 +53,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine
|
|||||||
|
|
||||||
if (isCurrentState)
|
if (isCurrentState)
|
||||||
{
|
{
|
||||||
await ExecuteExitAsync(Current!, null);
|
await ExecuteExitAsync(Current!, null).ConfigureAwait(false);
|
||||||
Current = null;
|
Current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine
|
|||||||
/// <returns>如果可以切换则返回true,否则返回false</returns>
|
/// <returns>如果可以切换则返回true,否则返回false</returns>
|
||||||
public async Task<bool> CanChangeToAsync<T>() where T : IState
|
public async Task<bool> CanChangeToAsync<T>() where T : IState
|
||||||
{
|
{
|
||||||
await _transitionLock.WaitAsync();
|
await _transitionLock.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!States.TryGetValue(typeof(T), out var target))
|
if (!States.TryGetValue(typeof(T), out var target))
|
||||||
@ -81,7 +81,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine
|
|||||||
|
|
||||||
if (Current == null) return true;
|
if (Current == null) return true;
|
||||||
|
|
||||||
return await CanTransitionToAsync(Current, target);
|
return await CanTransitionToAsync(Current, target).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@ -98,7 +98,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine
|
|||||||
/// <exception cref="InvalidOperationException">当目标状态未注册时抛出</exception>
|
/// <exception cref="InvalidOperationException">当目标状态未注册时抛出</exception>
|
||||||
public async Task<bool> ChangeToAsync<T>() where T : IState
|
public async Task<bool> ChangeToAsync<T>() where T : IState
|
||||||
{
|
{
|
||||||
await _transitionLock.WaitAsync();
|
await _transitionLock.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
IState target;
|
IState target;
|
||||||
@ -114,15 +114,15 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine
|
|||||||
|
|
||||||
if (currentSnapshot != null)
|
if (currentSnapshot != null)
|
||||||
{
|
{
|
||||||
var canTransition = await CanTransitionToAsync(currentSnapshot, target);
|
var canTransition = await CanTransitionToAsync(currentSnapshot, target).ConfigureAwait(false);
|
||||||
if (!canTransition)
|
if (!canTransition)
|
||||||
{
|
{
|
||||||
await OnTransitionRejectedAsync(currentSnapshot, target);
|
await OnTransitionRejectedAsync(currentSnapshot, target).ConfigureAwait(false);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await ChangeInternalAsync(target);
|
await ChangeInternalAsync(target).ConfigureAwait(false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -190,13 +190,13 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine
|
|||||||
/// <returns>如果成功回退则返回true,否则返回false</returns>
|
/// <returns>如果成功回退则返回true,否则返回false</returns>
|
||||||
public async Task<bool> GoBackAsync()
|
public async Task<bool> GoBackAsync()
|
||||||
{
|
{
|
||||||
await _transitionLock.WaitAsync();
|
await _transitionLock.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var previousState = FindValidPreviousState();
|
var previousState = FindValidPreviousState();
|
||||||
if (previousState == null) return false;
|
if (previousState == null) return false;
|
||||||
|
|
||||||
await ChangeInternalWithoutHistoryAsync(previousState);
|
await ChangeInternalWithoutHistoryAsync(previousState).ConfigureAwait(false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -282,13 +282,13 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine
|
|||||||
if (Current == next) return;
|
if (Current == next) return;
|
||||||
|
|
||||||
var old = Current;
|
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;
|
Current = next;
|
||||||
await ExecuteEnterAsync(Current, old);
|
await ExecuteEnterAsync(Current, old).ConfigureAwait(false);
|
||||||
|
|
||||||
await OnStateChangedAsync(old, Current);
|
await OnStateChangedAsync(old, Current).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -300,18 +300,18 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine
|
|||||||
if (Current == next) return;
|
if (Current == next) return;
|
||||||
|
|
||||||
var old = Current;
|
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);
|
AddToHistory(old);
|
||||||
|
|
||||||
Current = next;
|
Current = next;
|
||||||
|
|
||||||
await ExecuteEnterAsync(Current, old);
|
await ExecuteEnterAsync(Current, old).ConfigureAwait(false);
|
||||||
|
|
||||||
|
|
||||||
await OnStateChangedAsync(old, Current);
|
await OnStateChangedAsync(old, Current).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -344,7 +344,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine
|
|||||||
if (state == null) return;
|
if (state == null) return;
|
||||||
|
|
||||||
if (state is IAsyncState asyncState)
|
if (state is IAsyncState asyncState)
|
||||||
await asyncState.OnEnterAsync(from);
|
await asyncState.OnEnterAsync(from).ConfigureAwait(false);
|
||||||
else
|
else
|
||||||
state.OnEnter(from);
|
state.OnEnter(from);
|
||||||
}
|
}
|
||||||
@ -357,7 +357,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine
|
|||||||
if (state == null) return;
|
if (state == null) return;
|
||||||
|
|
||||||
if (state is IAsyncState asyncState)
|
if (state is IAsyncState asyncState)
|
||||||
await asyncState.OnExitAsync(to);
|
await asyncState.OnExitAsync(to).ConfigureAwait(false);
|
||||||
else
|
else
|
||||||
state.OnExit(to);
|
state.OnExit(to);
|
||||||
}
|
}
|
||||||
@ -368,7 +368,7 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine
|
|||||||
private static async Task<bool> CanTransitionToAsync(IState current, IState target)
|
private static async Task<bool> CanTransitionToAsync(IState current, IState target)
|
||||||
{
|
{
|
||||||
if (current is IAsyncState asyncState)
|
if (current is IAsyncState asyncState)
|
||||||
return await asyncState.CanTransitionToAsync(target);
|
return await asyncState.CanTransitionToAsync(target).ConfigureAwait(false);
|
||||||
|
|
||||||
return current.CanTransitionTo(target);
|
return current.CanTransitionTo(target);
|
||||||
}
|
}
|
||||||
@ -432,4 +432,4 @@ public class StateMachine(int maxHistorySize = 10) : IStateMachine
|
|||||||
OnStateChanged(from, to);
|
OnStateChanged(from, to);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,7 +72,7 @@ public class StateMachineSystem : StateMachine, IStateMachineSystem
|
|||||||
{
|
{
|
||||||
if (Current is IAsyncState asyncState)
|
if (Current is IAsyncState asyncState)
|
||||||
{
|
{
|
||||||
await asyncState.OnExitAsync(null); // ✅ 正确等待异步清理
|
await asyncState.OnExitAsync(null).ConfigureAwait(false); // ✅ 正确等待异步清理
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -87,7 +87,7 @@ public class StateMachineSystem : StateMachine, IStateMachineSystem
|
|||||||
{
|
{
|
||||||
if (state is IAsyncDestroyable asyncDestroyable)
|
if (state is IAsyncDestroyable asyncDestroyable)
|
||||||
{
|
{
|
||||||
await asyncDestroyable.DestroyAsync();
|
await asyncDestroyable.DestroyAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else if (state is IDestroyable destroyable)
|
else if (state is IDestroyable destroyable)
|
||||||
{
|
{
|
||||||
@ -106,7 +106,7 @@ public class StateMachineSystem : StateMachine, IStateMachineSystem
|
|||||||
protected override async Task ChangeInternalAsync(IState next)
|
protected override async Task ChangeInternalAsync(IState next)
|
||||||
{
|
{
|
||||||
var old = Current;
|
var old = Current;
|
||||||
await base.ChangeInternalAsync(next);
|
await base.ChangeInternalAsync(next).ConfigureAwait(false);
|
||||||
|
|
||||||
// 发送状态变更事件,通知监听者状态已发生改变
|
// 发送状态变更事件,通知监听者状态已发生改变
|
||||||
this.SendEvent(new StateChangedEvent
|
this.SendEvent(new StateChangedEvent
|
||||||
@ -115,4 +115,4 @@ public class StateMachineSystem : StateMachine, IStateMachineSystem
|
|||||||
NewState = Current
|
NewState = Current
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -50,7 +50,7 @@ public sealed class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRe
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var response = await next(message, cancellationToken);
|
var response = await next(message, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var elapsed = Stopwatch.GetElapsedTime(start);
|
var elapsed = Stopwatch.GetElapsedTime(start);
|
||||||
_logger.Debug($"Handled {requestName} successfully in {elapsed.TotalMilliseconds} ms");
|
_logger.Debug($"Handled {requestName} successfully in {elapsed.TotalMilliseconds} ms");
|
||||||
|
|||||||
@ -48,7 +48,7 @@ public sealed class PerformanceBehavior<TRequest, TResponse> : IPipelineBehavior
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return await next(message, cancellationToken);
|
return await next(message, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@ -73,7 +73,7 @@ internal sealed class CqrsDispatcher(
|
|||||||
foreach (var handler in handlers)
|
foreach (var handler in handlers)
|
||||||
{
|
{
|
||||||
PrepareHandler(handler, context);
|
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);
|
PrepareHandler(behavior, context);
|
||||||
|
|
||||||
if (behaviors.Count == 0)
|
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>
|
/// <summary>
|
||||||
|
|||||||
@ -25,10 +25,8 @@ internal sealed class WeakKeyCache<TKey, TValue>
|
|||||||
/// <param name="key">缓存键。</param>
|
/// <param name="key">缓存键。</param>
|
||||||
/// <param name="valueFactory">创建缓存值的工厂方法。</param>
|
/// <param name="valueFactory">创建缓存值的工厂方法。</param>
|
||||||
/// <returns>已存在或新创建的缓存值。</returns>
|
/// <returns>已存在或新创建的缓存值。</returns>
|
||||||
/// <exception cref="ArgumentNullException">
|
/// <exception cref="ArgumentNullException"><paramref name="key" /> 或 <paramref name="valueFactory" /> 为 <see langword="null" />。</exception>
|
||||||
/// <paramref name="key" /> 或 <paramref name="valueFactory" /> 为 <see langword="null" />。
|
/// <exception cref="InvalidOperationException"><paramref name="valueFactory" /> 返回 <see langword="null" />。</exception>
|
||||||
/// 或 <paramref name="valueFactory" /> 返回 <see langword="null" />。
|
|
||||||
/// </exception>
|
|
||||||
public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
|
public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(key);
|
ArgumentNullException.ThrowIfNull(key);
|
||||||
@ -44,8 +42,8 @@ internal sealed class WeakKeyCache<TKey, TValue>
|
|||||||
if (entries.TryGetValue(key, out cachedValue))
|
if (entries.TryGetValue(key, out cachedValue))
|
||||||
return cachedValue;
|
return cachedValue;
|
||||||
|
|
||||||
var createdValue = valueFactory(key);
|
var createdValue = valueFactory(key) ??
|
||||||
ArgumentNullException.ThrowIfNull(createdValue);
|
throw new InvalidOperationException("The value factory returned null.");
|
||||||
entries.Add(key, createdValue);
|
entries.Add(key, createdValue);
|
||||||
return createdValue;
|
return createdValue;
|
||||||
}
|
}
|
||||||
@ -59,10 +57,8 @@ internal sealed class WeakKeyCache<TKey, TValue>
|
|||||||
/// <param name="state">创建缓存值时复用的附加状态。</param>
|
/// <param name="state">创建缓存值时复用的附加状态。</param>
|
||||||
/// <param name="valueFactory">基于键与附加状态创建缓存值的工厂方法。</param>
|
/// <param name="valueFactory">基于键与附加状态创建缓存值的工厂方法。</param>
|
||||||
/// <returns>已存在或新创建的缓存值。</returns>
|
/// <returns>已存在或新创建的缓存值。</returns>
|
||||||
/// <exception cref="ArgumentNullException">
|
/// <exception cref="ArgumentNullException"><paramref name="key" /> 或 <paramref name="valueFactory" /> 为 <see langword="null" />。</exception>
|
||||||
/// <paramref name="key" /> 或 <paramref name="valueFactory" /> 为 <see langword="null" />。
|
/// <exception cref="InvalidOperationException"><paramref name="valueFactory" /> 返回 <see langword="null" />。</exception>
|
||||||
/// 或 <paramref name="valueFactory" /> 返回 <see langword="null" />。
|
|
||||||
/// </exception>
|
|
||||||
public TValue GetOrAdd<TState>(TKey key, TState state, Func<TKey, TState, TValue> valueFactory)
|
public TValue GetOrAdd<TState>(TKey key, TState state, Func<TKey, TState, TValue> valueFactory)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(key);
|
ArgumentNullException.ThrowIfNull(key);
|
||||||
@ -78,8 +74,8 @@ internal sealed class WeakKeyCache<TKey, TValue>
|
|||||||
if (entries.TryGetValue(key, out cachedValue))
|
if (entries.TryGetValue(key, out cachedValue))
|
||||||
return cachedValue;
|
return cachedValue;
|
||||||
|
|
||||||
var createdValue = valueFactory(key, state);
|
var createdValue = valueFactory(key, state) ??
|
||||||
ArgumentNullException.ThrowIfNull(createdValue);
|
throw new InvalidOperationException("The value factory returned null.");
|
||||||
entries.Add(key, createdValue);
|
entries.Add(key, createdValue);
|
||||||
return createdValue;
|
return createdValue;
|
||||||
}
|
}
|
||||||
@ -125,90 +121,3 @@ internal sealed class WeakKeyCache<TKey, TValue>
|
|||||||
return TryGetValue(key, out var value) ? value : null;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
96
GFramework.Cqrs/Internal/WeakTypePairCache.cs
Normal file
96
GFramework.Cqrs/Internal/WeakTypePairCache.cs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
namespace GFramework.Cqrs.Internal;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 提供以两段 <see cref="Type" /> 为键的弱引用缓存。
|
||||||
|
/// 适用于请求/响应或流请求/响应这类组合类型元数据的复用场景。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TValue">缓存值类型。</typeparam>
|
||||||
|
/// <remarks>
|
||||||
|
/// 第一层和第二层键都使用弱键缓存,因此只要任一类型不再被外部引用,
|
||||||
|
/// 对应条目都允许被 GC 清理,并在后续首次访问时重新建立。
|
||||||
|
/// 线程安全:该类型支持并发访问,读写与清理由底层弱键缓存实现保证一致性。
|
||||||
|
/// 失败模式:键被 GC 回收或调用 <see cref="Clear" /> 后,后续读取可能未命中并触发重建。
|
||||||
|
/// </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>
|
||||||
|
/// <exception cref="InvalidOperationException"><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);
|
||||||
|
|
||||||
|
// 第一层按 primaryType 定位或创建二级缓存,避免每次命中都重新分配容器。
|
||||||
|
var secondaryEntries = _entries.GetOrAdd(primaryType, static _ => new WeakKeyCache<Type, TValue>());
|
||||||
|
return secondaryEntries.GetOrAdd(
|
||||||
|
secondaryType,
|
||||||
|
(PrimaryType: primaryType, Factory: valueFactory),
|
||||||
|
// 使用 static lambda + state 传参,避免热路径上的闭包捕获与额外分配。
|
||||||
|
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>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// <paramref name="primaryType" /> 或 <paramref name="secondaryType" /> 为 <see langword="null" />。
|
||||||
|
/// </exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// 该入口仅用于测试通过反射观察缓存状态,不应用于运行时代码路径。
|
||||||
|
/// </remarks>
|
||||||
|
public TValue? GetValueOrDefaultForTesting(Type primaryType, Type secondaryType)
|
||||||
|
{
|
||||||
|
return TryGetValue(primaryType, secondaryType, out var value) ? value : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,11 @@
|
|||||||
public interface IApplyAbleSettings : ISettingsSection
|
public interface IApplyAbleSettings : ISettingsSection
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 应用当前设置到系统中
|
/// 异步应用当前设置到目标系统中。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task Apply();
|
/// <returns>
|
||||||
}
|
/// 表示应用流程完成的任务。
|
||||||
|
/// 对于仅执行同步引擎调用的实现,可以返回已完成任务。
|
||||||
|
/// </returns>
|
||||||
|
Task ApplyAsync();
|
||||||
|
}
|
||||||
|
|||||||
@ -12,8 +12,12 @@ namespace GFramework.Game.Tests.Setting;
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public sealed class GodotLocalizationSettingsTests
|
public sealed class GodotLocalizationSettingsTests
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 验证应用英文设置时,会同时同步 Godot locale 与框架语言管理器。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>表示异步断言完成的任务。</returns>
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Apply_ShouldSyncEnglishToGodotLocaleAndFrameworkLanguage()
|
public async Task ApplyAsync_ShouldSyncEnglishToGodotLocaleAndFrameworkLanguage()
|
||||||
{
|
{
|
||||||
var manager = new Mock<ILocalizationManager>(MockBehavior.Strict);
|
var manager = new Mock<ILocalizationManager>(MockBehavior.Strict);
|
||||||
manager.Setup(it => it.SetLanguage("eng"));
|
manager.Setup(it => it.SetLanguage("eng"));
|
||||||
@ -21,14 +25,18 @@ public sealed class GodotLocalizationSettingsTests
|
|||||||
|
|
||||||
var applicator = CreateApplicator("English", manager.Object, locale => appliedLocale = locale);
|
var applicator = CreateApplicator("English", manager.Object, locale => appliedLocale = locale);
|
||||||
|
|
||||||
await applicator.Apply();
|
await applicator.ApplyAsync();
|
||||||
|
|
||||||
Assert.That(appliedLocale, Is.EqualTo("en"));
|
Assert.That(appliedLocale, Is.EqualTo("en"));
|
||||||
manager.Verify(it => it.SetLanguage("eng"), Times.Once);
|
manager.Verify(it => it.SetLanguage("eng"), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证应用简体中文设置时,会同时同步 Godot locale 与框架语言管理器。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>表示异步断言完成的任务。</returns>
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Apply_ShouldSyncChineseToGodotLocaleAndFrameworkLanguage()
|
public async Task ApplyAsync_ShouldSyncChineseToGodotLocaleAndFrameworkLanguage()
|
||||||
{
|
{
|
||||||
var manager = new Mock<ILocalizationManager>(MockBehavior.Strict);
|
var manager = new Mock<ILocalizationManager>(MockBehavior.Strict);
|
||||||
manager.Setup(it => it.SetLanguage("zhs"));
|
manager.Setup(it => it.SetLanguage("zhs"));
|
||||||
@ -36,14 +44,18 @@ public sealed class GodotLocalizationSettingsTests
|
|||||||
|
|
||||||
var applicator = CreateApplicator("简体中文", manager.Object, locale => appliedLocale = locale);
|
var applicator = CreateApplicator("简体中文", manager.Object, locale => appliedLocale = locale);
|
||||||
|
|
||||||
await applicator.Apply();
|
await applicator.ApplyAsync();
|
||||||
|
|
||||||
Assert.That(appliedLocale, Is.EqualTo("zh_CN"));
|
Assert.That(appliedLocale, Is.EqualTo("zh_CN"));
|
||||||
manager.Verify(it => it.SetLanguage("zhs"), Times.Once);
|
manager.Verify(it => it.SetLanguage("zhs"), Times.Once);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证未知语言会回退到英文 locale,并同步默认框架语言代码。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>表示异步断言完成的任务。</returns>
|
||||||
[Test]
|
[Test]
|
||||||
public async Task Apply_ShouldFallbackUnknownLanguageToEnglish()
|
public async Task ApplyAsync_ShouldFallbackUnknownLanguageToEnglish()
|
||||||
{
|
{
|
||||||
var manager = new Mock<ILocalizationManager>(MockBehavior.Strict);
|
var manager = new Mock<ILocalizationManager>(MockBehavior.Strict);
|
||||||
manager.Setup(it => it.SetLanguage("eng"));
|
manager.Setup(it => it.SetLanguage("eng"));
|
||||||
@ -51,7 +63,7 @@ public sealed class GodotLocalizationSettingsTests
|
|||||||
|
|
||||||
var applicator = CreateApplicator("Esperanto", manager.Object, locale => appliedLocale = locale);
|
var applicator = CreateApplicator("Esperanto", manager.Object, locale => appliedLocale = locale);
|
||||||
|
|
||||||
await applicator.Apply();
|
await applicator.ApplyAsync();
|
||||||
|
|
||||||
Assert.That(appliedLocale, Is.EqualTo("en"));
|
Assert.That(appliedLocale, Is.EqualTo("en"));
|
||||||
manager.Verify(it => it.SetLanguage("eng"), Times.Once);
|
manager.Verify(it => it.SetLanguage("eng"), Times.Once);
|
||||||
@ -71,4 +83,4 @@ public sealed class GodotLocalizationSettingsTests
|
|||||||
return new GodotLocalizationSettings(settingsModel.Object, new LocalizationMap(), () => manager,
|
return new GodotLocalizationSettings(settingsModel.Object, new LocalizationMap(), () => manager,
|
||||||
applyGodotLocale);
|
applyGodotLocale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -314,7 +314,7 @@ public sealed class SettingsSystemTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task Apply()
|
public async Task ApplyAsync()
|
||||||
{
|
{
|
||||||
ApplyCount++;
|
ApplyCount++;
|
||||||
|
|
||||||
|
|||||||
@ -198,7 +198,7 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
|
|||||||
foreach (var applicator in _applicators)
|
foreach (var applicator in _applicators)
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await applicator.Value.Apply();
|
await applicator.Value.ApplyAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -302,4 +302,4 @@ public class SettingsModel<TRepository>(IDataLocationProvider? locationProvider,
|
|||||||
|
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,7 +87,7 @@ public class SettingsSystem : AbstractSystem, ISettingsSystem
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await applyAbleSettings.Apply();
|
await applyAbleSettings.ApplyAsync();
|
||||||
// 发送设置应用成功事件
|
// 发送设置应用成功事件
|
||||||
this.SendEvent(new SettingsAppliedEvent<ISettingsSection>(section, true));
|
this.SendEvent(new SettingsAppliedEvent<ISettingsSection>(section, true));
|
||||||
}
|
}
|
||||||
@ -97,4 +97,4 @@ public class SettingsSystem : AbstractSystem, ISettingsSystem
|
|||||||
this.SendEvent(new SettingsAppliedEvent<ISettingsSection>(section, false, ex));
|
this.SendEvent(new SettingsAppliedEvent<ISettingsSection>(section, false, ex));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,64 @@
|
|||||||
|
using GFramework.Core.Abstractions.Architectures;
|
||||||
|
using GFramework.Godot.Architectures;
|
||||||
|
|
||||||
|
namespace GFramework.Godot.Tests.Architectures;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 Godot 架构在模块安装前会先检查锚点状态,避免未绑定场景树时留下半安装副作用。
|
||||||
|
/// </summary>
|
||||||
|
[TestFixture]
|
||||||
|
public sealed class AbstractArchitectureModuleInstallationTests
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 验证当锚点尚未初始化时,安装流程会直接失败,且不会执行模块安装逻辑。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>表示异步断言完成的任务。</returns>
|
||||||
|
[Test]
|
||||||
|
public async Task InstallGodotModuleAsync_ShouldThrowBeforeInvokingModuleInstall_WhenAnchorIsMissing()
|
||||||
|
{
|
||||||
|
var architecture = new TestArchitecture();
|
||||||
|
var module = new RecordingGodotModule();
|
||||||
|
|
||||||
|
var exception = Assert.ThrowsAsync<InvalidOperationException>(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()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,8 +2,6 @@
|
|||||||
using GFramework.Core.Abstractions.Environment;
|
using GFramework.Core.Abstractions.Environment;
|
||||||
using GFramework.Core.Architectures;
|
using GFramework.Core.Architectures;
|
||||||
using GFramework.Core.Constants;
|
using GFramework.Core.Constants;
|
||||||
using GFramework.Godot.Extensions;
|
|
||||||
using Godot;
|
|
||||||
|
|
||||||
namespace GFramework.Godot.Architectures;
|
namespace GFramework.Godot.Architectures;
|
||||||
|
|
||||||
@ -85,7 +83,7 @@ public abstract class AbstractArchitecture(
|
|||||||
Name = _architectureAnchorName
|
Name = _architectureAnchorName
|
||||||
};
|
};
|
||||||
|
|
||||||
_anchor.Bind(() => DestroyAsync().AsTask());
|
_anchor.Bind(ObserveDestroyAsync);
|
||||||
|
|
||||||
tree.Root.CallDeferred(Node.MethodName.AddChild, _anchor);
|
tree.Root.CallDeferred(Node.MethodName.AddChild, _anchor);
|
||||||
}
|
}
|
||||||
@ -97,25 +95,31 @@ public abstract class AbstractArchitecture(
|
|||||||
/// <typeparam name="TModule">模块类型,必须实现IGodotModule接口</typeparam>
|
/// <typeparam name="TModule">模块类型,必须实现IGodotModule接口</typeparam>
|
||||||
/// <param name="module">要安装的模块实例</param>
|
/// <param name="module">要安装的模块实例</param>
|
||||||
/// <returns>异步任务</returns>
|
/// <returns>异步任务</returns>
|
||||||
|
/// <exception cref="ArgumentNullException">当 <paramref name="module" /> 为 <see langword="null" /> 时抛出。</exception>
|
||||||
|
/// <exception cref="InvalidOperationException">当架构锚点尚未初始化时抛出。</exception>
|
||||||
|
/// <remarks>
|
||||||
|
/// 该方法会等待锚点进入场景树后再继续执行附加回调,避免模块在非主线程或未就绪状态下访问 Godot 节点 API。
|
||||||
|
/// </remarks>
|
||||||
protected async Task InstallGodotModule<TModule>(TModule module) where TModule : IGodotModule
|
protected async Task InstallGodotModule<TModule>(TModule module) where TModule : IGodotModule
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(module);
|
||||||
|
|
||||||
|
// 先确认锚点可用,避免模块安装产生副作用后再因架构未绑定场景树而失败。
|
||||||
|
var anchor = _anchor ?? throw new InvalidOperationException("Anchor not initialized");
|
||||||
|
|
||||||
module.Install(this);
|
module.Install(this);
|
||||||
|
|
||||||
// 检查锚点是否已初始化,未初始化则抛出异常
|
// 在附加流程完成前先登记模块,保证后续任一步失败时仍能参与架构销毁阶段的清理。
|
||||||
if (_anchor == null)
|
_extensions.Add(module);
|
||||||
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);
|
module.OnAttach(this);
|
||||||
|
|
||||||
// 将扩展添加到扩展集合中
|
|
||||||
_extensions.Add(module);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -136,6 +140,34 @@ public abstract class AbstractArchitecture(
|
|||||||
|
|
||||||
_extensions.Clear();
|
_extensions.Clear();
|
||||||
|
|
||||||
await base.DestroyAsync();
|
await base.DestroyAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/// <summary>
|
||||||
|
/// 观察架构异步销毁流程,确保退出树时触发的 fire-and-forget 清理失败可见。
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Godot 的 <see cref="Node._ExitTree" /> 回调是同步入口,无法直接等待异步销毁完成;
|
||||||
|
/// 因此这里显式附加错误观察器,把异常写入 Godot 错误输出,避免未观测任务异常被静默吞掉。
|
||||||
|
/// </remarks>
|
||||||
|
private void ObserveDestroyAsync()
|
||||||
|
{
|
||||||
|
_ = ObserveDestroyCoreAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行并观察异步销毁流程。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>表示观察任务本身完成的任务。</returns>
|
||||||
|
private async Task ObserveDestroyCoreAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await DestroyAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
GD.PushError($"Architecture destruction failed: {ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
33
GFramework.Godot/Config/GodotYamlConfigDirectoryEntry.cs
Normal file
33
GFramework.Godot/Config/GodotYamlConfigDirectoryEntry.cs
Normal 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; }
|
||||||
|
}
|
||||||
209
GFramework.Godot/Config/GodotYamlConfigEnvironment.cs
Normal file
209
GFramework.Godot/Config/GodotYamlConfigEnvironment.cs
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
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())
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var name = directory.GetNext();
|
||||||
|
if (string.IsNullOrEmpty(name))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.Add(new GodotYamlConfigDirectoryEntry(name, directory.CurrentIsDir()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
// 目录枚举句柄必须成对结束,避免未来循环体扩展后在异常路径上遗留引擎状态。
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -153,7 +153,7 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
|
|||||||
SynchronizeRuntimeCache(cancellationToken);
|
SynchronizeRuntimeCache(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _loader.LoadAsync(registry, cancellationToken);
|
await _loader.LoadAsync(registry, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -511,194 +511,3 @@ public sealed class GodotYamlConfigLoader : IConfigLoader
|
|||||||
innerException);
|
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; }
|
|
||||||
}
|
|
||||||
|
|||||||
@ -209,7 +209,8 @@ public class GodotResourceRepository<TKey, TResource>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 只处理.tres和.res扩展名的资源文件
|
// 只处理.tres和.res扩展名的资源文件
|
||||||
if (!entry.EndsWith(".tres") && !entry.EndsWith(".res"))
|
if (!entry.EndsWith(".tres", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!entry.EndsWith(".res", StringComparison.OrdinalIgnoreCase))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// 加载资源文件
|
// 加载资源文件
|
||||||
@ -224,4 +225,4 @@ public class GodotResourceRepository<TKey, TResource>
|
|||||||
if (!_storage.TryAdd(resource.Key, resource))
|
if (!_storage.TryAdd(resource.Key, resource))
|
||||||
Log.Warn($"Duplicate key detected: {resource.Key}");
|
Log.Warn($"Duplicate key detected: {resource.Key}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ public static class GodotPathExtensions
|
|||||||
/// <returns>如果路径以 "user://" 开头且不为空,则返回 true;否则返回 false。</returns>
|
/// <returns>如果路径以 "user://" 开头且不为空,则返回 true;否则返回 false。</returns>
|
||||||
public static bool IsUserPath(this string path)
|
public static bool IsUserPath(this string path)
|
||||||
{
|
{
|
||||||
return !string.IsNullOrEmpty(path) && path.StartsWith("user://");
|
return !string.IsNullOrEmpty(path) && path.StartsWith("user://", StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -23,7 +23,7 @@ public static class GodotPathExtensions
|
|||||||
/// <returns>如果路径以 "res://" 开头且不为空,则返回 true;否则返回 false。</returns>
|
/// <returns>如果路径以 "res://" 开头且不为空,则返回 true;否则返回 false。</returns>
|
||||||
public static bool IsResPath(this string path)
|
public static bool IsResPath(this string path)
|
||||||
{
|
{
|
||||||
return !string.IsNullOrEmpty(path) && path.StartsWith("res://");
|
return !string.IsNullOrEmpty(path) && path.StartsWith("res://", StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -35,4 +35,4 @@ public static class GodotPathExtensions
|
|||||||
{
|
{
|
||||||
return path.IsUserPath() || path.IsResPath();
|
return path.IsUserPath() || path.IsResPath();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -176,7 +176,7 @@ public static class NodeExtensions
|
|||||||
public static async Task AddChildXAsync(this Node parent, Node child)
|
public static async Task AddChildXAsync(this Node parent, Node child)
|
||||||
{
|
{
|
||||||
parent.AddChild(child);
|
parent.AddChild(child);
|
||||||
await child.WaitUntilReadyAsync();
|
await child.WaitUntilReadyAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -285,4 +285,4 @@ public static class NodeExtensions
|
|||||||
return t;
|
return t;
|
||||||
throw new InvalidCastException($"Cannot cast {node} to {typeof(T)}");
|
throw new InvalidCastException($"Cannot cast {node} to {typeof(T)}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using GFramework.Core.Abstractions.Logging;
|
using System.Globalization;
|
||||||
|
using GFramework.Core.Abstractions.Logging;
|
||||||
using GFramework.Core.Logging;
|
using GFramework.Core.Logging;
|
||||||
using Godot;
|
using Godot;
|
||||||
|
|
||||||
@ -36,7 +37,7 @@ public sealed class GodotLogger(
|
|||||||
protected override void Write(LogLevel level, string message, Exception? exception)
|
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 levelStr = LevelStrings[(int)level];
|
||||||
var logPrefix = $"[{timestamp}] {levelStr} [{Name()}]";
|
var logPrefix = $"[{timestamp}] {levelStr} [{Name()}]";
|
||||||
|
|
||||||
@ -71,4 +72,4 @@ public sealed class GodotLogger(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -114,7 +114,7 @@ public abstract class SceneBehaviorBase<T> : ISceneBehavior
|
|||||||
|
|
||||||
// 调用可选接口
|
// 调用可选接口
|
||||||
if (_scene != null)
|
if (_scene != null)
|
||||||
await _scene.OnLoadAsync(param);
|
await _scene.OnLoadAsync(param).ConfigureAwait(false);
|
||||||
|
|
||||||
_isLoaded = true;
|
_isLoaded = true;
|
||||||
_isTransitioning = false;
|
_isTransitioning = false;
|
||||||
@ -130,7 +130,7 @@ public abstract class SceneBehaviorBase<T> : ISceneBehavior
|
|||||||
_isTransitioning = true;
|
_isTransitioning = true;
|
||||||
|
|
||||||
if (_scene != null)
|
if (_scene != null)
|
||||||
await _scene.OnEnterAsync();
|
await _scene.OnEnterAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
_isActive = true;
|
_isActive = true;
|
||||||
_isTransitioning = false;
|
_isTransitioning = false;
|
||||||
@ -185,7 +185,7 @@ public abstract class SceneBehaviorBase<T> : ISceneBehavior
|
|||||||
_isTransitioning = true;
|
_isTransitioning = true;
|
||||||
|
|
||||||
if (_scene != null)
|
if (_scene != null)
|
||||||
await _scene.OnExitAsync();
|
await _scene.OnExitAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
_isActive = false;
|
_isActive = false;
|
||||||
}
|
}
|
||||||
@ -208,4 +208,4 @@ public abstract class SceneBehaviorBase<T> : ISceneBehavior
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ public class LocalizationMap
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用户语言 -> Godot locale 映射表。
|
/// 用户语言 -> Godot locale 映射表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Dictionary<string, string> LanguageMap { get; set; } = new()
|
public Dictionary<string, string> LanguageMap { get; set; } = new(StringComparer.Ordinal)
|
||||||
{
|
{
|
||||||
{ "简体中文", "zh_CN" },
|
{ "简体中文", "zh_CN" },
|
||||||
{ "English", "en" }
|
{ "English", "en" }
|
||||||
@ -33,7 +33,7 @@ public class LocalizationMap
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 用户语言 -> GFramework 本地化语言码映射表。
|
/// 用户语言 -> GFramework 本地化语言码映射表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Dictionary<string, string> FrameworkLanguageMap { get; set; } = new()
|
public Dictionary<string, string> FrameworkLanguageMap { get; set; } = new(StringComparer.Ordinal)
|
||||||
{
|
{
|
||||||
{ "简体中文", "zhs" },
|
{ "简体中文", "zhs" },
|
||||||
{ "English", "eng" }
|
{ "English", "eng" }
|
||||||
@ -68,4 +68,4 @@ public class LocalizationMap
|
|||||||
|
|
||||||
return FrameworkLanguageMap.GetValueOrDefault(storedLanguage, DefaultFrameworkLanguage);
|
return FrameworkLanguageMap.GetValueOrDefault(storedLanguage, DefaultFrameworkLanguage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,8 +16,8 @@ public class GodotAudioSettings(ISettingsModel model, AudioBusMap audioBusMap)
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 应用音频设置到Godot音频系统
|
/// 应用音频设置到Godot音频系统
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>表示异步操作的任务</returns>
|
/// <returns>已完成的任务;该实现只执行同步音频总线更新。</returns>
|
||||||
public Task Apply()
|
public Task ApplyAsync()
|
||||||
{
|
{
|
||||||
var settings = model.GetData<AudioSettings>();
|
var settings = model.GetData<AudioSettings>();
|
||||||
SetBus(audioBusMap.Master, settings.MasterVolume);
|
SetBus(audioBusMap.Master, settings.MasterVolume);
|
||||||
@ -67,4 +67,4 @@ public class GodotAudioSettings(ISettingsModel model, AudioBusMap audioBusMap)
|
|||||||
Mathf.LinearToDb(Mathf.Clamp(linear, 0.0001f, 1f))
|
Mathf.LinearToDb(Mathf.Clamp(linear, 0.0001f, 1f))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,10 +11,10 @@ namespace GFramework.Godot.Setting;
|
|||||||
public class GodotGraphicsSettings(ISettingsModel model) : IResetApplyAbleSettings
|
public class GodotGraphicsSettings(ISettingsModel model) : IResetApplyAbleSettings
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 应用图形设置到Godot引擎
|
/// 将当前图形设置同步应用到 Godot 引擎。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>异步任务</returns>
|
/// <returns>已完成的任务;该实现只执行同步引擎调用,不会启动后台异步工作。</returns>
|
||||||
public async Task Apply()
|
public Task ApplyAsync()
|
||||||
{
|
{
|
||||||
var settings = model.GetData<GraphicsSettings>();
|
var settings = model.GetData<GraphicsSettings>();
|
||||||
// 创建分辨率向量
|
// 创建分辨率向量
|
||||||
@ -40,7 +40,7 @@ public class GodotGraphicsSettings(ISettingsModel model) : IResetApplyAbleSettin
|
|||||||
DisplayServer.WindowSetPosition(pos);
|
DisplayServer.WindowSetPosition(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -62,4 +62,4 @@ public class GodotGraphicsSettings(ISettingsModel model) : IResetApplyAbleSettin
|
|||||||
/// 该属性返回图形设置数据的具体类型信息。
|
/// 该属性返回图形设置数据的具体类型信息。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Type DataType { get; } = typeof(GraphicsSettings);
|
public Type DataType { get; } = typeof(GraphicsSettings);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -77,8 +77,8 @@ public class GodotLocalizationSettings : IResetApplyAbleSettings
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 应用本地化设置到 Godot 引擎与 GFramework 本地化管理器。
|
/// 应用本地化设置到 Godot 引擎与 GFramework 本地化管理器。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>完成的任务</returns>
|
/// <returns>已完成的任务;该实现通过同步 API 推进语言切换。</returns>
|
||||||
public Task Apply()
|
public Task ApplyAsync()
|
||||||
{
|
{
|
||||||
var settings = _model.GetData<LocalizationSettings>();
|
var settings = _model.GetData<LocalizationSettings>();
|
||||||
var locale = _localizationMap.ResolveGodotLocale(settings.Language);
|
var locale = _localizationMap.ResolveGodotLocale(settings.Language);
|
||||||
@ -122,4 +122,4 @@ public class GodotLocalizationSettings : IResetApplyAbleSettings
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,8 +81,9 @@ public sealed class GodotFileStorage : IStorage, IDisposable
|
|||||||
{
|
{
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
var path = ToAbsolutePath(key);
|
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文件系统路径的删除操作
|
// 处理Godot文件系统路径的删除操作
|
||||||
if (path.IsGodotPath())
|
if (path.IsGodotPath())
|
||||||
@ -179,8 +180,9 @@ public sealed class GodotFileStorage : IStorage, IDisposable
|
|||||||
{
|
{
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
var path = ToAbsolutePath(key);
|
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);
|
if (!path.IsGodotPath()) return File.Exists(path);
|
||||||
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
|
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
|
||||||
@ -238,8 +240,9 @@ public sealed class GodotFileStorage : IStorage, IDisposable
|
|||||||
{
|
{
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
var path = ToAbsolutePath(key);
|
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;
|
string content;
|
||||||
|
|
||||||
@ -290,7 +293,7 @@ public sealed class GodotFileStorage : IStorage, IDisposable
|
|||||||
|
|
||||||
dir.ListDirEnd();
|
dir.ListDirEnd();
|
||||||
return (IReadOnlyList<string>)result;
|
return (IReadOnlyList<string>)result;
|
||||||
});
|
}).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -319,7 +322,7 @@ public sealed class GodotFileStorage : IStorage, IDisposable
|
|||||||
|
|
||||||
dir.ListDirEnd();
|
dir.ListDirEnd();
|
||||||
return (IReadOnlyList<string>)result;
|
return (IReadOnlyList<string>)result;
|
||||||
});
|
}).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -345,7 +348,7 @@ public sealed class GodotFileStorage : IStorage, IDisposable
|
|||||||
var fullPath = ToAbsolutePath(path);
|
var fullPath = ToAbsolutePath(path);
|
||||||
if (!DirAccess.DirExistsAbsolute(fullPath))
|
if (!DirAccess.DirExistsAbsolute(fullPath))
|
||||||
DirAccess.MakeDirRecursiveAbsolute(fullPath);
|
DirAccess.MakeDirRecursiveAbsolute(fullPath);
|
||||||
});
|
}).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -378,8 +381,9 @@ public sealed class GodotFileStorage : IStorage, IDisposable
|
|||||||
{
|
{
|
||||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
var path = ToAbsolutePath(key);
|
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);
|
var content = _serializer.Serialize(value);
|
||||||
if (path.IsGodotPath())
|
if (path.IsGodotPath())
|
||||||
@ -397,4 +401,4 @@ public sealed class GodotFileStorage : IStorage, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ public static class UiPageBehaviorFactory
|
|||||||
/// <param name="key">UI 标识键</param>
|
/// <param name="key">UI 标识键</param>
|
||||||
/// <param name="layer">目标层级</param>
|
/// <param name="layer">目标层级</param>
|
||||||
/// <returns>对应层级的 IUiPageBehavior 实例</returns>
|
/// <returns>对应层级的 IUiPageBehavior 实例</returns>
|
||||||
|
/// <exception cref="ArgumentException">当 <paramref name="layer"/> 不是受支持的 UI 层级时抛出。</exception>
|
||||||
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
|
public static IUiPageBehavior Create<T>(T owner, string key, UiLayer layer)
|
||||||
where T : CanvasItem
|
where T : CanvasItem
|
||||||
{
|
{
|
||||||
@ -40,7 +41,7 @@ public static class UiPageBehaviorFactory
|
|||||||
UiLayer.Modal => new ModalLayerUiPageBehavior<T>(owner, key),
|
UiLayer.Modal => new ModalLayerUiPageBehavior<T>(owner, key),
|
||||||
UiLayer.Toast => new ToastLayerUiPageBehavior<T>(owner, key),
|
UiLayer.Toast => new ToastLayerUiPageBehavior<T>(owner, key),
|
||||||
UiLayer.Topmost => new TopmostLayerUiPageBehavior<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))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,7 +72,7 @@ public interface ISettingsModel : IModel
|
|||||||
- `RegisterApplicator<T>()` 注册应用器,并把其 `Data` 纳入模型管理
|
- `RegisterApplicator<T>()` 注册应用器,并把其 `Data` 纳入模型管理
|
||||||
- `InitializeAsync()` 从 `ISettingsDataRepository` 读取所有已注册设置,并在需要时执行迁移
|
- `InitializeAsync()` 从 `ISettingsDataRepository` 读取所有已注册设置,并在需要时执行迁移
|
||||||
- `SaveAllAsync()` 持久化当前所有设置数据
|
- `SaveAllAsync()` 持久化当前所有设置数据
|
||||||
- `ApplyAllAsync()` 依次调用所有 applicator 的 `Apply()`
|
- `ApplyAllAsync()` 依次调用所有 applicator 的 `ApplyAsync()`
|
||||||
|
|
||||||
## SettingsSystem
|
## SettingsSystem
|
||||||
|
|
||||||
@ -139,7 +139,7 @@ public sealed class GameplaySettingsApplicator : IResetApplyAbleSettings
|
|||||||
Data.Reset();
|
Data.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Apply()
|
public Task ApplyAsync()
|
||||||
{
|
{
|
||||||
var settings = (GameplaySettings)Data;
|
var settings = (GameplaySettings)Data;
|
||||||
TimeScale.Current = settings.GameSpeed;
|
TimeScale.Current = settings.GameSpeed;
|
||||||
|
|||||||
@ -44,7 +44,7 @@ GodotAudioSettings (Godot 特定实现) → IApplyAbleSettings (可应用设置
|
|||||||
**功能:**
|
**功能:**
|
||||||
|
|
||||||
- 接收 AudioSettings 配置对象和 AudioBusMap 总线映射
|
- 接收 AudioSettings 配置对象和 AudioBusMap 总线映射
|
||||||
- 实现 Apply() 方法,将音量设置应用到指定音频总线
|
- 实现 `ApplyAsync()` 方法,将音量设置应用到指定音频总线
|
||||||
- 支持自定义音频总线映射
|
- 支持自定义音频总线映射
|
||||||
- 自动处理音量格式转换(线性值到分贝)
|
- 自动处理音量格式转换(线性值到分贝)
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ graph TD
|
|||||||
F --> L[TranslationServer API]
|
F --> L[TranslationServer API]
|
||||||
F --> M[ILocalizationManager]
|
F --> M[ILocalizationManager]
|
||||||
|
|
||||||
N[SettingsSystem] --> O[Apply Method]
|
N[SettingsSystem] --> O[ApplyAsync Method]
|
||||||
O --> B
|
O --> B
|
||||||
O --> D
|
O --> D
|
||||||
O --> F
|
O --> F
|
||||||
@ -131,7 +131,7 @@ var settings = new AudioSettings
|
|||||||
var audioSettings = new GodotAudioSettings(settings, new AudioBusMap());
|
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);
|
var audioSettings = new GodotAudioSettings(settings, customBusMap);
|
||||||
await audioSettings.Apply();
|
await audioSettings.ApplyAsync();
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 通过设置系统使用
|
#### 通过设置系统使用
|
||||||
@ -170,7 +170,7 @@ audioSettingsData.SfxVolume = 0.9f;
|
|||||||
|
|
||||||
// 创建 Godot 音频设置应用器
|
// 创建 Godot 音频设置应用器
|
||||||
var godotAudioSettings = new GodotAudioSettings(audioSettingsData, new AudioBusMap());
|
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()
|
public async Task ToggleFullscreen()
|
||||||
{
|
{
|
||||||
_graphicsSettings.Fullscreen = !_graphicsSettings.Fullscreen;
|
_graphicsSettings.Fullscreen = !_graphicsSettings.Fullscreen;
|
||||||
await _graphicsSettings.Apply();
|
await _graphicsSettings.ApplyAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetResolution(int width, int height)
|
public async Task SetResolution(int width, int height)
|
||||||
@ -213,7 +213,7 @@ public class DisplayManager : Node
|
|||||||
_graphicsSettings.ResolutionWidth = width;
|
_graphicsSettings.ResolutionWidth = width;
|
||||||
_graphicsSettings.ResolutionHeight = height;
|
_graphicsSettings.ResolutionHeight = height;
|
||||||
_graphicsSettings.Fullscreen = false; // 窗口化时自动关闭全屏
|
_graphicsSettings.Fullscreen = false; // 窗口化时自动关闭全屏
|
||||||
await _graphicsSettings.Apply();
|
await _graphicsSettings.ApplyAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -237,7 +237,7 @@ public class ResolutionPresets
|
|||||||
settings.ResolutionWidth = width;
|
settings.ResolutionWidth = width;
|
||||||
settings.ResolutionHeight = height;
|
settings.ResolutionHeight = height;
|
||||||
settings.Fullscreen = false;
|
settings.Fullscreen = false;
|
||||||
await settings.Apply();
|
await settings.ApplyAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -266,7 +266,7 @@ public sealed class AudioBusMap
|
|||||||
```csharp
|
```csharp
|
||||||
public class GodotAudioSettings(AudioSettings settings, AudioBusMap busMap) : IApplyAbleSettings
|
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 方法实现:**
|
**Apply 方法实现:**
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public Task Apply()
|
public Task ApplyAsync()
|
||||||
{
|
{
|
||||||
SetBus(busMap.Master, settings.MasterVolume);
|
SetBus(busMap.Master, settings.MasterVolume);
|
||||||
SetBus(busMap.Bgm, settings.BgmVolume);
|
SetBus(busMap.Bgm, settings.BgmVolume);
|
||||||
@ -292,7 +292,7 @@ public Task Apply()
|
|||||||
```csharp
|
```csharp
|
||||||
public class GodotGraphicsSettings : GraphicsSettings, IApplyAbleSettings
|
public class GodotGraphicsSettings : GraphicsSettings, IApplyAbleSettings
|
||||||
{
|
{
|
||||||
public Task Apply();
|
public Task ApplyAsync();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -378,11 +378,11 @@ public class AudioManager : Node
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetMasterVolume(float linearVolume)
|
private async void SetMasterVolume(float linearVolume)
|
||||||
{
|
{
|
||||||
var settings = new AudioSettings { MasterVolume = linearVolume };
|
var settings = new AudioSettings { MasterVolume = linearVolume };
|
||||||
var audioSettings = new GodotAudioSettings(settings, new AudioBusMap());
|
var audioSettings = new GodotAudioSettings(settings, new AudioBusMap());
|
||||||
audioSettings.Apply();
|
await audioSettings.ApplyAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -419,11 +419,11 @@ public class CustomAudioManager : Node
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetMasterVolume(float linearVolume)
|
private async void SetMasterVolume(float linearVolume)
|
||||||
{
|
{
|
||||||
var audioSettingsData = new AudioSettings { MasterVolume = linearVolume };
|
var audioSettingsData = new AudioSettings { MasterVolume = linearVolume };
|
||||||
var audioSettings = new GodotAudioSettings(audioSettingsData, _customBusMap);
|
var audioSettings = new GodotAudioSettings(audioSettingsData, _customBusMap);
|
||||||
audioSettings.Apply();
|
await audioSettings.ApplyAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -522,7 +522,7 @@ public class GraphicsSettingsManager : Node
|
|||||||
|
|
||||||
public async Task ApplyAndSave()
|
public async Task ApplyAndSave()
|
||||||
{
|
{
|
||||||
await _settings.Apply();
|
await _settings.ApplyAsync();
|
||||||
SaveSettings();
|
SaveSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user