Compare commits

..

38 Commits

Author SHA1 Message Date
gewuyou
4ad880c1e3
Merge pull request #288 from GeWuYou/fix/analyzer-warning-reduction-batch
Fix/analyzer warning reduction batch
2026-04-25 14:35:38 +08:00
gewuyou
0161852618
Merge pull request #289 from GeWuYou/docs/sdk-update-documentation
Docs/sdk update documentation
2026-04-25 14:27:03 +08:00
gewuyou
a7a3eca40d fix(pr-review): 收敛PR建议并修复构建验证
- 修复 PR #288 中经本地复核后仍成立的 Core、Game 与测试建议

- 更新 WSL 标准 dotnet build 验证路径并确认 Release 构建可通过

- 补充 analyzer-warning-reduction 跟踪文档记录本轮结论与恢复点
2026-04-25 14:26:49 +08:00
GeWuYou
70c42b579f fix(scene): 修复场景替换核心异步执行的配置问题
- 将 ConfigureAwait 参数从 false 修改为 true
- 确保异步操作在正确的上下文中继续执行
- 避免潜在的死锁或性能问题
2026-04-25 13:11:45 +08:00
gewuyou
4740d30fb7 fix(core): 修复 PR 评审指出的编译与样式问题
- 修复 AsyncExtensionsTests 中错误返回 ConfiguredTaskAwaitable 导致的测试编译失败

- 收敛多处测试中的冗余 async/await 与 ValueTask 断言包装,减少 PR review 指出的告警

- 更新 StoreSelection 的 net9+ 锁实现与 analyzer-warning-reduction 跟踪文档,记录 PR #288 与当前 MSB4018 环境阻塞
2026-04-25 13:03:17 +08:00
gewuyou
c95545db92 docs(ai-plan): 同步文档治理恢复点
- 更新 tracking 与 trace 的 branch diff 实际值和后续恢复建议

- 记录本轮 README 与代码块标记批次已经提交完成
2026-04-25 11:47:52 +08:00
gewuyou
9dfee7538d docs(documentation): 补齐文档代码块标记
- 补齐 Core 热点页与基础教程页的 fenced code block 语言标记

- 更新 documentation-full-coverage-governance 的 tracking 与 trace,记录 RP-034 和 50 文件阈值
2026-04-25 11:46:17 +08:00
gewuyou
bd5cdb561f docs(readme): 优化链接标签
- 更新 5 个模块 README 的 reader-facing 链接标签,去掉裸路径和文件名式文案

- 保持原有链接目标、章节结构与正文语义不变
2026-04-25 11:35:25 +08:00
gewuyou
984fb21b94
Merge pull request #287 from GeWuYou/docs/sdk-update-documentation
docs(documentation): 收口公开文档口吻约束
2026-04-25 11:11:56 +08:00
gewuyou
18bf9f6730 docs(api-reference): 统一文档入口标签写法
- 修复 API 参考页站内入口标签风格不一致的 PR review 问题
- 更新 active tracking 与 trace,记录 PR #287 抓取结果和验证状态
- 补充 docs 构建验证结论,保持当前恢复点与后续动作同步
2026-04-25 10:50:38 +08:00
gewuyou
be336b2088 docs(ai-plan): 修正 analyzer warning reduction 指标
- 更新 active tracking 与 trace 中的当前 branch line 统计

- 将恢复点表述改为代码 stop commit 以避免后续 docs 提交导致失真
2026-04-25 10:48:13 +08:00
gewuyou
e9cd41da86 docs(ai-plan): 更新 analyzer warning reduction 恢复点
- 更新 active tracking 与 trace 到 RP-062 并收口到当前真值

- 记录本轮已达到 75 files 阈值的停止结论与最新 branch 指标

- 补充 Core build 通过结果与 Core.Tests 的 MSB4276 环境阻塞说明
2026-04-25 10:45:00 +08:00
gewuyou
9ce1fa630c refactor(core): 收敛 Core 扩展与测试的机械 warning
- 更新 ContextAware、Store 与通用扩展中的参数空校验写法以满足 analyzer 约束

- 简化 coroutine、pause、log 与 async 测试中的等待和断言包装并保持测试语义不变

- 调整测试替身异常类型与 Result 系列断言样例以减少低风险 warning 噪音
2026-04-25 10:38:48 +08:00
gewuyou
03c73a8ee5 test(core-tests): 收敛测试桩与辅助类型 warning
- 更新 ArchitectureContext、ArchitectureServices、GameContext 与环境测试桩的异常类型以满足 analyzer 约束

- 补齐 AsyncTestModel 与 AsyncTestSystem 的异步等待配置并保持测试语义不变

- 调整 ResultTests 的异类异常断言样例以避免新增编译与 analyzer 噪音
2026-04-25 10:18:40 +08:00
gewuyou
b45e551fa8 test(core-tests): 收敛选项与扩展测试的基础 warning
- 更新 OptionTests 中的 culture-sensitive 转换与 TryParse 写法
- 修正 AsyncExtensionsTests 与 CollectionExtensionsTests 的低风险异步和字符串比较写法
2026-04-25 10:12:35 +08:00
gewuyou
b7560fcc08 test(core-tests): 收敛函数式与状态测试的低风险 warning
- 补齐 WaitForTask、ResourceManager、State 与 StateMachine 测试中的低风险 ConfigureAwait(false)
- 更新 AsyncKeyLock、ResultExtensions、ResultT 与 Pipe 测试中的 culture 和异步等待写法
2026-04-25 10:08:59 +08:00
gewuyou
737d0b5037 test(core-tests): 显式指定日志测试字符串比较器
- 为 LogEntryTests 与格式化器测试中的字符串字典补充 StringComparer.Ordinal
- 保持日志相关测试断言与行为路径不变
2026-04-25 10:03:26 +08:00
gewuyou
6188876570 test(core-tests): 规范字符串比较断言写法
- 优化 ContextAwareServiceExtensionsTests 中的字符串相等断言,显式使用 Ordinal 比较
- 优化 RollingFileAppenderTests 中的 StartsWith、EndsWith、排序比较与文件名判等写法,补充 Ordinal 比较并保持测试语义不变
2026-04-25 10:01:26 +08:00
gewuyou
f67b2cedb2 refactor(core-tests): 显式指定字符串字典比较器
- 补充 LocalizationTableTests 中 string key Dictionary 的 Ordinal comparer

- 补充 QueryCoroutineExtensionsTests 中 Metadata 字典的 Ordinal comparer 并保持测试语义不变
2026-04-25 10:01:05 +08:00
gewuyou
2a9e9f26c7 test(game-tests): 简化架构配置集成测试异步断言
- 简化 ArchitectureConfigIntegrationTests 中的异步异常断言包装
- 保持重复初始化场景的测试语义与验证路径不变
2026-04-25 09:56:32 +08:00
gewuyou
65cd23ff3e test(core-tests): 简化结果与协程异步断言包装
- 简化 ResultExtensionsTests 中的异步异常断言包装
- 更新 AsyncOperationTests 的 Task 断言写法以消除低风险 analyzer 噪音
2026-04-25 09:55:10 +08:00
gewuyou
ffd62bb475 docs(ai-plan): 更新 analyzer-warning-reduction 恢复点
- 更新 analyzer-warning-reduction 跟踪文档到 RP-061 并记录当前 HEAD 与 branch diff 真值
- 补充最近批次的验证结论与当前 subagent 恢复入口
2026-04-25 09:51:37 +08:00
gewuyou
0d8f854dd2 refactor(core-tests): 简化状态机异步断言包装
- 简化 StateMachineSystemTests 中的 Task 异常断言包装

- 简化 StateMachineTests 中的异步异常断言包装并保持测试语义不变
2026-04-25 09:49:20 +08:00
gewuyou
67c9359fd2 test(core-tests): 简化异步断言包装
- 简化 Architecture、Command、Query 与 AsyncArchitecture 测试中的机械型 async/await 异步断言包装
- 更新 AsyncKeyLockManagerTests 中的 Task 断言写法以消除低风险 analyzer 噪音
2026-04-25 09:46:44 +08:00
gewuyou
9b20a07c0a refactor(game-tests): 简化异步异常断言包装
- 简化 YAML 配置加载测试中的 Assert.ThrowsAsync Task 包装

- 简化持久化测试中的异步异常断言包装并保持原有断言语义
2026-04-25 09:43:32 +08:00
gewuyou
09cbd16d3a test(game-tests): 简化 YAML 配置加载异常断言包装
- 优化 allOf、enum、not 测试中的 Assert.ThrowsAsync 委托写法,移除冗余 async/await 包装
- 保持 schema 用例语义、断言内容和异常路径不变
2026-04-25 09:43:10 +08:00
gewuyou
3be299e6f1 fix(game): 清理 UiRouterBase 的低风险异步包装
- 调整 UiRouterBase 的异步过渡调用以显式保留同步上下文

- 清理 Push Pop Replace Clear 流程中的低风险 MA0004

- 保持 UI 生命周期顺序与过渡阶段语义不变
2026-04-25 09:38:09 +08:00
gewuyou
e8eda8170c fix(routing): 清理 RouterBase 守卫异步等待的 MA0004
- 修复 RouterBase 进入守卫异步调用缺少 ConfigureAwait(false) 的机械型 MA0004
- 修复 RouterBase 离开守卫异步调用缺少 ConfigureAwait(false) 的机械型 MA0004
2026-04-25 09:35:33 +08:00
gewuyou
bad6c1b108 fix(game): 清理 FileStorage 异步存储路径的 MA0004
- 修复 FileStorage 在锁获取与异步释放路径上的 ConfigureAwait(false) 缺失
- 保持文件锁、临时文件写入和原子替换流程不变
2026-04-25 09:34:19 +08:00
gewuyou
4bb8f4f429 fix(game): 清理 SceneRouterBase 低风险异步包装
- 重构 Replace、Push、Pop、Clear 的 around pipeline 核心委托,移除匿名 async 包装

- 补充 BeforeChange 与 AfterChange 的 ConfigureAwait(false),收敛明显低风险 MA0004

- 保留场景生命周期与栈操作相关 await 的默认上下文行为,并在代码中说明原因
2026-04-25 09:32:54 +08:00
gewuyou
64c8589489 fix(game): 清理 SettingsSystem 与 ScopedStorage 的 MA0004
- 修复 SettingsSystem 中不依赖上下文的 await,补充 ConfigureAwait(false)
- 修复 ScopedStorage.DeleteAsync 的 await,保持作用域前缀语义不变
2026-04-25 09:29:01 +08:00
gewuyou
8209d7a29f docs(documentation): 收口公开文档口吻约束
- 修复公开 README 与 docs 页面中的反问式标题、维护者口吻和裸文件名链接标签
- 补充 AGENTS.md 与 gframework-doc-refresh 的 reader-facing 文档输出约束
- 更新 documentation-full-coverage-governance 的恢复点与验证记录
2026-04-25 09:27:06 +08:00
gewuyou
425c22d98f docs(ai-plan): 更新 analyzer-warning-reduction 恢复点
- 更新 analyzer-warning-reduction 的 RP-060 跟踪与 trace

- 记录并行 warning-reduction 批次的验证结果与当前分支体积

- 补充下一轮继续朝 75 文件阈值推进的恢复建议
2026-04-25 09:24:44 +08:00
gewuyou
b27bcb5832 refactor(game-tests): 清理指定加载测试的 MA0051
- 重构四个纯加载测试的固定布置为文件内私有 helper,缩短方法体长度而不改变断言语义

- 保持 schema 内容、异常路径与 item/monster 注册顺序不变,并避免触碰热重载测试
2026-04-25 09:20:29 +08:00
gewuyou
27f5a2f58e fix(game): 清理切换管道中的低风险 MA0004
- 修复 Scene 与 UI 过渡管道中间件链的多余 async 包装

- 更新低风险 await 调用以显式使用 ConfigureAwait(false)
2026-04-25 09:20:00 +08:00
gewuyou
1dae0b11a0 test(game-tests): 清理配置测试中的机械型 MA0004 包装
- 修复三个配置测试文件中 Assert.ThrowsAsync 与 Assert.DoesNotThrowAsync 的冗余 async/await 包装
- 调整文本校验异步异常测试签名以匹配去包装后的同步断言写法
2026-04-25 09:19:11 +08:00
gewuyou
877d1f38a6 fix(godot-tests): 清理模块安装测试异步断言包装
- 修复 Assert.ThrowsAsync 中机械型 async 包装\n- 保持锚点缺失场景的异常与未安装断言语义不变
2026-04-25 09:17:22 +08:00
gewuyou
b56e08adae fix(analyzer): 清理 YamlConfigLoaderTests 的异步等待 warning
- 修复 YamlConfigLoaderTests 中 Assert.ThrowsAsync 的冗余 async/await 写法

- 补充 WaitForTaskWithinAsync 的 ConfigureAwait(false) 以消除剩余 MA0004

- 更新 analyzer-warning-reduction 的 RP-059 跟踪与验证记录
2026-04-25 08:27:32 +08:00
128 changed files with 1492 additions and 1365 deletions

View File

@ -175,6 +175,15 @@ Use this exact priority:
- Do not copy wording from outdated docs just to keep page volume.
- Public docs must stay reader-facing. Do not write inventory, coverage baseline, recovery-point, batch-metric, review
backlog, or audit-wave wording into `README.md` or `docs/**`.
- Use neutral, destination-first section names and link labels. Do not expose raw filenames or paths such as
`game/index.md`, `README.md`, or `../core/cqrs.md` as visible reader-facing labels when a semantic label is
available.
- Do not use rhetorical or conversational headings in public docs, such as “你真正会用到的公开入口”、
“先理解包关系” or “想看……转到……”. Prefer direct labels such as “公开入口”、
“模块与包关系” and “相关主题”.
- Keep public docs out of internal product-decision tone. Do not publish repository-governance wording such as
“当前阶段的结论”、“不建议立即启动” or audience-maintainer tradeoff discussions unless the page itself is a public
adoption guide and the wording has been rewritten as reader-facing suitability guidance.
- If XML or audit evidence is relevant, translate it into reader guidance such as “which types to inspect first” or
“which entry points define the contract”, instead of exposing counts, dates, or governance status.
- Escape generics outside code blocks.

View File

@ -21,7 +21,7 @@ description: {{MODULE_DESCRIPTION}}
{{KEY_ENTRY_POINTS}}
## 当前边界
## 适用范围与边界
{{CURRENT_BOUNDARIES}}

View File

@ -323,6 +323,21 @@ bash scripts/validate-csharp-naming.sh
- Public documentation under `README.md` and `docs/**` MUST stay reader-facing. Do not publish governance-only content
such as inventory tables, coverage baselines, review queues, batch metrics, recovery points, trace summaries, or
“this still needs a later audit wave” notes in those user-facing pages.
- Public documentation MUST use semantic section titles and link labels. Do not surface raw filenames or paths such as
`README.md``game/index.md``../core/cqrs.md` as reader-facing navigation text when a meaningful destination label is
available.
- Public documentation MUST avoid rhetorical, self-referential, or AI-sounding headings and prompts such as
“你真正会用到的公开入口”、
“先理解包关系”、
“这个栏目应该回答什么” or “想看……转到……”. Prefer neutral labels such as
“公开入口”、
“模块与包关系”、
“栏目覆盖范围” and “相关主题”.
- Public documentation MUST present limitations, suitability, and migration boundaries as adoption guidance for readers.
Do not publish internal-governance or product-roadmap wording such as “当前阶段的结论”、
“不建议立即启动”、
“仓库当前的主要使用者” or similar maintainer-facing decision records in `README.md` or `docs/**`; that material
belongs in `ai-plan/**` if it must be tracked.
- Governance-only material such as XML audit snapshots, documentation remediation baselines, backlog status, and
recovery metadata belongs in `ai-plan/**` or other contributor-only artifacts, not in public docs.
- Treat `ai-libs/` as a read-only third-party source reference area.

View File

@ -68,7 +68,7 @@ XML 注释。这里记录的是类型族级基线,成员级契约细节仍需
## 对应文档
- 抽象接口栏目:[`../docs/zh-CN/abstractions/index.md`](../docs/zh-CN/abstractions/index.md)
- Core 抽象页:[`../docs/zh-CN/abstractions/core-abstractions.md`](../docs/zh-CN/abstractions/core-abstractions.md)
- Core 运行时入口:[`../GFramework.Core/README.md`](../GFramework.Core/README.md)
- API 参考入口:[`../docs/zh-CN/api-reference/index.md`](../docs/zh-CN/api-reference/index.md)
- 抽象接口栏目:[抽象接口总览](../docs/zh-CN/abstractions/index.md)
- Core 抽象页:[Core 抽象层说明](../docs/zh-CN/abstractions/core-abstractions.md)
- Core 运行时入口:[Core 运行时说明](../GFramework.Core/README.md)
- API 参考入口:[API 参考](../docs/zh-CN/api-reference/index.md)

View File

@ -91,5 +91,5 @@
## 对应文档
- 源码生成器总览:[`../docs/zh-CN/source-generators/index.md`](../docs/zh-CN/source-generators/index.md)
- Core 栏目:[`../docs/zh-CN/core/index.md`](../docs/zh-CN/core/index.md)
- 源码生成器总览:[源码生成器文档首页](../docs/zh-CN/source-generators/index.md)
- Core 栏目:[Core 文档首页](../docs/zh-CN/core/index.md)

View File

@ -174,8 +174,15 @@ internal sealed class AdditionalAssemblyNotificationHandlerRegistry : ICqrsHandl
/// </exception>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
if (services is null)
{
throw new ArgumentNullException(nameof(services));
}
if (logger is null)
{
throw new ArgumentNullException(nameof(logger));
}
services.AddTransient<INotificationHandler<AdditionalAssemblyNotification>>(_ => CreateHandler());
logger.Debug(

View File

@ -339,7 +339,7 @@ public class ArchitectureContextTests
{
workersReady.Signal();
startGate.Wait();
return await context.SendRequestAsync(new TestCqrsRequest());
return await context.SendRequestAsync(new TestCqrsRequest()).ConfigureAwait(false);
}))
.ToArray();

View File

@ -95,14 +95,14 @@ public class ArchitectureLifecycleBehaviorTests
/// 验证用户初始化失败时,等待 Ready 的任务会失败并进入 FailedInitialization 阶段。
/// </summary>
[Test]
public async Task InitializeAsync_When_OnInitialize_Throws_Should_Mark_FailedInitialization()
public void InitializeAsync_When_OnInitialize_Throws_Should_Mark_FailedInitialization()
{
var architecture = new PhaseTrackingArchitecture(() => throw new InvalidOperationException("boom"));
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await architecture.InitializeAsync());
var exception = Assert.ThrowsAsync<InvalidOperationException>(() => architecture.InitializeAsync());
Assert.That(exception, Is.Not.Null);
Assert.That(architecture.CurrentPhase, Is.EqualTo(ArchitecturePhase.FailedInitialization));
Assert.ThrowsAsync<InvalidOperationException>(async () => await architecture.WaitUntilReadyAsync());
Assert.ThrowsAsync<InvalidOperationException>(() => architecture.WaitUntilReadyAsync());
}
/// <summary>
@ -139,7 +139,7 @@ public class ArchitectureLifecycleBehaviorTests
var destroyOrder = new List<string>();
var architecture = new FailingInitializationArchitecture(destroyOrder);
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await architecture.InitializeAsync());
var exception = Assert.ThrowsAsync<InvalidOperationException>(() => architecture.InitializeAsync());
Assert.That(exception, Is.Not.Null);
Assert.That(architecture.CurrentPhase, Is.EqualTo(ArchitecturePhase.FailedInitialization));

View File

@ -351,12 +351,12 @@ public class TestArchitectureContextV3 : IArchitectureContext
public ValueTask<TResponse> SendRequestAsync<TResponse>(IRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public TResponse SendRequest<TResponse>(IRequest<TResponse> request)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -366,12 +366,12 @@ public class TestArchitectureContextV3 : IArchitectureContext
/// <param name="command">要发送的命令。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>命令响应任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask<TResponse> SendCommandAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -380,10 +380,10 @@ public class TestArchitectureContextV3 : IArchitectureContext
/// <typeparam name="TResponse">命令响应类型。</typeparam>
/// <param name="command">要发送的命令。</param>
/// <returns>命令响应。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public TResponse SendCommand<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -393,12 +393,12 @@ public class TestArchitectureContextV3 : IArchitectureContext
/// <param name="query">要发送的查询。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>查询结果任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask<TResponse> SendQueryAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -407,35 +407,35 @@ public class TestArchitectureContextV3 : IArchitectureContext
/// <typeparam name="TResponse">查询结果类型。</typeparam>
/// <param name="query">要发送的查询。</param>
/// <returns>查询结果。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public TResponse SendQuery<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public ValueTask PublishAsync<TNotification>(TNotification notification,
CancellationToken cancellationToken = default) where TNotification : INotification
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(
IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public ValueTask SendAsync<TCommand>(TCommand command, CancellationToken cancellationToken = default)
where TCommand : IRequest<Unit>
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public ValueTask<TResponse> SendAsync<TResponse>(IRequest<TResponse> command,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void SendCommand(ICommand command)

View File

@ -401,11 +401,11 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="request">要发送的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求响应任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask<TResponse> SendRequestAsync<TResponse>(IRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -414,10 +414,10 @@ public class TestArchitectureContext : IArchitectureContext
/// <typeparam name="TResponse">响应类型。</typeparam>
/// <param name="request">要发送的请求。</param>
/// <returns>请求响应。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public TResponse SendRequest<TResponse>(IRequest<TResponse> request)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -427,12 +427,12 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="command">要发送的命令。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>命令响应任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask<TResponse> SendCommandAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -441,10 +441,10 @@ public class TestArchitectureContext : IArchitectureContext
/// <typeparam name="TResponse">命令响应类型。</typeparam>
/// <param name="command">要发送的命令。</param>
/// <returns>命令响应。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public TResponse SendCommand<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Command.ICommand<TResponse> command)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -454,12 +454,12 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="query">要发送的查询。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>查询结果任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask<TResponse> SendQueryAsync<TResponse>(
GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -468,10 +468,10 @@ public class TestArchitectureContext : IArchitectureContext
/// <typeparam name="TResponse">查询结果类型。</typeparam>
/// <param name="query">要发送的查询。</param>
/// <returns>查询结果。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public TResponse SendQuery<TResponse>(GFramework.Cqrs.Abstractions.Cqrs.Query.IQuery<TResponse> query)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -481,11 +481,11 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="notification">要发布的通知。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>通知发布任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask PublishAsync<TNotification>(TNotification notification,
CancellationToken cancellationToken = default) where TNotification : INotification
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -495,12 +495,12 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="request">流式请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>异步响应流。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public IAsyncEnumerable<TResponse> CreateStream<TResponse>(
IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -510,11 +510,11 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="command">要发送的命令。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>命令发送任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask SendAsync<TCommand>(TCommand command, CancellationToken cancellationToken = default)
where TCommand : IRequest<Unit>
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
@ -524,11 +524,11 @@ public class TestArchitectureContext : IArchitectureContext
/// <param name="command">要发送的请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求响应任务。</returns>
/// <exception cref="NotImplementedException">该测试桩未实现此成员。</exception>
/// <exception cref="NotSupportedException">该测试桩不支持此成员。</exception>
public ValueTask<TResponse> SendAsync<TResponse>(IRequest<TResponse> command,
CancellationToken cancellationToken = default)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>

View File

@ -168,52 +168,52 @@ public class TestArchitectureWithRegistry : IArchitecture
T IArchitecture.RegisterSystem<T>(T system)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
T IArchitecture.RegisterModel<T>(T model)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
T IArchitecture.RegisterUtility<T>(T utility)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void RegisterCqrsPipelineBehavior<TBehavior>() where TBehavior : class
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
/// </summary>
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
/// <exception cref="NotSupportedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
public void RegisterCqrsHandlersFromAssembly(Assembly assembly)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
/// <exception cref="NotSupportedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public IArchitectureModule InstallModule(IArchitectureModule module)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
IArchitectureLifecycleHook IArchitecture.RegisterLifecycleHook(IArchitectureLifecycleHook hook)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
Task IArchitecture.WaitUntilReadyAsync()
@ -223,17 +223,17 @@ public class TestArchitectureWithRegistry : IArchitecture
public void RegisterUtility<T>(Action<T>? onCreated = default(Action<T>?)) where T : class, IUtility
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void RegisterModel<T>(Action<T>? onCreated = default(Action<T>?)) where T : class, IModel
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void RegisterSystem<T>(Action<T>? onCreated = default(Action<T>?)) where T : class, ISystem
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void Initialize()
@ -242,7 +242,7 @@ public class TestArchitectureWithRegistry : IArchitecture
public void Destroy()
{
throw new NotImplementedException();
throw new NotSupportedException();
}
Task IAsyncInitializable.InitializeAsync()
@ -257,7 +257,7 @@ public class TestArchitectureWithRegistry : IArchitecture
public Task WaitUntilReadyAsync()
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void RegisterLifecycleHook(IArchitectureLifecycleHook hook)
@ -266,12 +266,12 @@ public class TestArchitectureWithRegistry : IArchitecture
public Task InitializeAsync()
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public ValueTask DestroyAsync()
{
throw new NotImplementedException();
throw new NotSupportedException();
}
}
@ -287,7 +287,7 @@ public class TestArchitectureContextWithRegistry : TestArchitectureContext
_registry = registry;
}
public override TUtility GetUtility<TUtility>()
public override TUtility? GetUtility<TUtility>() where TUtility : class
{
if (typeof(TUtility) == typeof(TestRegistry))
{
@ -313,72 +313,72 @@ public class TestArchitectureWithoutRegistry : IArchitecture
T IArchitecture.RegisterSystem<T>(T system)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
T IArchitecture.RegisterModel<T>(T model)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
T IArchitecture.RegisterUtility<T>(T utility)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void RegisterCqrsPipelineBehavior<TBehavior>() where TBehavior : class
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
/// </summary>
/// <param name="assembly">包含 CQRS 处理器或生成注册器的程序集。</param>
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
/// <exception cref="NotSupportedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
public void RegisterCqrsHandlersFromAssembly(Assembly assembly)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
/// <summary>
/// 测试替身未实现显式程序集 CQRS 处理器接入入口。
/// </summary>
/// <param name="assemblies">要接入的程序集集合。</param>
/// <exception cref="NotImplementedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
/// <exception cref="NotSupportedException">该测试替身不参与 CQRS 程序集接入路径验证。</exception>
public void RegisterCqrsHandlersFromAssemblies(IEnumerable<Assembly> assemblies)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public IArchitectureModule InstallModule(IArchitectureModule module)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
IArchitectureLifecycleHook IArchitecture.RegisterLifecycleHook(IArchitectureLifecycleHook hook)
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public Task WaitUntilReadyAsync()
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void RegisterUtility<T>(Action<T>? onCreated = default(Action<T>?)) where T : class, IUtility
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void RegisterModel<T>(Action<T>? onCreated = default(Action<T>?)) where T : class, IModel
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void RegisterSystem<T>(Action<T>? onCreated = default(Action<T>?)) where T : class, ISystem
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void Initialize()
@ -387,17 +387,17 @@ public class TestArchitectureWithoutRegistry : IArchitecture
public Task InitializeAsync()
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public ValueTask DestroyAsync()
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void Destroy()
{
throw new NotImplementedException();
throw new NotSupportedException();
}
public void RegisterLifecycleHook(IArchitectureLifecycleHook hook)

View File

@ -110,7 +110,7 @@ public class AbstractAsyncCommandTests
var command = new TestAsyncCommandWithExceptionV3(input);
var asyncCommand = (IAsyncCommand)command;
Assert.ThrowsAsync<InvalidOperationException>(async () => await asyncCommand.ExecuteAsync());
Assert.ThrowsAsync<InvalidOperationException>(() => asyncCommand.ExecuteAsync());
}
/// <summary>

View File

@ -94,7 +94,7 @@ public class CommandExecutorTests
[Test]
public void SendAsync_WithNullCommand_Should_ThrowArgumentNullException()
{
Assert.ThrowsAsync<ArgumentNullException>(async () => await _commandExecutor.SendAsync(null!));
Assert.ThrowsAsync<ArgumentNullException>(() => _commandExecutor.SendAsync(null!));
}
/// <summary>
@ -118,7 +118,7 @@ public class CommandExecutorTests
[Test]
public void SendAsync_WithResult_AndNullCommand_Should_ThrowArgumentNullException()
{
Assert.ThrowsAsync<ArgumentNullException>(async () => await _commandExecutor.SendAsync<int>(null!));
Assert.ThrowsAsync<ArgumentNullException>(() => _commandExecutor.SendAsync<int>(null!));
}
}

View File

@ -25,7 +25,7 @@ public sealed class AsyncKeyLockManagerTests
using var manager = new AsyncKeyLockManager();
// Act
await using var handle = await manager.AcquireLockAsync("test-key");
await using var handle = await manager.AcquireLockAsync("test-key").ConfigureAwait(false);
// Assert
Assert.That(handle, Is.Not.Null);
@ -47,13 +47,13 @@ public sealed class AsyncKeyLockManagerTests
var index = i;
tasks.Add(Task.Run(async () =>
{
await using var handle = await manager.AcquireLockAsync("same-key");
await using var handle = await manager.AcquireLockAsync("same-key").ConfigureAwait(false);
executionOrder.Add(index);
await Task.Delay(10);
await Task.Delay(10).ConfigureAwait(false);
}));
}
await Task.WhenAll(tasks);
await Task.WhenAll(tasks).ConfigureAwait(false);
// Assert
Assert.That(executionOrder.Count, Is.EqualTo(5));
@ -75,15 +75,15 @@ public sealed class AsyncKeyLockManagerTests
var key = $"key-{i}";
tasks.Add(Task.Run(async () =>
{
await using var handle = await manager.AcquireLockAsync(key);
await using var handle = await manager.AcquireLockAsync(key).ConfigureAwait(false);
var current = Interlocked.Increment(ref concurrentCount);
maxConcurrent = Math.Max(maxConcurrent, current);
await Task.Delay(50);
await Task.Delay(50).ConfigureAwait(false);
Interlocked.Decrement(ref concurrentCount);
}));
}
await Task.WhenAll(tasks);
await Task.WhenAll(tasks).ConfigureAwait(false);
// Assert
Assert.That(maxConcurrent, Is.GreaterThan(1));
@ -94,13 +94,13 @@ public sealed class AsyncKeyLockManagerTests
{
// Arrange
using var manager = new AsyncKeyLockManager();
var handle = await manager.AcquireLockAsync("test-key");
var handle = await manager.AcquireLockAsync("test-key").ConfigureAwait(false);
// Act
await handle.DisposeAsync();
await handle.DisposeAsync().ConfigureAwait(false);
// Assert - 应该能再次获取锁
await using var handle2 = await manager.AcquireLockAsync("test-key");
await using var handle2 = await manager.AcquireLockAsync("test-key").ConfigureAwait(false);
Assert.That(handle2, Is.Not.Null);
}
@ -117,13 +117,13 @@ public sealed class AsyncKeyLockManagerTests
var key = $"key-{i % 10}";
tasks.Add(Task.Run(async () =>
{
await using var handle = await manager.AcquireLockAsync(key);
await Task.Delay(1);
await using var handle = await manager.AcquireLockAsync(key).ConfigureAwait(false);
await Task.Delay(1).ConfigureAwait(false);
}));
}
// Assert
Assert.DoesNotThrowAsync(async () => await Task.WhenAll(tasks));
await Task.WhenAll(tasks).ConfigureAwait(false);
}
[Test]
@ -139,14 +139,14 @@ public sealed class AsyncKeyLockManagerTests
{
tasks.Add(Task.Run(async () =>
{
await using var handle = await manager.AcquireLockAsync("same-key");
await using var handle = await manager.AcquireLockAsync("same-key").ConfigureAwait(false);
var temp = counter;
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
counter = temp + 1;
}));
}
await Task.WhenAll(tasks);
await Task.WhenAll(tasks).ConfigureAwait(false);
// Assert
Assert.That(counter, Is.EqualTo(100));
@ -161,13 +161,13 @@ public sealed class AsyncKeyLockManagerTests
lockTimeout: TimeSpan.FromMilliseconds(200));
// Act
await using (var handle = await manager.AcquireLockAsync("temp-key"))
await using (var handle = await manager.AcquireLockAsync("temp-key").ConfigureAwait(false))
{
// 持有锁
}
// 等待清理
await Task.Delay(400);
await Task.Delay(400).ConfigureAwait(false);
var stats = manager.GetStatistics();
@ -184,10 +184,10 @@ public sealed class AsyncKeyLockManagerTests
lockTimeout: TimeSpan.FromMilliseconds(200));
// Act
await using var handle = await manager.AcquireLockAsync("active-key");
await using var handle = await manager.AcquireLockAsync("active-key").ConfigureAwait(false);
// 等待清理尝试
await Task.Delay(400);
await Task.Delay(400).ConfigureAwait(false);
var activeLocks = manager.GetActiveLocks();
@ -202,9 +202,9 @@ public sealed class AsyncKeyLockManagerTests
using var manager = new AsyncKeyLockManager();
// Act
await using (await manager.AcquireLockAsync("key1"))
await using (await manager.AcquireLockAsync("key1").ConfigureAwait(false))
{
await using var handle2 = await manager.AcquireLockAsync("key2");
await using var handle2 = await manager.AcquireLockAsync("key2").ConfigureAwait(false);
var stats = manager.GetStatistics();
// Assert
@ -223,8 +223,8 @@ public sealed class AsyncKeyLockManagerTests
using var manager = new AsyncKeyLockManager();
// Act
await using var handle1 = await manager.AcquireLockAsync("key1");
await using var handle2 = await manager.AcquireLockAsync("key2");
await using var handle1 = await manager.AcquireLockAsync("key1").ConfigureAwait(false);
await using var handle2 = await manager.AcquireLockAsync("key2").ConfigureAwait(false);
var activeLocks = manager.GetActiveLocks();
@ -243,7 +243,7 @@ public sealed class AsyncKeyLockManagerTests
manager.Dispose();
// Act & Assert
Assert.ThrowsAsync<ObjectDisposedException>(async () => await manager.AcquireLockAsync("test-key"));
Assert.ThrowsAsync<ObjectDisposedException>(() => manager.AcquireLockAsync("test-key").AsTask());
}
[Test]
@ -254,14 +254,14 @@ public sealed class AsyncKeyLockManagerTests
using var cts = new CancellationTokenSource();
// 先获取锁
await using var handle = await manager.AcquireLockAsync("test-key", cts.Token);
await using var handle = await manager.AcquireLockAsync("test-key", cts.Token).ConfigureAwait(false);
// Act
await cts.CancelAsync();
await cts.CancelAsync().ConfigureAwait(false);
// Assert
Assert.CatchAsync<OperationCanceledException>(async () =>
await manager.AcquireLockAsync("test-key", cts.Token));
Assert.CatchAsync<OperationCanceledException>(() =>
manager.AcquireLockAsync("test-key", cts.Token).AsTask());
}
[Test]
@ -295,14 +295,14 @@ public sealed class AsyncKeyLockManagerTests
{
for (var j = 0; j < 10; j++)
{
await using var handle = await manager.AcquireLockAsync($"key-{j % 5}");
await Task.Delay(10);
await using var handle = await manager.AcquireLockAsync($"key-{j % 5}").ConfigureAwait(false);
await Task.Delay(10).ConfigureAwait(false);
}
}));
}
// Assert
Assert.DoesNotThrowAsync(async () => await Task.WhenAll(tasks));
await Task.WhenAll(tasks).ConfigureAwait(false);
}
[Test]
@ -324,14 +324,14 @@ public sealed class AsyncKeyLockManagerTests
{
// Arrange
using var manager = new AsyncKeyLockManager();
var handle = await manager.AcquireLockAsync("test-key");
var handle = await manager.AcquireLockAsync("test-key").ConfigureAwait(false);
// Act
await handle.DisposeAsync();
await handle.DisposeAsync();
await handle.DisposeAsync().ConfigureAwait(false);
await handle.DisposeAsync().ConfigureAwait(false);
handle.Dispose();
// Assert - 不应该抛出异常
Assert.Pass();
}
}
}

View File

@ -104,7 +104,7 @@ public class AsyncOperationTests
op.SetException(expectedException);
Assert.That(async () => await op.Task, Throws.InstanceOf<InvalidOperationException>());
Assert.That(() => op.Task, Throws.InstanceOf<InvalidOperationException>());
}
/// <summary>
@ -339,4 +339,4 @@ public class AsyncOperationTests
Assert.DoesNotThrow(() => op.SetCompleted());
}
}
}

View File

@ -6,6 +6,7 @@ using GFramework.Core.Abstractions.Rule;
using GFramework.Core.Coroutine.Extensions;
using GFramework.Core.Coroutine.Instructions;
using Moq;
using NUnit.Framework;
namespace GFramework.Core.Tests.Coroutine;
@ -18,6 +19,8 @@ namespace GFramework.Core.Tests.Coroutine;
[TestFixture]
public class CommandCoroutineExtensionsTests
{
private static readonly TimeSpan WaitForTaskTimeout = TimeSpan.FromSeconds(1);
/// <summary>
/// 测试用的简单命令类
/// </summary>
@ -83,15 +86,10 @@ public class CommandCoroutineExtensionsTests
var coroutine = contextAware.SendCommandCoroutineWithErrorHandler(command, ex => capturedException = ex);
// 迭代协程直到完成
while (coroutine.MoveNext())
{
if (coroutine.Current is WaitForTask waitForTask)
{
// 等待任务完成
await Task.Delay(10);
}
}
Assert.That(coroutine.MoveNext(), Is.True);
Assert.That(coroutine.Current, Is.TypeOf<WaitForTask>());
await WaitForTaskAsync((WaitForTask)coroutine.Current);
Assert.That(coroutine.MoveNext(), Is.False);
Assert.That(capturedException, Is.Null);
}
@ -114,15 +112,10 @@ public class CommandCoroutineExtensionsTests
var coroutine = contextAware.SendCommandCoroutineWithErrorHandler(command, ex => capturedException = ex);
// 迭代协程直到完成
while (coroutine.MoveNext())
{
if (coroutine.Current is WaitForTask waitForTask)
{
// 等待任务完成
await Task.Delay(10);
}
}
Assert.That(coroutine.MoveNext(), Is.True);
Assert.That(coroutine.Current, Is.TypeOf<WaitForTask>());
await WaitForTaskAsync((WaitForTask)coroutine.Current);
Assert.That(coroutine.MoveNext(), Is.False);
Assert.That(capturedException, Is.Not.Null);
// 异常被包装为 AggregateException
@ -148,17 +141,12 @@ public class CommandCoroutineExtensionsTests
var coroutine = contextAware.SendCommandCoroutineWithErrorHandler(command);
// 迭代协程应该抛出异常
Assert.Throws<InvalidOperationException>(() =>
{
while (coroutine.MoveNext())
{
if (coroutine.Current is WaitForTask waitForTask)
{
Task.Delay(10).Wait();
}
}
});
Assert.That(coroutine.MoveNext(), Is.True);
Assert.That(coroutine.Current, Is.TypeOf<WaitForTask>());
Assert.That(
SpinWait.SpinUntil(() => ((WaitForTask)coroutine.Current).IsDone, WaitForTaskTimeout),
Is.True);
Assert.Throws<InvalidOperationException>(() => coroutine.MoveNext());
}
/// <summary>
@ -201,8 +189,9 @@ public class CommandCoroutineExtensionsTests
});
// 启动协程并等待命令执行完成
coroutine.MoveNext(); // 进入命令发送阶段
if (coroutine.Current is WaitForTask waitForTask) await Task.Delay(10); // 等待命令任务完成
Assert.That(coroutine.MoveNext(), Is.True); // 进入命令发送阶段
Assert.That(coroutine.Current, Is.TypeOf<WaitForTask>());
await WaitForTaskAsync((WaitForTask)coroutine.Current);
// 此时协程应该在等待事件
Assert.That(coroutine.MoveNext(), Is.True); // 等待事件阶段
@ -296,15 +285,16 @@ public class CommandCoroutineExtensionsTests
command); // null回调
// 启动协程
coroutine.MoveNext(); // 进入命令发送阶段
if (coroutine.Current is WaitForTask waitForTask) await Task.Delay(10); // 等待命令任务完成
Assert.That(coroutine.MoveNext(), Is.True); // 进入命令发送阶段
Assert.That(coroutine.Current, Is.TypeOf<WaitForTask>());
await WaitForTaskAsync((WaitForTask)coroutine.Current);
// 触发事件
var testEvent = new TestEvent { Data = "TestData" };
eventCallback?.Invoke(testEvent);
// 协程应该能正常完成
Assert.That(() => coroutine.MoveNext(), Throws.Nothing);
Assert.DoesNotThrow(() => coroutine.MoveNext());
}
/// <summary>
@ -340,8 +330,9 @@ public class CommandCoroutineExtensionsTests
_ => { });
// 启动协程 - 命令失败时协程仍然继续
coroutine.MoveNext(); // 进入命令发送阶段
if (coroutine.Current is WaitForTask waitForTask) await Task.Delay(10); // 等待命令任务完成
Assert.That(coroutine.MoveNext(), Is.True); // 进入命令发送阶段
Assert.That(coroutine.Current, Is.TypeOf<WaitForTask>());
await WaitForTaskAsync((WaitForTask)coroutine.Current);
// 命令执行失败后,协程继续执行
Assert.Pass();
@ -441,4 +432,18 @@ public class CommandCoroutineExtensionsTests
// 调用 MoveNext 时应该抛出 InvalidOperationException
Assert.Throws<InvalidOperationException>(() => coroutine.MoveNext());
}
}
private static async Task WaitForTaskAsync(WaitForTask waitForTask)
{
var timeoutAt = DateTime.UtcNow + WaitForTaskTimeout;
// 协程通过轮询 IsDone 观察异步命令完成,这里保持相同语义但避免固定延时。
while (!waitForTask.IsDone)
{
if (DateTime.UtcNow >= timeoutAt)
Assert.Fail("WaitForTask did not complete within the expected time.");
await Task.Yield();
}
}
}

View File

@ -254,7 +254,7 @@ public class QueryCoroutineExtensionsTests
{
Name = "ComplexName",
Values = new List<int> { 1, 2, 3 },
Metadata = new Dictionary<string, object> { { "key", "value" } }
Metadata = new Dictionary<string, object>(StringComparer.Ordinal) { { "key", "value" } }
};
ComplexResult? receivedResult = null;
@ -362,7 +362,7 @@ internal class ComplexQuery : IQuery<ComplexResult>
private IArchitectureContext? _context;
public string Name { get; set; } = string.Empty;
public List<int> Values { get; set; } = new();
public Dictionary<string, object> Metadata { get; set; } = new();
public Dictionary<string, object> Metadata { get; set; } = new(StringComparer.Ordinal);
public void SetContext(IArchitectureContext context)
{
@ -388,4 +388,4 @@ internal class ComplexResult
public string ProcessedName { get; set; } = string.Empty;
public int Sum { get; set; }
public int Count { get; set; }
}
}

View File

@ -66,8 +66,6 @@ public class TaskCoroutineExtensionsTests
var task = Task.FromResult(42);
var instruction = task.AsCoroutineInstruction();
task.Wait();
Assert.That(instruction.Result, Is.EqualTo(42));
}
@ -165,7 +163,7 @@ public class TaskCoroutineExtensionsTests
Assert.That(completed, Is.False);
tcs.SetResult(null);
Task.Delay(50).Wait();
Task.Delay(50).ConfigureAwait(false).GetAwaiter().GetResult();
scheduler.Update();
scheduler.Update();
@ -189,7 +187,7 @@ public class TaskCoroutineExtensionsTests
Assert.That(scheduler.ActiveCoroutineCount, Is.EqualTo(1));
tcs.SetResult(42);
Task.Delay(50).Wait();
Task.Delay(50).ConfigureAwait(false).GetAwaiter().GetResult();
scheduler.Update();
scheduler.Update();
@ -265,7 +263,7 @@ public class TaskCoroutineExtensionsTests
tcs.SetResult(null);
tcs2.SetResult(42);
Task.Delay(50).Wait();
Task.Delay(50).ConfigureAwait(false).GetAwaiter().GetResult();
scheduler.Update();
scheduler.Update();
@ -286,4 +284,4 @@ public class TaskCoroutineExtensionsTests
CurrentTime += DeltaTime;
}
}
}
}

View File

@ -14,7 +14,7 @@ namespace GFramework.Core.Tests.Coroutine
}
[Test]
public async Task Constructor_WithCompletedTask_IsDoneImmediately()
public void Constructor_WithCompletedTask_IsDoneImmediately()
{
// Arrange
var completedTask = Task.FromResult("test");
@ -28,7 +28,7 @@ namespace GFramework.Core.Tests.Coroutine
}
[Test]
public async Task Constructor_WithIncompleteTask_IsNotDoneInitially()
public void Constructor_WithIncompleteTask_IsNotDoneInitially()
{
// Arrange
var tcs = new TaskCompletionSource<string>();
@ -54,7 +54,7 @@ namespace GFramework.Core.Tests.Coroutine
// Act
tcs.SetResult("completed");
await Task.Delay(10); // Allow time for continuation
await Task.Delay(10).ConfigureAwait(false); // Allow time for continuation
// Assert final state
Assert.That(waitForTask.IsDone, Is.True);
@ -62,7 +62,7 @@ namespace GFramework.Core.Tests.Coroutine
}
[Test]
public async Task Update_DoesNotChangeState()
public void Update_DoesNotChangeState()
{
// Arrange
var completedTask = Task.FromResult("test");
@ -85,7 +85,7 @@ namespace GFramework.Core.Tests.Coroutine
// Act
tcs.SetException(new InvalidOperationException("Test exception"));
await Task.Delay(10); // Allow time for continuation
await Task.Delay(10).ConfigureAwait(false); // Allow time for continuation
// Assert
Assert.That(waitForTask.IsDone, Is.True);
@ -93,4 +93,4 @@ namespace GFramework.Core.Tests.Coroutine
Assert.That(waitForTask.Exception?.InnerException, Is.TypeOf<InvalidOperationException>());
}
}
}
}

View File

@ -284,15 +284,14 @@ public class WaitForTaskTests
var expectedValue = 123;
var task = Task.Run(async () =>
{
await Task.Delay(50);
await Task.Delay(50).ConfigureAwait(false);
return expectedValue;
});
var wait = new WaitForTask<int>(task);
await task;
Task.Delay(100).Wait();
await task.ConfigureAwait(false);
await Task.Delay(100).ConfigureAwait(false);
Assert.That(wait.IsDone, Is.True);
Assert.That(wait.Result, Is.EqualTo(expectedValue));
@ -313,4 +312,4 @@ public class WaitForTaskTests
Assert.That(wait.IsDone, Is.True);
}
}
}

View File

@ -33,11 +33,11 @@ public class AsyncExtensionsTests
public void WithTimeout_Should_Throw_TimeoutException_When_Task_Exceeds_Timeout()
{
// Act & Assert
Assert.ThrowsAsync<TimeoutException>(async () =>
await AsyncExtensions.WithTimeoutAsync(
Assert.ThrowsAsync<TimeoutException>(() =>
AsyncExtensions.WithTimeoutAsync(
async ct =>
{
await Task.Delay(TimeSpan.FromSeconds(2), ct);
await Task.Delay(TimeSpan.FromSeconds(2), ct).ConfigureAwait(false);
return 42;
},
TimeSpan.FromMilliseconds(100)));
@ -53,11 +53,11 @@ public class AsyncExtensionsTests
using var cts = new CancellationTokenSource();
cts.Cancel();
// Act & Assert
Assert.ThrowsAsync<TaskCanceledException>(async () =>
await AsyncExtensions.WithTimeoutAsync(
Assert.ThrowsAsync<TaskCanceledException>(() =>
AsyncExtensions.WithTimeoutAsync(
async ct =>
{
await Task.Delay(TimeSpan.FromSeconds(2), ct);
await Task.Delay(TimeSpan.FromSeconds(2), ct).ConfigureAwait(false);
return 42;
},
TimeSpan.FromSeconds(1),
@ -74,13 +74,13 @@ public class AsyncExtensionsTests
var innerTaskCanceled = false;
// Act & Assert
Assert.ThrowsAsync<TimeoutException>(async () =>
await AsyncExtensions.WithTimeoutAsync(
Assert.ThrowsAsync<TimeoutException>(() =>
AsyncExtensions.WithTimeoutAsync(
async ct =>
{
try
{
await Task.Delay(TimeSpan.FromSeconds(5), ct);
await Task.Delay(TimeSpan.FromSeconds(5), ct).ConfigureAwait(false);
return 0;
}
catch (OperationCanceledException)
@ -121,8 +121,8 @@ public class AsyncExtensionsTests
public void WithTimeout_NoResult_Should_Throw_TimeoutException_When_Task_Exceeds_Timeout()
{
// Act & Assert
Assert.ThrowsAsync<TimeoutException>(async () =>
await AsyncExtensions.WithTimeoutAsync(
Assert.ThrowsAsync<TimeoutException>(() =>
AsyncExtensions.WithTimeoutAsync(
ct => Task.Delay(TimeSpan.FromSeconds(2), ct),
TimeSpan.FromMilliseconds(100)));
}
@ -137,13 +137,13 @@ public class AsyncExtensionsTests
var innerTaskCanceled = false;
// Act & Assert
Assert.ThrowsAsync<TimeoutException>(async () =>
await AsyncExtensions.WithTimeoutAsync(
Assert.ThrowsAsync<TimeoutException>(() =>
AsyncExtensions.WithTimeoutAsync(
async ct =>
{
try
{
await Task.Delay(TimeSpan.FromSeconds(5), ct);
await Task.Delay(TimeSpan.FromSeconds(5), ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@ -217,8 +217,8 @@ public class AsyncExtensionsTests
};
// Act & Assert
Assert.ThrowsAsync<AggregateException>(async () =>
await taskFactory.WithRetryAsync(2, TimeSpan.FromMilliseconds(10)));
Assert.ThrowsAsync<AggregateException>(() =>
taskFactory.WithRetryAsync(2, TimeSpan.FromMilliseconds(10)));
}
/// <summary>
@ -236,11 +236,11 @@ public class AsyncExtensionsTests
};
// Act & Assert
Assert.ThrowsAsync<AggregateException>(async () =>
await taskFactory.WithRetryAsync(3, TimeSpan.FromMilliseconds(10),
Assert.ThrowsAsync<AggregateException>(() =>
taskFactory.WithRetryAsync(3, TimeSpan.FromMilliseconds(10),
ex => ex is not ArgumentException));
await Task.Delay(50); // 等待任务完成
await Task.Delay(50).ConfigureAwait(false); // 等待任务完成
Assert.That(attemptCount, Is.EqualTo(1)); // 不应该重试
}
@ -330,4 +330,4 @@ public class AsyncExtensionsTests
// Assert
Assert.That(capturedEx, Is.SameAs(expectedException));
}
}
}

View File

@ -173,7 +173,7 @@ public class CollectionExtensionsTests
{
var method = typeof(GFramework.Core.Extensions.CollectionExtensions)
.GetMethods()
.Single(static method => method.Name == nameof(GFramework.Core.Extensions.CollectionExtensions.ToDictionarySafe));
.Single(static method => string.Equals(method.Name, nameof(GFramework.Core.Extensions.CollectionExtensions.ToDictionarySafe), StringComparison.Ordinal));
var methodGenericArguments = method.GetGenericArguments();
var returnTypeGenericArguments = method.ReturnType.GetGenericArguments();

View File

@ -11,6 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Globalization;
using GFramework.Core.Functional;
namespace GFramework.Core.Tests.Extensions;
@ -122,7 +123,7 @@ public class ResultExtensionsTests
public void Map_Should_Transform_Success_Value()
{
var result = Result<int>.Succeed(42);
var mapped = result.Map(x => x.ToString());
var mapped = result.Map(x => x.ToString(CultureInfo.InvariantCulture));
Assert.That(mapped.IsSuccess, Is.True);
Assert.That(mapped.Match(succ: v => v, fail: _ => ""), Is.EqualTo("42"));
}
@ -135,7 +136,7 @@ public class ResultExtensionsTests
{
var exception = new Exception("Error");
var result = Result<int>.Fail(exception);
var mapped = result.Map(x => x.ToString());
var mapped = result.Map(x => x.ToString(CultureInfo.InvariantCulture));
Assert.That(mapped.IsFaulted, Is.True);
Assert.That(mapped.Exception, Is.SameAs(exception));
}
@ -157,7 +158,7 @@ public class ResultExtensionsTests
public void Bind_Should_Chain_Success_Results()
{
var result = Result<int>.Succeed(42);
var bound = result.Bind(x => Result<string>.Succeed(x.ToString()));
var bound = result.Bind(x => Result<string>.Succeed(x.ToString(CultureInfo.InvariantCulture)));
Assert.That(bound.IsSuccess, Is.True);
Assert.That(bound.Match(succ: v => v, fail: _ => ""), Is.EqualTo("42"));
}
@ -170,7 +171,7 @@ public class ResultExtensionsTests
{
var exception = new Exception("Error");
var result = Result<int>.Fail(exception);
var bound = result.Bind(x => Result<string>.Succeed(x.ToString()));
var bound = result.Bind(x => Result<string>.Succeed(x.ToString(CultureInfo.InvariantCulture)));
Assert.That(bound.IsFaulted, Is.True);
Assert.That(bound.Exception, Is.SameAs(exception));
}
@ -449,9 +450,9 @@ public class ResultExtensionsTests
{
var result = await ResultExtensions.TryAsync(async () =>
{
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
return 42;
});
}).ConfigureAwait(false);
Assert.That(result.IsSuccess, Is.True);
Assert.That(result.Match(succ: v => v, fail: _ => 0), Is.EqualTo(42));
}
@ -464,9 +465,9 @@ public class ResultExtensionsTests
{
var result = await ResultExtensions.TryAsync<int>(async () =>
{
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
throw new InvalidOperationException("Error");
});
}).ConfigureAwait(false);
Assert.That(result.IsFaulted, Is.True);
Assert.That(result.Exception, Is.TypeOf<InvalidOperationException>());
}
@ -477,7 +478,8 @@ public class ResultExtensionsTests
[Test]
public async Task TryAsync_Should_Handle_Synchronous_Exceptions()
{
var result = await ResultExtensions.TryAsync<int>(() => throw new InvalidOperationException("Sync error"));
var result = await ResultExtensions.TryAsync<int>(() => throw new InvalidOperationException("Sync error"))
.ConfigureAwait(false);
Assert.That(result.IsFaulted, Is.True);
}
@ -487,7 +489,7 @@ public class ResultExtensionsTests
[Test]
public void TryAsync_Should_Throw_ArgumentNullException_When_Function_Is_Null()
{
Assert.ThrowsAsync<ArgumentNullException>(async () => await ResultExtensions.TryAsync<int>(null!));
Assert.ThrowsAsync<ArgumentNullException>(() => ResultExtensions.TryAsync<int>(null!));
}
/// <summary>

View File

@ -11,6 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Globalization;
using GFramework.Core.Functional;
using NUnit.Framework;
@ -122,7 +123,7 @@ public class OptionTests
public void Map_WithSome_Should_Map_Value()
{
var option = Option<int>.Some(42);
var mapped = option.Map(x => x.ToString());
var mapped = option.Map(x => x.ToString(CultureInfo.InvariantCulture));
Assert.That(mapped.IsSome, Is.True);
Assert.That(mapped.GetOrElse(""), Is.EqualTo("42"));
}
@ -134,7 +135,7 @@ public class OptionTests
public void Map_WithNone_Should_Return_None()
{
var option = Option<int>.None;
var mapped = option.Map(x => x.ToString());
var mapped = option.Map(x => x.ToString(CultureInfo.InvariantCulture));
Assert.That(mapped.IsNone, Is.True);
}
@ -155,7 +156,7 @@ public class OptionTests
public void Bind_WithSome_Should_Bind_Value()
{
var option = Option<string>.Some("42");
var bound = option.Bind(s => int.TryParse(s, out var i)
var bound = option.Bind(s => int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i)
? Option<int>.Some(i)
: Option<int>.None);
@ -170,7 +171,7 @@ public class OptionTests
public void Bind_WithSome_Can_Return_None()
{
var option = Option<string>.Some("invalid");
var bound = option.Bind(s => int.TryParse(s, out var i)
var bound = option.Bind(s => int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i)
? Option<int>.Some(i)
: Option<int>.None);
@ -505,4 +506,4 @@ public class OptionTests
var result = option.ToString();
Assert.That(result, Is.EqualTo("None"));
}
}
}

View File

@ -1,3 +1,4 @@
using System.Globalization;
using GFramework.Core.Functional.Pipe;
using NUnit.Framework;
@ -123,7 +124,7 @@ public class PipeExtensionsTests
var result = value
.Pipe(x => x * 2)
.Pipe(x => x + 10)
.Pipe(x => x.ToString());
.Pipe(x => x.ToString(CultureInfo.InvariantCulture));
// Assert
Assert.That(result, Is.EqualTo("20"));
@ -139,7 +140,7 @@ public class PipeExtensionsTests
var value = 42;
// Act
var result = value.Let(x => x.ToString());
var result = value.Let(x => x.ToString(CultureInfo.InvariantCulture));
// Assert
Assert.That(result, Is.EqualTo("42"));
@ -171,7 +172,7 @@ public class PipeExtensionsTests
var result = value.Let(s => new
{
Original = s,
Upper = s.ToUpper(),
Upper = s.ToUpperInvariant(),
Length = s.Length
});
@ -280,4 +281,4 @@ public class PipeExtensionsTests
// Assert
Assert.That(result, Is.EqualTo("Large: 20"));
}
}
}

View File

@ -11,6 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Globalization;
using GFramework.Core.Functional;
using NUnit.Framework;
@ -314,7 +315,7 @@ public class ResultTTests
public void Map_Should_Transform_Value_When_Success()
{
var result = Result<int>.Succeed(42);
var mapped = result.Map(x => x.ToString());
var mapped = result.Map(x => x.ToString(CultureInfo.InvariantCulture));
Assert.That(mapped.IsSuccess, Is.True);
Assert.That(mapped.Match(succ: v => v, fail: _ => ""), Is.EqualTo("42"));
}
@ -327,7 +328,7 @@ public class ResultTTests
{
var exception = new Exception("Error");
var result = Result<int>.Fail(exception);
var mapped = result.Map(x => x.ToString());
var mapped = result.Map(x => x.ToString(CultureInfo.InvariantCulture));
Assert.That(mapped.IsFaulted, Is.True);
Assert.That(mapped.Exception, Is.SameAs(exception));
}
@ -360,7 +361,7 @@ public class ResultTTests
public void Bind_Should_Chain_Success_Results()
{
var result = Result<int>.Succeed(42);
var bound = result.Bind(x => Result<string>.Succeed(x.ToString()));
var bound = result.Bind(x => Result<string>.Succeed(x.ToString(CultureInfo.InvariantCulture)));
Assert.That(bound.IsSuccess, Is.True);
Assert.That(bound.Match(succ: v => v, fail: _ => ""), Is.EqualTo("42"));
}
@ -373,7 +374,7 @@ public class ResultTTests
{
var exception = new Exception("Error");
var result = Result<int>.Fail(exception);
var bound = result.Bind(x => Result<string>.Succeed(x.ToString()));
var bound = result.Bind(x => Result<string>.Succeed(x.ToString(CultureInfo.InvariantCulture)));
Assert.That(bound.IsFaulted, Is.True);
Assert.That(bound.Exception, Is.SameAs(exception));
}
@ -413,9 +414,9 @@ public class ResultTTests
var result = Result<int>.Succeed(42);
var mapped = await result.MapAsync(async x =>
{
await Task.Delay(1);
return x.ToString();
});
await Task.Delay(1).ConfigureAwait(false);
return x.ToString(CultureInfo.InvariantCulture);
}).ConfigureAwait(false);
Assert.That(mapped.IsSuccess, Is.True);
Assert.That(mapped.Match(succ: v => v, fail: _ => ""), Is.EqualTo("42"));
}
@ -430,9 +431,9 @@ public class ResultTTests
var result = Result<int>.Fail(exception);
var mapped = await result.MapAsync(async x =>
{
await Task.Delay(1);
return x.ToString();
});
await Task.Delay(1).ConfigureAwait(false);
return x.ToString(CultureInfo.InvariantCulture);
}).ConfigureAwait(false);
Assert.That(mapped.IsFaulted, Is.True);
Assert.That(mapped.Exception, Is.SameAs(exception));
}
@ -446,9 +447,9 @@ public class ResultTTests
var result = Result<int>.Succeed(42);
var mapped = await result.MapAsync<string>(async _ =>
{
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
throw new InvalidOperationException("Async error");
});
}).ConfigureAwait(false);
Assert.That(mapped.IsFaulted, Is.True);
Assert.That(mapped.Exception, Is.TypeOf<InvalidOperationException>());
}
@ -550,7 +551,7 @@ public class ResultTTests
public void Equals_Should_Return_False_When_Exception_Types_Differ()
{
var result1 = Result<int>.Fail(new InvalidOperationException("Error"));
var result2 = Result<int>.Fail(new ArgumentException("Error"));
var result2 = Result<int>.Fail(new InvalidCastException("Error"));
Assert.That(result1.Equals(result2), Is.False);
}
@ -804,4 +805,4 @@ public class ResultTTests
var bottom2 = Result<int>.Bottom;
Assert.That(bottom1.Equals(bottom2), Is.True);
}
}
}

View File

@ -329,7 +329,7 @@ public class ResultTests
{
// Arrange
var result1 = Result.Failure(new InvalidOperationException("Error"));
var result2 = Result.Failure(new ArgumentException("Error"));
var result2 = Result.Failure(new InvalidCastException("Error"));
// Act & Assert
Assert.That(result1.Equals(result2), Is.False);

View File

@ -9,7 +9,7 @@ public class LocalizationTableTests
public void GetRawText_ShouldReturnCorrectText()
{
// Arrange
var data = new Dictionary<string, string>
var data = new Dictionary<string, string>(StringComparer.Ordinal)
{
["test.key"] = "Test Value"
};
@ -26,13 +26,13 @@ public class LocalizationTableTests
public void GetRawText_WithFallback_ShouldReturnFallbackValue()
{
// Arrange
var fallbackData = new Dictionary<string, string>
var fallbackData = new Dictionary<string, string>(StringComparer.Ordinal)
{
["test.key"] = "Fallback Value"
};
var fallbackTable = new LocalizationTable("test", "eng", fallbackData);
var data = new Dictionary<string, string>();
var data = new Dictionary<string, string>(StringComparer.Ordinal);
var table = new LocalizationTable("test", "zhs", data, fallbackTable);
// Act
@ -46,7 +46,7 @@ public class LocalizationTableTests
public void ContainsKey_ShouldReturnTrue_WhenKeyExists()
{
// Arrange
var data = new Dictionary<string, string>
var data = new Dictionary<string, string>(StringComparer.Ordinal)
{
["test.key"] = "Test Value"
};
@ -63,13 +63,13 @@ public class LocalizationTableTests
public void Merge_ShouldOverrideExistingValues()
{
// Arrange
var data = new Dictionary<string, string>
var data = new Dictionary<string, string>(StringComparer.Ordinal)
{
["test.key"] = "Original Value"
};
var table = new LocalizationTable("test", "eng", data);
var overrides = new Dictionary<string, string>
var overrides = new Dictionary<string, string>(StringComparer.Ordinal)
{
["test.key"] = "Override Value"
};
@ -81,4 +81,4 @@ public class LocalizationTableTests
// Assert
Assert.That(result, Is.EqualTo("Override Value"));
}
}
}

View File

@ -48,7 +48,7 @@ public class DefaultLogFormatterTests
[Test]
public void Format_WithProperties_ShouldIncludeProperties()
{
var properties = new Dictionary<string, object?>
var properties = new Dictionary<string, object?>(StringComparer.Ordinal)
{
["UserId"] = 12345,
["UserName"] = "TestUser"
@ -66,7 +66,7 @@ public class DefaultLogFormatterTests
[Test]
public void Format_WithNullProperty_ShouldHandleNull()
{
var properties = new Dictionary<string, object?>
var properties = new Dictionary<string, object?>(StringComparer.Ordinal)
{
["Key1"] = null
};
@ -114,4 +114,4 @@ public class DefaultLogFormatterTests
Assert.That(result, Does.Contain(message));
}
}
}

View File

@ -54,7 +54,7 @@ public class JsonLogFormatterTests
[Test]
public void Format_WithProperties_ShouldIncludePropertiesObject()
{
var properties = new Dictionary<string, object?>
var properties = new Dictionary<string, object?>(StringComparer.Ordinal)
{
["UserId"] = 12345,
["UserName"] = "TestUser",
@ -93,7 +93,7 @@ public class JsonLogFormatterTests
[Test]
public void Format_WithNullProperty_ShouldHandleNull()
{
var properties = new Dictionary<string, object?>
var properties = new Dictionary<string, object?>(StringComparer.Ordinal)
{
["Key1"] = null,
["Key2"] = "value"
@ -170,7 +170,7 @@ public class JsonLogFormatterTests
[Test]
public void Format_WithComplexProperties_ShouldSerializeCorrectly()
{
var properties = new Dictionary<string, object?>
var properties = new Dictionary<string, object?>(StringComparer.Ordinal)
{
["Number"] = 123,
["String"] = "test",
@ -184,4 +184,4 @@ public class JsonLogFormatterTests
Assert.That(() => JsonDocument.Parse(result), Throws.Nothing);
}
}
}

View File

@ -152,22 +152,22 @@ public class LogContextTests
var task1Values = new List<object?>();
var task2Values = new List<object?>();
var task1 = Task.Run(() =>
var task1 = Task.Run(async () =>
{
using (LogContext.Push("TaskId", "Task1"))
{
task1Values.Add(LogContext.Current["TaskId"]);
Task.Delay(50).Wait();
await Task.Delay(50);
task1Values.Add(LogContext.Current["TaskId"]);
}
});
var task2 = Task.Run(() =>
var task2 = Task.Run(async () =>
{
using (LogContext.Push("TaskId", "Task2"))
{
task2Values.Add(LogContext.Current["TaskId"]);
Task.Delay(50).Wait();
await Task.Delay(50);
task2Values.Add(LogContext.Current["TaskId"]);
}
});
@ -201,4 +201,4 @@ public class LogContextTests
Assert.That(LogContext.Current.Count, Is.EqualTo(0));
}
}
}

View File

@ -13,7 +13,7 @@ public class LogEntryTests
public void Constructor_WithAllParameters_ShouldCreateEntry()
{
var timestamp = DateTime.UtcNow;
var properties = new Dictionary<string, object?> { ["Key1"] = "Value1" };
var properties = new Dictionary<string, object?>(StringComparer.Ordinal) { ["Key1"] = "Value1" };
var exception = new InvalidOperationException("Test");
var entry = new LogEntry(timestamp, LogLevel.Info, "TestLogger", "Test message", exception, properties);
@ -63,7 +63,7 @@ public class LogEntryTests
public void GetAllProperties_WithProperties_ShouldReturnOnlyProperties()
{
LogContext.Clear();
var properties = new Dictionary<string, object?> { ["PropKey"] = "PropValue" };
var properties = new Dictionary<string, object?>(StringComparer.Ordinal) { ["PropKey"] = "PropValue" };
var entry = new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", "Test message", null, properties);
var allProps = entry.GetAllProperties();
@ -78,7 +78,7 @@ public class LogEntryTests
LogContext.Clear();
using (LogContext.Push("ContextKey", "ContextValue"))
{
var properties = new Dictionary<string, object?> { ["PropKey"] = "PropValue" };
var properties = new Dictionary<string, object?>(StringComparer.Ordinal) { ["PropKey"] = "PropValue" };
var entry = new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", "Test message", null, properties);
var allProps = entry.GetAllProperties();
@ -97,7 +97,7 @@ public class LogEntryTests
LogContext.Clear();
using (LogContext.Push("Key1", "ContextValue"))
{
var properties = new Dictionary<string, object?> { ["Key1"] = "PropValue" };
var properties = new Dictionary<string, object?>(StringComparer.Ordinal) { ["Key1"] = "PropValue" };
var entry = new LogEntry(DateTime.UtcNow, LogLevel.Info, "TestLogger", "Test message", null, properties);
var allProps = entry.GetAllProperties();
@ -124,7 +124,7 @@ public class LogEntryTests
public void RecordEquality_WithSameValues_ShouldBeEqual()
{
var timestamp = DateTime.UtcNow;
var properties = new Dictionary<string, object?> { ["Key1"] = "Value1" };
var properties = new Dictionary<string, object?>(StringComparer.Ordinal) { ["Key1"] = "Value1" };
var entry1 = new LogEntry(timestamp, LogLevel.Info, "TestLogger", "Test message", null, properties);
var entry2 = new LogEntry(timestamp, LogLevel.Info, "TestLogger", "Test message", null, properties);
@ -142,4 +142,4 @@ public class LogEntryTests
Assert.That(entry1, Is.Not.EqualTo(entry2));
}
}
}

View File

@ -68,7 +68,7 @@ public class RollingFileAppenderTests
}
// 检查是否生成了多个文件
var files = Directory.GetFiles(_testDir, "*.log").OrderBy(f => f).ToArray();
var files = Directory.GetFiles(_testDir, "*.log");
Assert.That(files.Length, Is.GreaterThan(1));
}
@ -108,13 +108,21 @@ public class RollingFileAppenderTests
appender.Flush();
}
var files = Directory.GetFiles(_testDir, "*.log").Select(Path.GetFileName).OrderBy(f => f).ToArray();
var files = Directory.GetFiles(_testDir, "*.log")
.Select(static path => Path.GetFileName(path) ?? string.Empty)
.OrderBy(f => f, System.StringComparer.Ordinal)
.ToArray();
// 应该有 app.log, app.1.log, app.2.log 等
Assert.That(files, Does.Contain("app.log"));
if (files.Length > 1)
{
Assert.That(files.Any(f => f.StartsWith("app.") && f.EndsWith(".log") && f != "app.log"), Is.True);
Assert.That(
files.Any(f =>
f.StartsWith("app.", System.StringComparison.Ordinal) &&
f.EndsWith(".log", System.StringComparison.Ordinal) &&
!string.Equals(f, "app.log", System.StringComparison.Ordinal)),
Is.True);
}
}
@ -163,4 +171,4 @@ public class RollingFileAppenderTests
var content = File.ReadAllText(_testFilePath);
Assert.That(content, Does.Contain("Test message"));
}
}
}

View File

@ -20,7 +20,7 @@ public sealed class AsyncTestModel : AbstractModel, IAsyncInitializable
/// <returns>表示异步操作的Task</returns>
public async Task InitializeAsync()
{
await Task.Delay(10);
await Task.Delay(10).ConfigureAwait(false);
Initialized = true;
}
@ -45,4 +45,4 @@ public sealed class AsyncTestModel : AbstractModel, IAsyncInitializable
protected override void OnInit()
{
}
}
}

View File

@ -22,9 +22,9 @@ public class PauseStackManagerTests
/// 在每个测试方法执行后清理资源
/// </summary>
[TearDown]
public void TearDown()
public async Task TearDown()
{
_manager.DestroyAsync();
await _manager.DestroyAsync().ConfigureAwait(false);
}
private PauseStackManager _manager = null!;
@ -416,7 +416,7 @@ public class PauseStackManagerTests
_manager.Push("Gameplay", PauseGroup.Gameplay);
mockHandler.Reset();
await _manager.DestroyAsync();
await _manager.DestroyAsync().ConfigureAwait(false);
Assert.That(mockHandler.CallCount, Is.EqualTo(2));
Assert.That(mockHandler.LastIsPaused, Is.False);

View File

@ -80,7 +80,7 @@ public class AbstractAsyncQueryTests
var query = new TestAsyncQueryWithExceptionV4(input);
var asyncQuery = (IAsyncQuery<int>)query;
Assert.ThrowsAsync<InvalidOperationException>(async () => await asyncQuery.DoAsync());
Assert.ThrowsAsync<InvalidOperationException>(() => asyncQuery.DoAsync());
}
/// <summary>

View File

@ -44,7 +44,7 @@ public class AsyncQueryExecutorTests
[Test]
public void SendAsync_WithNullQuery_Should_ThrowArgumentNullException()
{
Assert.ThrowsAsync<ArgumentNullException>(async () => await _asyncQueryExecutor.SendAsync<int>(null!));
Assert.ThrowsAsync<ArgumentNullException>(() => _asyncQueryExecutor.SendAsync<int>(null!));
}
/// <summary>
@ -100,7 +100,7 @@ public class AsyncQueryExecutorTests
var input = new TestAsyncQueryInput { Value = 0 };
var query = new TestAsyncQueryWithException(input);
Assert.ThrowsAsync<InvalidOperationException>(async () => await _asyncQueryExecutor.SendAsync(query));
Assert.ThrowsAsync<InvalidOperationException>(() => _asyncQueryExecutor.SendAsync(query));
}
/// <summary>

View File

@ -33,7 +33,7 @@ public class TestResourceLoader : IResourceLoader<TestResource>
public async Task<TestResource> LoadAsync(string path)
{
await Task.Delay(10); // 模拟异步加载
await Task.Delay(10).ConfigureAwait(false); // 模拟异步加载
return Load(path);
}
@ -99,7 +99,7 @@ public class ResourceManagerTests
[Test]
public async Task LoadAsync_Should_Load_Resource()
{
var resource = await _resourceManager.LoadAsync<TestResource>("test/resource1.txt");
var resource = await _resourceManager.LoadAsync<TestResource>("test/resource1.txt").ConfigureAwait(false);
Assert.That(resource, Is.Not.Null);
Assert.That(resource!.Content, Is.EqualTo("Content 1"));
@ -404,4 +404,4 @@ public class ResourceManagerTests
handle2!.Dispose();
Assert.That(_resourceManager.IsLoaded("test/resource2.txt"), Is.False);
}
}
}

View File

@ -93,7 +93,7 @@ public class ContextAwareEnvironmentExtensionsTests
return false;
}
public T GetRequired<T>(string key) where T : class => throw new NotImplementedException();
public T GetRequired<T>(string key) where T : class => throw new NotSupportedException();
public void Register(string key, object value)
{
@ -115,7 +115,7 @@ public class ContextAwareEnvironmentExtensionsTests
return false;
}
public T GetRequired<T>(string key) where T : class => throw new NotImplementedException();
public T GetRequired<T>(string key) where T : class => throw new NotSupportedException();
public void Register(string key, object value)
{
@ -129,4 +129,4 @@ public class ContextAwareEnvironmentExtensionsTests
private class TestContextAware : ContextAwareBase
{
}
}
}

View File

@ -181,8 +181,8 @@ public class ContextAwareServiceExtensionsTests
// Assert
Assert.That(results, Has.Count.GreaterThanOrEqualTo(2));
Assert.That(results.Any(s => s is TestSystem ts && ts.Name == "System1"), Is.True);
Assert.That(results.Any(s => s is TestSystem ts && ts.Name == "System2"), Is.True);
Assert.That(results.Any(s => s is TestSystem ts && string.Equals(ts.Name, "System1", System.StringComparison.Ordinal)), Is.True);
Assert.That(results.Any(s => s is TestSystem ts && string.Equals(ts.Name, "System2", System.StringComparison.Ordinal)), Is.True);
}
[Test]
@ -200,8 +200,8 @@ public class ContextAwareServiceExtensionsTests
// Assert
Assert.That(results, Has.Count.EqualTo(2));
Assert.That(results.Any(m => m.Name == "Model1"), Is.True);
Assert.That(results.Any(m => m.Name == "Model2"), Is.True);
Assert.That(results.Any(m => string.Equals(m.Name, "Model1", System.StringComparison.Ordinal)), Is.True);
Assert.That(results.Any(m => string.Equals(m.Name, "Model2", System.StringComparison.Ordinal)), Is.True);
}
[Test]
@ -219,8 +219,8 @@ public class ContextAwareServiceExtensionsTests
// Assert
Assert.That(results, Has.Count.EqualTo(2));
Assert.That(results.Any(u => u.Name == "Utility1"), Is.True);
Assert.That(results.Any(u => u.Name == "Utility2"), Is.True);
Assert.That(results.Any(u => string.Equals(u.Name, "Utility1", System.StringComparison.Ordinal)), Is.True);
Assert.That(results.Any(u => string.Equals(u.Name, "Utility2", System.StringComparison.Ordinal)), Is.True);
}
[Test]
@ -304,4 +304,4 @@ public class ContextAwareServiceExtensionsTests
private class TestContextAware : ContextAwareBase
{
}
}
}

View File

@ -136,9 +136,9 @@ public class StateMachineSystemTests
/// 测试DestroyAsync方法不抛出异常
/// </summary>
[Test]
public async Task DestroyAsync_Should_Not_Throw_Exception()
public void DestroyAsync_Should_Not_Throw_Exception()
{
Assert.That(async () => await _stateMachine!.DestroyAsync(), Throws.Nothing);
Assert.That(() => _stateMachine!.DestroyAsync(), Throws.Nothing);
}
/// <summary>

View File

@ -216,7 +216,7 @@ public class StateMachineTests
[Test]
public void ChangeToAsync_ToUnregisteredState_Should_ThrowInvalidOperationException()
{
Assert.ThrowsAsync<InvalidOperationException>(async () => await _stateMachine.ChangeToAsync<TestStateV2>());
Assert.ThrowsAsync<InvalidOperationException>(() => _stateMachine.ChangeToAsync<TestStateV2>());
}
/// <summary>
@ -525,7 +525,7 @@ public sealed class TestAsyncState : IState, IAsyncState
/// <param name="from">从哪个状态进入</param>
public async Task OnEnterAsync(IState? from)
{
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
EnterCalled = true;
EnterCallCount++;
EnterFrom = from;
@ -537,7 +537,7 @@ public sealed class TestAsyncState : IState, IAsyncState
/// <param name="to">离开到哪个状态</param>
public async Task OnExitAsync(IState? to)
{
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
ExitCalled = true;
ExitCallCount++;
ExitTo = to;
@ -550,7 +550,7 @@ public sealed class TestAsyncState : IState, IAsyncState
/// <returns>是否允许转换</returns>
public async Task<bool> CanTransitionToAsync(IState target)
{
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
CanTransitionToCallCount++;
return AllowTransition;
}
@ -601,4 +601,4 @@ public static class StateMachineExtensions
.GetValue(stateMachine) is Dictionary<Type, IState> states &&
states.ContainsKey(typeof(T));
}
}
}

View File

@ -674,7 +674,7 @@ public sealed class ConcreteAsyncStateV2 : IState, IAsyncState
/// <param name="from">从哪个状态进入</param>
public async Task OnEnterAsync(IState? from)
{
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
EnterCalled = true;
EnterCallCount++;
EnterFrom = from;
@ -686,7 +686,7 @@ public sealed class ConcreteAsyncStateV2 : IState, IAsyncState
/// <param name="to">退出到哪个状态</param>
public async Task OnExitAsync(IState? to)
{
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
ExitCalled = true;
ExitCallCount++;
ExitTo = to;
@ -699,7 +699,7 @@ public sealed class ConcreteAsyncStateV2 : IState, IAsyncState
/// <returns>如果可以转换则返回true否则返回false</returns>
public async Task<bool> CanTransitionToAsync(IState target)
{
await Task.Delay(1);
await Task.Delay(1).ConfigureAwait(false);
CanTransitionToAsyncAction?.Invoke(target);
return AllowTransitions;
}
@ -731,4 +731,4 @@ public sealed class ConcreteAsyncStateV2 : IState, IAsyncState
{
throw new InvalidOperationException("Sync CanTransitionTo should not be called for async state");
}
}
}

View File

@ -16,7 +16,7 @@ public sealed class AsyncTestSystem : ISystem, IAsyncInitializable
public async Task InitializeAsync()
{
await Task.Delay(10);
await Task.Delay(10).ConfigureAwait(false);
Initialized = true;
}
@ -44,4 +44,4 @@ public sealed class AsyncTestSystem : ISystem, IAsyncInitializable
public void OnArchitecturePhase(ArchitecturePhase phase)
{
}
}
}

View File

@ -89,11 +89,11 @@ public class AsyncArchitectureTests : ArchitectureTestsBase<AsyncTestArchitectur
/// </summary>
/// <returns>异步任务</returns>
[Test]
public async Task Architecture_Should_Stop_Initialization_When_Model_Init_Fails()
public void Architecture_Should_Stop_Initialization_When_Model_Init_Fails()
{
Architecture!.AddPostRegistrationHook(a => { a.RegisterModel(new FailingModel()); });
Assert.ThrowsAsync<InvalidOperationException>(async () => await Architecture.InitializeAsync());
Assert.ThrowsAsync<InvalidOperationException>(() => Architecture.InitializeAsync());
Assert.That(
Architecture.CurrentPhase,
@ -134,14 +134,14 @@ public class AsyncArchitectureTests : ArchitectureTestsBase<AsyncTestArchitectur
/// </summary>
/// <returns>异步任务</returns>
[Test]
public async Task InitializeAsync_Should_Handle_Exception_Correctly()
public void InitializeAsync_Should_Handle_Exception_Correctly()
{
Architecture!.AddPostRegistrationHook(a =>
a.RegisterModel(new FailingModel())
);
Assert.ThrowsAsync<InvalidOperationException>(async () => await Architecture.InitializeAsync());
Assert.ThrowsAsync<InvalidOperationException>(() => Architecture.InitializeAsync());
AssertInitializationFailed();
}
}
}

View File

@ -20,8 +20,15 @@ public static class CollectionExtensions
/// </example>
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(action);
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}
if (action is null)
{
throw new ArgumentNullException(nameof(action));
}
foreach (var item in source) action(item);
}
@ -58,7 +65,10 @@ public static class CollectionExtensions
/// </example>
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source) where T : class
{
ArgumentNullException.ThrowIfNull(source);
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}
return source.Where(item => item is not null)!;
}
@ -88,9 +98,20 @@ public static class CollectionExtensions
Func<T, TValue> valueSelector) where TKey : notnull
#pragma warning restore MA0016
{
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(keySelector);
ArgumentNullException.ThrowIfNull(valueSelector);
if (source is null)
{
throw new ArgumentNullException(nameof(source));
}
if (keySelector is null)
{
throw new ArgumentNullException(nameof(keySelector));
}
if (valueSelector is null)
{
throw new ArgumentNullException(nameof(valueSelector));
}
var dictionary = new Dictionary<TKey, TValue>();

View File

@ -16,7 +16,11 @@ public static class ContextAwareEnvironmentExtensions
/// <returns>指定类型的环境对象,如果无法转换则返回null</returns>
public static T? GetEnvironment<T>(this IContextAware contextAware) where T : class
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetEnvironment() as T;
}
@ -28,8 +32,12 @@ public static class ContextAwareEnvironmentExtensions
/// <returns>环境对象</returns>
public static IEnvironment GetEnvironment(this IContextAware contextAware)
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetEnvironment();
}
}
}

View File

@ -17,6 +17,7 @@ public static class ContextAwareEventExtensions
public static void SendEvent<TEvent>(this IContextAware contextAware) where TEvent : new()
{
ArgumentNullException.ThrowIfNull(contextAware);
var context = contextAware.GetContext();
context.SendEvent<TEvent>();
}
@ -67,4 +68,4 @@ public static class ContextAwareEventExtensions
var context = contextAware.GetContext();
context.UnRegisterEvent(onEvent);
}
}
}

View File

@ -24,7 +24,11 @@ public static class ContextAwareServiceExtensions
/// <exception cref="InvalidOperationException">当指定服务未注册时抛出</exception>
public static TService GetService<TService>(this IContextAware contextAware) where TService : class
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return GetRequiredComponent(context, static architectureContext => architectureContext.GetService<TService>(),
"Service");
@ -40,7 +44,11 @@ public static class ContextAwareServiceExtensions
/// <exception cref="InvalidOperationException">当指定系统未注册时抛出</exception>
public static TSystem GetSystem<TSystem>(this IContextAware contextAware) where TSystem : class, ISystem
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return GetRequiredComponent(context, static architectureContext => architectureContext.GetSystem<TSystem>(),
"System");
@ -56,7 +64,11 @@ public static class ContextAwareServiceExtensions
/// <exception cref="InvalidOperationException">当指定模型未注册时抛出</exception>
public static TModel GetModel<TModel>(this IContextAware contextAware) where TModel : class, IModel
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return GetRequiredComponent(context, static architectureContext => architectureContext.GetModel<TModel>(),
"Model");
@ -72,7 +84,11 @@ public static class ContextAwareServiceExtensions
/// <exception cref="InvalidOperationException">当指定工具未注册时抛出</exception>
public static TUtility GetUtility<TUtility>(this IContextAware contextAware) where TUtility : class, IUtility
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return GetRequiredComponent(context, static architectureContext => architectureContext.GetUtility<TUtility>(),
"Utility");
@ -92,7 +108,11 @@ public static class ContextAwareServiceExtensions
public static IReadOnlyList<TService> GetServices<TService>(this IContextAware contextAware)
where TService : class
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetServices<TService>();
}
@ -107,7 +127,11 @@ public static class ContextAwareServiceExtensions
public static IReadOnlyList<TSystem> GetSystems<TSystem>(this IContextAware contextAware)
where TSystem : class, ISystem
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetSystems<TSystem>();
}
@ -122,7 +146,11 @@ public static class ContextAwareServiceExtensions
public static IReadOnlyList<TModel> GetModels<TModel>(this IContextAware contextAware)
where TModel : class, IModel
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetModels<TModel>();
}
@ -137,7 +165,11 @@ public static class ContextAwareServiceExtensions
public static IReadOnlyList<TUtility> GetUtilities<TUtility>(this IContextAware contextAware)
where TUtility : class, IUtility
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetUtilities<TUtility>();
}
@ -152,7 +184,11 @@ public static class ContextAwareServiceExtensions
public static IReadOnlyList<TService> GetServicesByPriority<TService>(this IContextAware contextAware)
where TService : class
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetServicesByPriority<TService>();
}
@ -167,7 +203,11 @@ public static class ContextAwareServiceExtensions
public static IReadOnlyList<TSystem> GetSystemsByPriority<TSystem>(this IContextAware contextAware)
where TSystem : class, ISystem
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetSystemsByPriority<TSystem>();
}
@ -182,7 +222,11 @@ public static class ContextAwareServiceExtensions
public static IReadOnlyList<TModel> GetModelsByPriority<TModel>(this IContextAware contextAware)
where TModel : class, IModel
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetModelsByPriority<TModel>();
}
@ -197,7 +241,11 @@ public static class ContextAwareServiceExtensions
public static IReadOnlyList<TUtility> GetUtilitiesByPriority<TUtility>(this IContextAware contextAware)
where TUtility : class, IUtility
{
ArgumentNullException.ThrowIfNull(contextAware);
if (contextAware is null)
{
throw new ArgumentNullException(nameof(contextAware));
}
var context = contextAware.GetContext();
return context.GetUtilitiesByPriority<TUtility>();
}
@ -206,11 +254,14 @@ public static class ContextAwareServiceExtensions
Func<IArchitectureContext, TComponent> resolver, string componentKind)
where TComponent : class
{
ArgumentNullException.ThrowIfNull(context);
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
var component = resolver(context);
return component ?? throw new InvalidOperationException($"{componentKind} {typeof(TComponent)} not registered");
}
#endregion
}
}

View File

@ -27,7 +27,9 @@ public static class GuardExtensions
this T? value,
[CallerArgumentExpression(nameof(value))] string? paramName = null) where T : class
{
ArgumentNullException.ThrowIfNull(value, paramName);
if (value is null)
throw new ArgumentNullException(paramName);
return value;
}
@ -51,7 +53,8 @@ public static class GuardExtensions
this string? value,
[CallerArgumentExpression(nameof(value))] string? paramName = null)
{
ArgumentNullException.ThrowIfNull(value, paramName);
if (value is null)
throw new ArgumentNullException(paramName);
if (value.Length == 0)
throw new ArgumentException("字符串不能为空", paramName);
@ -79,7 +82,8 @@ public static class GuardExtensions
this string? value,
[CallerArgumentExpression(nameof(value))] string? paramName = null)
{
ArgumentNullException.ThrowIfNull(value, paramName);
if (value is null)
throw new ArgumentNullException(paramName);
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("字符串不能为空或仅包含空白字符", paramName);
@ -108,7 +112,8 @@ public static class GuardExtensions
this IEnumerable<T>? source,
[CallerArgumentExpression(nameof(source))] string? paramName = null)
{
ArgumentNullException.ThrowIfNull(source, paramName);
if (source is null)
throw new ArgumentNullException(paramName);
if (!source.Any())
throw new ArgumentException("集合不能为空", paramName);

View File

@ -30,10 +30,14 @@ public static class NumericExtensions
ArgumentNullException.ThrowIfNull(max);
if (min.CompareTo(max) > 0)
{
throw new ArgumentException($"最小值 ({min}) 不能大于最大值 ({max})", nameof(min));
}
if (inclusive)
{
return value.CompareTo(min) >= 0 && value.CompareTo(max) <= 0;
}
return value.CompareTo(min) > 0 && value.CompareTo(max) < 0;
}
@ -71,7 +75,9 @@ public static class NumericExtensions
public static float InverseLerp(this float value, float from, float to)
{
if (Math.Abs(to - from) < float.Epsilon)
{
throw new DivideByZeroException("起始值和目标值不能相等");
}
return (value - from) / (to - from);
}

View File

@ -27,8 +27,15 @@ public static class StoreEventBusExtensions
bool publishDispatches = true,
bool publishStateChanges = true)
{
ArgumentNullException.ThrowIfNull(store);
ArgumentNullException.ThrowIfNull(eventBus);
if (store is null)
{
throw new ArgumentNullException(nameof(store));
}
if (eventBus is null)
{
throw new ArgumentNullException(nameof(eventBus));
}
IUnRegister? dispatchBridge = null;
IUnRegister? stateBridge = null;
@ -60,8 +67,15 @@ public static class StoreEventBusExtensions
/// <returns>用于移除 dispatch 桥接中间件的句柄。</returns>
public static IUnRegister BridgeDispatchesToEventBus<TState>(this Store<TState> store, IEventBus eventBus)
{
ArgumentNullException.ThrowIfNull(store);
ArgumentNullException.ThrowIfNull(eventBus);
if (store is null)
{
throw new ArgumentNullException(nameof(store));
}
if (eventBus is null)
{
throw new ArgumentNullException(nameof(eventBus));
}
return store.RegisterMiddleware(new DispatchEventBusMiddleware<TState>(eventBus));
}
@ -77,8 +91,15 @@ public static class StoreEventBusExtensions
public static IUnRegister BridgeStateChangesToEventBus<TState>(this IReadonlyStore<TState> store,
IEventBus eventBus)
{
ArgumentNullException.ThrowIfNull(store);
ArgumentNullException.ThrowIfNull(eventBus);
if (store is null)
{
throw new ArgumentNullException(nameof(store));
}
if (eventBus is null)
{
throw new ArgumentNullException(nameof(eventBus));
}
return store.Subscribe(state =>
eventBus.Send(new StoreStateChangedEvent<TState>(state, DateTimeOffset.UtcNow)));
@ -115,4 +136,4 @@ public static class StoreEventBusExtensions
_eventBus.Send(new StoreDispatchedEvent<TState>(dispatchRecord));
}
}
}
}

View File

@ -42,11 +42,15 @@ public static class StringExtensions
ArgumentNullException.ThrowIfNull(suffix);
if (maxLength < suffix.Length)
{
throw new ArgumentOutOfRangeException(nameof(maxLength),
$"最大长度必须至少为后缀长度 ({suffix.Length})");
}
if (str.Length <= maxLength)
{
return str;
}
return string.Concat(str.AsSpan(0, maxLength - suffix.Length), suffix);
}
@ -71,4 +75,4 @@ public static class StringExtensions
return string.Join(separator, values);
}
}
}

View File

@ -31,7 +31,7 @@
- `GFramework.Game`
- `GFramework.Godot`
如果你只需要契约,不需要实现层,改为依赖 [`../GFramework.Core.Abstractions/README.md`](../GFramework.Core.Abstractions/README.md)。
如果你只需要契约,不需要实现层,改为依赖 [Core 抽象层说明](../GFramework.Core.Abstractions/README.md)。
## 子系统地图
@ -92,7 +92,7 @@ dotnet add package GeWuYou.GFramework.Core.Abstractions
最小示例见:
- [`../docs/zh-CN/getting-started/quick-start.md`](../docs/zh-CN/getting-started/quick-start.md)
- [快速开始示例](../docs/zh-CN/getting-started/quick-start.md)
## 什么时候继续接别的包
@ -103,8 +103,8 @@ dotnet add package GeWuYou.GFramework.Core.Abstractions
## 对应文档
- Core 栏目:[`../docs/zh-CN/core/index.md`](../docs/zh-CN/core/index.md)
- Core 抽象层:[`../docs/zh-CN/abstractions/core-abstractions.md`](../docs/zh-CN/abstractions/core-abstractions.md)
- API 参考入口:[`../docs/zh-CN/api-reference/index.md`](../docs/zh-CN/api-reference/index.md)
- CQRS[`../docs/zh-CN/core/cqrs.md`](../docs/zh-CN/core/cqrs.md)
- 入门指南:[`../docs/zh-CN/getting-started/index.md`](../docs/zh-CN/getting-started/index.md)
- Core 栏目:[Core 文档首页](../docs/zh-CN/core/index.md)
- Core 抽象层:[Core Abstractions 文档](../docs/zh-CN/abstractions/core-abstractions.md)
- API 参考入口:[API 参考首页](../docs/zh-CN/api-reference/index.md)
- CQRS[CQRS 文档](../docs/zh-CN/core/cqrs.md)
- 入门指南:[入门指南首页](../docs/zh-CN/getting-started/index.md)

View File

@ -40,6 +40,7 @@ public sealed class StoreBuilder<TState> : IStoreBuilder<TState>
public IStoreBuilder<TState> UseMiddleware(IStoreMiddleware<TState> middleware)
{
ArgumentNullException.ThrowIfNull(middleware);
_configurators.Add(store => store.UseMiddleware(middleware));
return this;
}
@ -109,6 +110,7 @@ public sealed class StoreBuilder<TState> : IStoreBuilder<TState>
public IStoreBuilder<TState> AddReducer<TAction>(Func<TState, TAction, TState> reducer)
{
ArgumentNullException.ThrowIfNull(reducer);
_configurators.Add(store => store.RegisterReducer(reducer));
return this;
}
@ -122,7 +124,8 @@ public sealed class StoreBuilder<TState> : IStoreBuilder<TState>
public IStoreBuilder<TState> AddReducer<TAction>(IReducer<TState, TAction> reducer)
{
ArgumentNullException.ThrowIfNull(reducer);
_configurators.Add(store => store.RegisterReducer(reducer));
return this;
}
}
}

View File

@ -24,10 +24,13 @@ public sealed class StoreSelection<TState, TSelected> : IReadonlyBindablePropert
/// </summary>
private readonly List<SelectionListenerSubscription> _listeners = [];
/// <summary>
/// 保护监听器集合和底层 Store 订阅句柄的同步锁。
/// </summary>
#if NET9_0_OR_GREATER
// net9.0 及以上目标使用专用 Lock以满足分析器对专用同步原语的建议。
private readonly System.Threading.Lock _lock = new();
#else
// net8.0 目标仍回退到 object 锁,以保持多目标编译兼容性。
private readonly object _lock = new();
#endif
/// <summary>
/// 负责从完整状态中投影出局部状态的选择器。
@ -82,7 +85,9 @@ public sealed class StoreSelection<TState, TSelected> : IReadonlyBindablePropert
/// <returns>用于取消订阅的句柄。</returns>
IUnRegister IEvent.Register(Action onEvent)
{
ArgumentNullException.ThrowIfNull(onEvent);
if (onEvent is null)
throw new ArgumentNullException(nameof(onEvent));
return Register(_ => onEvent());
}
@ -94,7 +99,8 @@ public sealed class StoreSelection<TState, TSelected> : IReadonlyBindablePropert
/// <exception cref="ArgumentNullException">当 <paramref name="onValueChanged"/> 为 <see langword="null"/> 时抛出。</exception>
public IUnRegister Register(Action<TSelected> onValueChanged)
{
ArgumentNullException.ThrowIfNull(onValueChanged);
if (onValueChanged is null)
throw new ArgumentNullException(nameof(onValueChanged));
var subscription = new SelectionListenerSubscription(onValueChanged);
var shouldAttach = false;
@ -126,7 +132,8 @@ public sealed class StoreSelection<TState, TSelected> : IReadonlyBindablePropert
/// <exception cref="ArgumentNullException">当 <paramref name="action"/> 为 <see langword="null"/> 时抛出。</exception>
public IUnRegister RegisterWithInitValue(Action<TSelected> action)
{
ArgumentNullException.ThrowIfNull(action);
if (action is null)
throw new ArgumentNullException(nameof(action));
var subscription = new SelectionListenerSubscription(action)
{
@ -189,7 +196,8 @@ public sealed class StoreSelection<TState, TSelected> : IReadonlyBindablePropert
/// <exception cref="ArgumentNullException">当 <paramref name="onValueChanged"/> 为 <see langword="null"/> 时抛出。</exception>
public void UnRegister(Action<TSelected> onValueChanged)
{
ArgumentNullException.ThrowIfNull(onValueChanged);
if (onValueChanged is null)
throw new ArgumentNullException(nameof(onValueChanged));
SelectionListenerSubscription? subscriptionToRemove = null;
@ -391,4 +399,4 @@ public sealed class StoreSelection<TState, TSelected> : IReadonlyBindablePropert
/// </summary>
public TSelected PendingValue { get; set; } = default!;
}
}
}

View File

@ -98,4 +98,4 @@ public sealed class GetPlayerProfileHandler
## 文档入口
- 运行时与整体接入说明:[CQRS 栏目](../docs/zh-CN/core/cqrs.md)
- 如果你需要默认实现而不是契约层,请看 [GFramework.Cqrs README](../GFramework.Cqrs/README.md)
- 如果你需要默认实现而不是契约层,请看 [CQRS 运行时说明](../GFramework.Cqrs/README.md)

View File

@ -63,5 +63,5 @@ RegisterCqrsHandlersFromAssembly(typeof(GameArchitecture).Assembly);
## 对应文档
- CQRS 栏目:[`../docs/zh-CN/core/cqrs.md`](../docs/zh-CN/core/cqrs.md)
- 源码生成器总览:[`../docs/zh-CN/source-generators/index.md`](../docs/zh-CN/source-generators/index.md)
- CQRS 栏目:[CQRS 文档](../docs/zh-CN/core/cqrs.md)
- 源码生成器总览:[源码生成器文档首页](../docs/zh-CN/source-generators/index.md)

View File

@ -151,4 +151,4 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu
## 文档入口
- 总体文档:[CQRS 栏目](../docs/zh-CN/core/cqrs.md)
- 契约层说明:[GFramework.Cqrs.Abstractions README](../GFramework.Cqrs.Abstractions/README.md)
- 契约层说明:[CQRS 抽象层说明](../GFramework.Cqrs.Abstractions/README.md)

View File

@ -88,7 +88,7 @@ var options = new ArchOptions
- Arch `World` 的创建、注册和查询能力
- 与 `GFramework` 架构生命周期绑定的默认模块实现
## 边界说明
## 适用边界
- 本包不提供 Arch `World` 的默认构造与注册逻辑。
- 本包不提供系统基类、扩展方法或默认服务实现。
@ -96,8 +96,8 @@ var options = new ArchOptions
## 对应文档入口
- 抽象接口总览:[`../docs/zh-CN/abstractions/index.md`](../docs/zh-CN/abstractions/index.md)
- Ecs.Arch 抽象层说明:[`../docs/zh-CN/abstractions/ecs-arch-abstractions.md`](../docs/zh-CN/abstractions/ecs-arch-abstractions.md)
- ECS 模块入口:[`../docs/zh-CN/ecs/index.md`](../docs/zh-CN/ecs/index.md)
- Arch ECS 集成:[`../docs/zh-CN/ecs/arch.md`](../docs/zh-CN/ecs/arch.md)
- 运行时实现入口:[`../GFramework.Ecs.Arch/README.md`](../GFramework.Ecs.Arch/README.md)
- 抽象接口总览:[抽象接口总览](../docs/zh-CN/abstractions/index.md)
- Ecs.Arch 抽象层说明:[ECS 抽象层说明](../docs/zh-CN/abstractions/ecs-arch-abstractions.md)
- ECS 模块入口:[ECS 模块总览](../docs/zh-CN/ecs/index.md)
- Arch ECS 集成:[Arch ECS 集成](../docs/zh-CN/ecs/arch.md)
- 运行时实现入口:[Ecs.Arch 运行时说明](../GFramework.Ecs.Arch/README.md)

View File

@ -132,7 +132,7 @@ ecsModule.Update(deltaTime);
## 对应文档入口
- ECS 总览:[`../docs/zh-CN/ecs/index.md`](../docs/zh-CN/ecs/index.md)
- Arch ECS 集成:[`../docs/zh-CN/ecs/arch.md`](../docs/zh-CN/ecs/arch.md)
- 抽象契约页:[`../docs/zh-CN/abstractions/ecs-arch-abstractions.md`](../docs/zh-CN/abstractions/ecs-arch-abstractions.md)
- 统一 API / XML 导航:[`../docs/zh-CN/api-reference/index.md`](../docs/zh-CN/api-reference/index.md)
- ECS 总览:[ECS 文档首页](../docs/zh-CN/ecs/index.md)
- Arch ECS 集成:[Arch ECS 集成说明](../docs/zh-CN/ecs/arch.md)
- 抽象契约页:[ECS Arch Abstractions 文档](../docs/zh-CN/abstractions/ecs-arch-abstractions.md)
- 统一 API / XML 导航:[API 参考首页](../docs/zh-CN/api-reference/index.md)

View File

@ -75,5 +75,5 @@ GameProject/
## 对应文档
- 配置系统:[`../docs/zh-CN/game/config-system.md`](../docs/zh-CN/game/config-system.md)
- 源码生成器总览:[`../docs/zh-CN/source-generators/index.md`](../docs/zh-CN/source-generators/index.md)
- 配置系统:[配置系统文档](../docs/zh-CN/game/config-system.md)
- 源码生成器总览:[源码生成器文档首页](../docs/zh-CN/source-generators/index.md)

View File

@ -128,7 +128,7 @@ public class ArchitectureConfigIntegrationTests
var secondArchitecture = new ModuleOnlyArchitecture(module);
var exception =
Assert.ThrowsAsync<InvalidOperationException>(async () => await secondArchitecture.InitializeAsync().ConfigureAwait(false));
Assert.ThrowsAsync<InvalidOperationException>(() => secondArchitecture.InitializeAsync());
Assert.Multiple(() =>
{

View File

@ -163,11 +163,11 @@ public class GameConfigBootstrapTests
Is.True,
"The first initialization attempt did not reach the guarded lifecycle section.");
var secondCallerException = Assert.ThrowsAsync<InvalidOperationException>(async () => await bootstrap.InitializeAsync().ConfigureAwait(false));
var secondCallerException = Assert.ThrowsAsync<InvalidOperationException>(() => bootstrap.InitializeAsync());
continueInitialization.Set();
Assert.DoesNotThrowAsync(async () => await firstInitializeTask.ConfigureAwait(false));
Assert.DoesNotThrowAsync(() => firstInitializeTask);
Assert.Multiple(() =>
{
@ -202,7 +202,7 @@ public class GameConfigBootstrapTests
})
});
var exception = Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () => await bootstrap.InitializeAsync().ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ArgumentOutOfRangeException>(() => bootstrap.InitializeAsync());
Assert.Multiple(() =>
{

View File

@ -90,7 +90,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -165,7 +165,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -223,7 +223,7 @@ public sealed class YamlConfigLoaderAllOfTests
static config => config.Id);
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -258,7 +258,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -298,7 +298,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -338,7 +338,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -378,7 +378,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -418,7 +418,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -458,7 +458,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -502,7 +502,7 @@ public sealed class YamlConfigLoaderAllOfTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{

View File

@ -75,7 +75,7 @@ public sealed class YamlConfigLoaderDependentRequiredTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -217,7 +217,7 @@ public sealed class YamlConfigLoaderDependentRequiredTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -267,7 +267,7 @@ public sealed class YamlConfigLoaderDependentRequiredTests
var loader = CreateCaseSensitiveRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -317,7 +317,7 @@ public sealed class YamlConfigLoaderDependentRequiredTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{

View File

@ -74,7 +74,7 @@ public sealed class YamlConfigLoaderDependentSchemasTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -174,7 +174,7 @@ public sealed class YamlConfigLoaderDependentSchemasTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -220,7 +220,7 @@ public sealed class YamlConfigLoaderDependentSchemasTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -264,7 +264,7 @@ public sealed class YamlConfigLoaderDependentSchemasTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{

View File

@ -127,7 +127,7 @@ public class YamlConfigLoaderEnumTests
var loader = CreateLoader<MonsterRewardConfigStub>();
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -176,7 +176,7 @@ public class YamlConfigLoaderEnumTests
var loader = CreateLoader<MonsterDropItemIdsConfigStub>();
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{

View File

@ -105,7 +105,7 @@ public sealed class YamlConfigLoaderIfThenElseTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -170,7 +170,7 @@ public sealed class YamlConfigLoaderIfThenElseTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -263,7 +263,7 @@ public sealed class YamlConfigLoaderIfThenElseTests
static config => config.Id);
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -304,7 +304,7 @@ public sealed class YamlConfigLoaderIfThenElseTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -345,7 +345,7 @@ public sealed class YamlConfigLoaderIfThenElseTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -393,7 +393,7 @@ public sealed class YamlConfigLoaderIfThenElseTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{

View File

@ -70,7 +70,7 @@ public sealed class YamlConfigLoaderNegationTests
var loader = CreateMonsterLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -172,7 +172,7 @@ public sealed class YamlConfigLoaderNegationTests
var loader = CreateMonsterRewardLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -272,7 +272,7 @@ public sealed class YamlConfigLoaderNegationTests
var loader = CreateMonsterLoader();
var registry = CreateRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{

View File

@ -143,7 +143,7 @@ public class YamlConfigLoaderTests
.RegisterTable<int, MonsterConfigStub>("monster", "monster", static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -185,7 +185,7 @@ public class YamlConfigLoaderTests
.RegisterTable<int, MonsterConfigStub>("monster", "monster", static config => config.Id)
.RegisterTable<int, MonsterConfigStub>("broken", "broken", static config => config.Id);
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -216,7 +216,7 @@ public class YamlConfigLoaderTests
.RegisterTable<int, MonsterConfigStub>("monster", "monster", static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -257,7 +257,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -306,7 +306,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -352,7 +352,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -398,7 +398,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -445,7 +445,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -494,7 +494,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -543,7 +543,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -591,7 +591,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -681,7 +681,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -769,7 +769,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -817,7 +817,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -931,7 +931,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -980,7 +980,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1028,7 +1028,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1074,7 +1074,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1174,7 +1174,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1226,7 +1226,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1280,7 +1280,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1336,7 +1336,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1394,7 +1394,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1452,7 +1452,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1561,7 +1561,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1617,7 +1617,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1635,57 +1635,7 @@ public class YamlConfigLoaderTests
[Test]
public async Task LoadAsync_Should_Accept_Object_Array_When_Contains_Matches_Declared_Subset_Properties()
{
CreateConfigFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
entries:
-
id: 1
weight: 2
-
id: 2
weight: 3
""");
CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name", "entries"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"entries": {
"type": "array",
"minContains": 1,
"contains": {
"type": "object",
"required": ["id"],
"properties": {
"id": {
"type": "integer",
"const": 1
}
}
},
"items": {
"type": "object",
"required": ["id", "weight"],
"properties": {
"id": { "type": "integer" },
"weight": { "type": "integer" }
}
}
}
}
}
""");
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<int, MonsterWeightedEntryArrayConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
var loader = CreateLoaderForContainsSubsetObjectArrayScenario();
var registry = new ConfigRegistry();
await loader.LoadAsync(registry);
@ -1740,7 +1690,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1797,7 +1747,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1904,7 +1854,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -1954,7 +1904,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2004,7 +1954,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2055,7 +2005,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2107,7 +2057,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2161,7 +2111,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2261,7 +2211,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2317,7 +2267,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2369,7 +2319,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2423,7 +2373,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2478,7 +2428,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2537,7 +2487,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2553,14 +2503,7 @@ public class YamlConfigLoaderTests
[Test]
public void LoadAsync_Should_Throw_When_Nested_Object_Array_Reference_Target_Is_Missing()
{
CreateConfigFile(
"item/potion.yaml",
"""
id: potion
name: Potion
""");
CreateConfigFile(
"monster/slime.yaml",
var loader = CreateItemBackedMonsterLoader<MonsterPhaseDropConfigStub>(
"""
id: 1
name: Slime
@ -2571,21 +2514,7 @@ public class YamlConfigLoaderTests
-
wave: 2
dropItemId: bomb
""");
CreateSchemaFile(
"schemas/item.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
}
}
""");
CreateSchemaFile(
"schemas/monster.schema.json",
""",
"""
{
"type": "object",
@ -2609,16 +2538,12 @@ public class YamlConfigLoaderTests
}
}
}
""");
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<string, ItemConfigStub>("item", "item", "schemas/item.schema.json",
static config => config.Id)
.RegisterTable<int, MonsterPhaseDropConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
""",
static config => config.Id,
("item/potion.yaml", "potion", "Potion"));
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2749,7 +2674,7 @@ public class YamlConfigLoaderTests
static config => config.Id);
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2766,41 +2691,14 @@ public class YamlConfigLoaderTests
[Test]
public void LoadAsync_Should_Throw_When_Array_Reference_Item_Is_Missing()
{
CreateConfigFile(
"item/potion.yaml",
"""
id: potion
name: Potion
""");
CreateConfigFile(
"item/slime-gel.yaml",
"""
id: slime_gel
name: Slime Gel
""");
CreateConfigFile(
"monster/slime.yaml",
var loader = CreateItemBackedMonsterLoader<MonsterDropArrayConfigStub>(
"""
id: 1
name: Slime
dropItemIds:
- potion
- missing_item
""");
CreateSchemaFile(
"schemas/item.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
}
}
""");
CreateSchemaFile(
"schemas/monster.schema.json",
""",
"""
{
"type": "object",
@ -2815,16 +2713,13 @@ public class YamlConfigLoaderTests
}
}
}
""");
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<string, ItemConfigStub>("item", "item", "schemas/item.schema.json",
static config => config.Id)
.RegisterTable<int, MonsterDropArrayConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
""",
static config => config.Id,
("item/potion.yaml", "potion", "Potion"),
("item/slime-gel.yaml", "slime_gel", "Slime Gel"));
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -2841,35 +2736,14 @@ public class YamlConfigLoaderTests
[Test]
public void LoadAsync_Should_Throw_When_Contains_Matched_Reference_Target_Is_Missing()
{
CreateConfigFile(
"item/potion.yaml",
"""
id: potion
name: Potion
""");
CreateConfigFile(
"monster/slime.yaml",
var loader = CreateItemBackedMonsterLoader<MonsterDropArrayConfigStub>(
"""
id: 1
name: Slime
dropItemIds:
- potion
- missing_item
""");
CreateSchemaFile(
"schemas/item.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
}
}
""");
CreateSchemaFile(
"schemas/monster.schema.json",
""",
"""
{
"type": "object",
@ -2890,16 +2764,12 @@ public class YamlConfigLoaderTests
}
}
}
""");
var loader = new YamlConfigLoader(_rootPath)
.RegisterTable<string, ItemConfigStub>("item", "item", "schemas/item.schema.json",
static config => config.Id)
.RegisterTable<int, MonsterDropArrayConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
""",
static config => config.Id,
("item/potion.yaml", "potion", "Potion"));
var registry = new ConfigRegistry();
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () => await loader.LoadAsync(registry));
var exception = Assert.ThrowsAsync<ConfigLoadException>(() => loader.LoadAsync(registry));
Assert.Multiple(() =>
{
@ -3353,6 +3223,111 @@ public class YamlConfigLoaderTests
}
}
/// <summary>
/// 为对象数组 <c>contains</c> 子集匹配场景创建加载器,避免测试方法体被大段固定 schema 稀释。
/// </summary>
/// <returns>已注册目标表的加载器。</returns>
private YamlConfigLoader CreateLoaderForContainsSubsetObjectArrayScenario()
{
CreateConfigFile(
"monster/slime.yaml",
"""
id: 1
name: Slime
entries:
-
id: 1
weight: 2
-
id: 2
weight: 3
""");
CreateSchemaFile(
"schemas/monster.schema.json",
"""
{
"type": "object",
"required": ["id", "name", "entries"],
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"entries": {
"type": "array",
"minContains": 1,
"contains": {
"type": "object",
"required": ["id"],
"properties": {
"id": {
"type": "integer",
"const": 1
}
}
},
"items": {
"type": "object",
"required": ["id", "weight"],
"properties": {
"id": { "type": "integer" },
"weight": { "type": "integer" }
}
}
}
}
}
""");
return new YamlConfigLoader(_rootPath)
.RegisterTable<int, MonsterWeightedEntryArrayConfigStub>("monster", "monster", "schemas/monster.schema.json",
static config => config.Id);
}
/// <summary>
/// 为跨表引用加载测试创建标准 item 表夹具,并按既有顺序注册 <c>item</c> 与 <c>monster</c>。
/// </summary>
/// <typeparam name="TMonsterConfig">monster 表的配置类型。</typeparam>
/// <param name="monsterConfigContent">monster 配置文件内容。</param>
/// <param name="monsterSchemaContent">monster schema 内容。</param>
/// <param name="keySelector">monster 表主键选择器。</param>
/// <param name="items">要写入的 item 配置文件集合。</param>
/// <returns>已完成 schema 与表注册的加载器。</returns>
private YamlConfigLoader CreateItemBackedMonsterLoader<TMonsterConfig>(
string monsterConfigContent,
string monsterSchemaContent,
Func<TMonsterConfig, int> keySelector,
params (string RelativePath, string ItemId, string Name)[] items)
{
foreach (var (relativePath, itemId, name) in items)
{
CreateConfigFile(
relativePath,
$"""
id: {itemId}
name: {name}
""");
}
CreateConfigFile("monster/slime.yaml", monsterConfigContent);
CreateSchemaFile(
"schemas/item.schema.json",
"""
{
"type": "object",
"required": ["id", "name"],
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
}
}
""");
CreateSchemaFile("schemas/monster.schema.json", monsterSchemaContent);
return new YamlConfigLoader(_rootPath)
.RegisterTable<string, ItemConfigStub>("item", "item", "schemas/item.schema.json",
static config => config.Id)
.RegisterTable<int, TMonsterConfig>("monster", "monster", "schemas/monster.schema.json", keySelector);
}
/// <summary>
/// 创建测试用配置文件。
/// </summary>
@ -3389,13 +3364,13 @@ public class YamlConfigLoaderTests
/// <returns>任务结果。</returns>
private static async Task<T> WaitForTaskWithinAsync<T>(Task<T> task, TimeSpan timeout)
{
var completedTask = await Task.WhenAny(task, Task.Delay(timeout));
var completedTask = await Task.WhenAny(task, Task.Delay(timeout)).ConfigureAwait(false);
if (!ReferenceEquals(completedTask, task))
{
Assert.Fail($"Timed out after {timeout} while waiting for file watcher notification.");
}
return await task;
return await task.ConfigureAwait(false);
}
/// <summary>

View File

@ -110,7 +110,7 @@ public sealed class YamlConfigTextValidatorTests
/// 验证异步入口与同步入口共享相同校验语义。
/// </summary>
[Test]
public async Task ValidateAsync_Should_Throw_ConfigLoadException_When_Required_Field_Is_Missing()
public void ValidateAsync_Should_Throw_ConfigLoadException_When_Required_Field_Is_Missing()
{
var schemaPath = CreateSchemaFile(
"schemas/monster.schema.json",
@ -125,14 +125,14 @@ public sealed class YamlConfigTextValidatorTests
}
""");
var exception = Assert.ThrowsAsync<ConfigLoadException>(async () =>
await YamlConfigTextValidator.ValidateAsync(
var exception = Assert.ThrowsAsync<ConfigLoadException>(() =>
YamlConfigTextValidator.ValidateAsync(
"monster",
schemaPath,
"monster/generated.yaml",
"""
id: 1
""").ConfigureAwait(false));
"""));
Assert.Multiple(() =>
{

View File

@ -43,7 +43,7 @@ public class PersistenceTests
var loaded = await storage.ReadAsync<TestSimpleData>("folder/item").ConfigureAwait(false);
Assert.That(loaded.Value, Is.EqualTo(saved.Value));
Assert.ThrowsAsync<ArgumentException>(async () => await storage.WriteAsync("../escape", new TestSimpleData()).ConfigureAwait(false));
Assert.ThrowsAsync<ArgumentException>(() => storage.WriteAsync("../escape", new TestSimpleData()));
}
/// <summary>
@ -185,7 +185,7 @@ public class PersistenceTests
var repository = new SaveRepository<TestVersionedSaveData>(storage, config)
.RegisterMigration(new TestSaveMigrationV1ToV2());
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await repository.LoadAsync(1).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<InvalidOperationException>(() => repository.LoadAsync(1));
Assert.That(exception!.Message, Does.Contain("from version 2"));
}
@ -218,7 +218,7 @@ public class PersistenceTests
var repository = new SaveRepository<TestVersionedSaveData>(storage, config)
.RegisterMigration(new TestSaveMigrationV1ToV2ReturningV3());
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await repository.LoadAsync(1).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<InvalidOperationException>(() => repository.LoadAsync(1));
var persisted = await storage.ReadAsync<TestVersionedSaveData>("saves/slot_1/save").ConfigureAwait(false);
Assert.Multiple(() =>
@ -270,7 +270,7 @@ public class PersistenceTests
repository.RegisterMigration(new TestSaveMigrationV2ToV3());
continueMigration.Set();
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () => await loadTask.ConfigureAwait(false));
var exception = Assert.ThrowsAsync<InvalidOperationException>(() => loadTask);
var persisted = await storage.ReadAsync<TestVersionedSaveData>("saves/slot_1/save").ConfigureAwait(false);
Assert.Multiple(() =>
@ -593,7 +593,7 @@ public class PersistenceTests
throwingStorage.ThrowOnWrite = true;
Assert.ThrowsAsync<InvalidOperationException>(
async () => await repository.SaveAsync(primaryLocation, new TestSimpleData { Value = 99 }).ConfigureAwait(false));
() => repository.SaveAsync(primaryLocation, new TestSimpleData { Value = 99 }));
var cachedAfterFailure = await repository.LoadAsync<TestSimpleData>(primaryLocation).ConfigureAwait(false);
Assert.That(cachedAfterFailure.Value, Is.EqualTo(1));
@ -656,7 +656,7 @@ public class PersistenceTests
throwingStorage.ThrowOnWrite = true;
Assert.ThrowsAsync<InvalidOperationException>(
async () => await repository.DeleteAsync(secondaryLocation).ConfigureAwait(false));
() => repository.DeleteAsync(secondaryLocation));
Assert.That(await repository.ExistsAsync(secondaryLocation).ConfigureAwait(false), Is.True);

View File

@ -122,7 +122,7 @@
对应文档:
- [存储系统](../docs/zh-CN/game/storage.md)
- [GFramework.Game/Storage/ReadMe.md](./Storage/ReadMe.md)
- [Storage 子模块说明](./Storage/ReadMe.md)
### `Serializer/`

View File

@ -140,7 +140,7 @@ public abstract class RouterBase<TRoute, TContext> : AbstractSystem
try
{
Log.Debug("Executing enter guard: {0} for {1}", guard.GetType().Name, routeKey);
var canEnter = await guard.CanEnterAsync(routeKey, context);
var canEnter = await guard.CanEnterAsync(routeKey, context).ConfigureAwait(false);
if (!canEnter)
{
@ -182,7 +182,7 @@ public abstract class RouterBase<TRoute, TContext> : AbstractSystem
try
{
Log.Debug("Executing leave guard: {0} for {1}", guard.GetType().Name, routeKey);
var canLeave = await guard.CanLeaveAsync(routeKey);
var canLeave = await guard.CanLeaveAsync(routeKey).ConfigureAwait(false);
if (!canLeave)
{
@ -241,4 +241,4 @@ public abstract class RouterBase<TRoute, TContext> : AbstractSystem
}
#endregion
}
}

View File

@ -89,13 +89,9 @@ public abstract class SceneRouterBase
var @event = CreateEvent(sceneKey, SceneTransitionType.Replace, param);
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await ClearInternalAsync();
await PushInternalAsync(sceneKey, param);
await AfterChangeAsync(@event);
});
await _pipeline.ExecuteAroundAsync(
@event,
() => ExecuteReplaceCoreAsync(@event, sceneKey, param)).ConfigureAwait(true);
}
finally
{
@ -195,12 +191,9 @@ public abstract class SceneRouterBase
var @event = CreateEvent(sceneKey, SceneTransitionType.Push, param);
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await PushInternalAsync(sceneKey, param);
await AfterChangeAsync(@event);
});
await _pipeline.ExecuteAroundAsync(
@event,
() => ExecutePushCoreAsync(@event, sceneKey, param)).ConfigureAwait(true);
}
finally
{
@ -276,12 +269,9 @@ public abstract class SceneRouterBase
var @event = CreateEvent(null, SceneTransitionType.Pop);
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await PopInternalAsync();
await AfterChangeAsync(@event);
});
await _pipeline.ExecuteAroundAsync(
@event,
() => ExecutePopCoreAsync(@event)).ConfigureAwait(true);
}
finally
{
@ -347,12 +337,9 @@ public abstract class SceneRouterBase
var @event = CreateEvent(null, SceneTransitionType.Clear);
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await ClearInternalAsync();
await AfterChangeAsync(@event);
});
await _pipeline.ExecuteAroundAsync(
@event,
() => ExecuteClearCoreAsync(@event)).ConfigureAwait(true);
}
finally
{
@ -370,7 +357,7 @@ public abstract class SceneRouterBase
{
while (Stack.Count > 0)
{
await PopInternalAsync();
await PopInternalAsync().ConfigureAwait(true);
}
}
@ -378,6 +365,67 @@ public abstract class SceneRouterBase
#region Helper Methods
// Scene 生命周期回调和 pipeline handlers 可能依赖引擎线程,因此这些核心切换顺序统一显式保留上下文。
/// <summary>
/// 执行 Replace 的核心切换顺序。
/// </summary>
/// <param name="event">场景转换事件。</param>
/// <param name="sceneKey">目标场景键名。</param>
/// <param name="param">场景进入参数。</param>
/// <returns>异步任务。</returns>
private async Task ExecuteReplaceCoreAsync(
SceneTransitionEvent @event,
string sceneKey,
ISceneEnterParam? param)
{
await BeforeChangeAsync(@event).ConfigureAwait(true);
await ClearInternalAsync().ConfigureAwait(true);
await PushInternalAsync(sceneKey, param).ConfigureAwait(true);
await AfterChangeAsync(@event).ConfigureAwait(true);
}
/// <summary>
/// 执行 Push 的核心切换顺序。
/// </summary>
/// <param name="event">场景转换事件。</param>
/// <param name="sceneKey">目标场景键名。</param>
/// <param name="param">场景进入参数。</param>
/// <returns>异步任务。</returns>
private async Task ExecutePushCoreAsync(
SceneTransitionEvent @event,
string sceneKey,
ISceneEnterParam? param)
{
await BeforeChangeAsync(@event).ConfigureAwait(true);
await PushInternalAsync(sceneKey, param).ConfigureAwait(true);
await AfterChangeAsync(@event).ConfigureAwait(true);
}
/// <summary>
/// 执行 Pop 的核心切换顺序。
/// </summary>
/// <param name="event">场景转换事件。</param>
/// <returns>异步任务。</returns>
private async Task ExecutePopCoreAsync(SceneTransitionEvent @event)
{
await BeforeChangeAsync(@event).ConfigureAwait(true);
await PopInternalAsync().ConfigureAwait(true);
await AfterChangeAsync(@event).ConfigureAwait(true);
}
/// <summary>
/// 执行 Clear 的核心切换顺序。
/// </summary>
/// <param name="event">场景转换事件。</param>
/// <returns>异步任务。</returns>
private async Task ExecuteClearCoreAsync(SceneTransitionEvent @event)
{
await BeforeChangeAsync(@event).ConfigureAwait(true);
await ClearInternalAsync().ConfigureAwait(true);
await AfterChangeAsync(@event).ConfigureAwait(true);
}
/// <summary>
/// 创建场景转换事件对象。
/// </summary>
@ -407,7 +455,7 @@ public abstract class SceneRouterBase
private async Task BeforeChangeAsync(SceneTransitionEvent @event)
{
Log.Debug("BeforeChange phases started: {0}", @event.TransitionType);
await _pipeline.ExecuteAsync(@event, SceneTransitionPhases.BeforeChange);
await _pipeline.ExecuteAsync(@event, SceneTransitionPhases.BeforeChange).ConfigureAwait(true);
Log.Debug("BeforeChange phases completed: {0}", @event.TransitionType);
}
@ -418,9 +466,9 @@ public abstract class SceneRouterBase
private async Task AfterChangeAsync(SceneTransitionEvent @event)
{
Log.Debug("AfterChange phases started: {0}", @event.TransitionType);
await _pipeline.ExecuteAsync(@event, SceneTransitionPhases.AfterChange);
await _pipeline.ExecuteAsync(@event, SceneTransitionPhases.AfterChange).ConfigureAwait(true);
Log.Debug("AfterChange phases completed: {0}", @event.TransitionType);
}
#endregion
}
}

View File

@ -148,7 +148,7 @@ public class SceneTransitionPipeline
foreach (var handler in sortedHandlers)
{
var options = _options[handler];
await ExecuteSingleHandlerAsync(handler, options, @event, cancellationToken);
await ExecuteSingleHandlerAsync(handler, options, @event, cancellationToken).ConfigureAwait(false);
}
Log.Debug("Pipeline execution completed for phases: {0}", phases);
@ -173,7 +173,7 @@ public class SceneTransitionPipeline
if (handlers.Count == 0)
{
await coreAction();
await coreAction().ConfigureAwait(false);
return;
}
@ -191,11 +191,11 @@ public class SceneTransitionPipeline
var options = _aroundOptions[handler];
var next = pipeline;
pipeline = async () => await ExecuteSingleAroundHandlerAsync(
handler, options, @event, next, cancellationToken);
pipeline = () => ExecuteSingleAroundHandlerAsync(
handler, options, @event, next, cancellationToken);
}
await pipeline();
await pipeline().ConfigureAwait(false);
}
private List<ISceneTransitionHandler> FilterAndSortHandlers(
@ -283,7 +283,7 @@ public class SceneTransitionPipeline
? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token)
: null;
await handler.HandleAsync(@event, next, linkedCts?.Token ?? cancellationToken);
await handler.HandleAsync(@event, next, linkedCts?.Token ?? cancellationToken).ConfigureAwait(false);
Log.Debug("Around handler completed: {0}", handler.GetType().Name);
}
@ -296,4 +296,4 @@ public class SceneTransitionPipeline
throw;
}
}
}
}

View File

@ -19,7 +19,7 @@ public class SettingsSystem : AbstractSystem, ISettingsSystem
public async Task ApplyAll()
{
// 遍历所有设置应用器并尝试应用
foreach (var applicator in _model.AllApplicators()) await TryApplyAsync(applicator);
foreach (var applicator in _model.AllApplicators()) await TryApplyAsync(applicator).ConfigureAwait(false);
}
/// <summary>
@ -41,7 +41,7 @@ public class SettingsSystem : AbstractSystem, ISettingsSystem
/// <returns>完成的任务</returns>
public async Task SaveAll()
{
await _model.SaveAllAsync();
await _model.SaveAllAsync().ConfigureAwait(false);
}
/// <summary>
@ -51,7 +51,7 @@ public class SettingsSystem : AbstractSystem, ISettingsSystem
public async Task ResetAll()
{
_model.ResetAll();
await ApplyAll();
await ApplyAll().ConfigureAwait(false);
}
/// <summary>
@ -62,7 +62,7 @@ public class SettingsSystem : AbstractSystem, ISettingsSystem
public async Task Reset<T>() where T : class, ISettingsData, IResetApplyAbleSettings, new()
{
_model.Reset<T>();
await Apply<T>();
await Apply<T>().ConfigureAwait(false);
}
@ -87,7 +87,7 @@ public class SettingsSystem : AbstractSystem, ISettingsSystem
try
{
await applyAbleSettings.ApplyAsync();
await applyAbleSettings.ApplyAsync().ConfigureAwait(false);
// 发送设置应用成功事件
this.SendEvent(new SettingsAppliedEvent<ISettingsSection>(section, true));
}

View File

@ -143,11 +143,11 @@ public sealed class FileStorage : IFileStorage, IDisposable
ObjectDisposedException.ThrowIf(_disposed, this);
var path = ToPath(key);
await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false))
{
if (File.Exists(path))
File.Delete(path);
}
var pathLock = await _lockManager.AcquireLockAsync(path).ConfigureAwait(false);
await using var configuredPathLock = pathLock.ConfigureAwait(false);
if (File.Exists(path))
File.Delete(path);
}
#endregion
@ -178,10 +178,10 @@ public sealed class FileStorage : IFileStorage, IDisposable
ObjectDisposedException.ThrowIf(_disposed, this);
var path = ToPath(key);
await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false))
{
return File.Exists(path);
}
var pathLock = await _lockManager.AcquireLockAsync(path).ConfigureAwait(false);
await using var configuredPathLock = pathLock.ConfigureAwait(false);
return File.Exists(path);
}
#endregion
@ -236,23 +236,24 @@ public sealed class FileStorage : IFileStorage, IDisposable
ObjectDisposedException.ThrowIf(_disposed, this);
var path = ToPath(key);
await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false))
{
if (!File.Exists(path))
throw new FileNotFoundException($"Storage key not found: {key}", path);
var pathLock = await _lockManager.AcquireLockAsync(path).ConfigureAwait(false);
await using var configuredPathLock = pathLock.ConfigureAwait(false);
await using var fs = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.Read,
_bufferSize,
useAsync: true);
if (!File.Exists(path))
throw new FileNotFoundException($"Storage key not found: {key}", path);
using var sr = new StreamReader(fs, Encoding.UTF8);
var content = await sr.ReadToEndAsync().ConfigureAwait(false);
return _serializer.Deserialize<T>(content);
}
var fs = new FileStream(
path,
FileMode.Open,
FileAccess.Read,
FileShare.Read,
_bufferSize,
useAsync: true);
await using var configuredFileStream = fs.ConfigureAwait(false);
using var sr = new StreamReader(fs, Encoding.UTF8, true, -1, leaveOpen: true);
var content = await sr.ReadToEndAsync().ConfigureAwait(false);
return _serializer.Deserialize<T>(content);
}
#endregion
@ -354,38 +355,42 @@ public sealed class FileStorage : IFileStorage, IDisposable
var path = ToPath(key);
var tempPath = path + ".tmp";
await using (await _lockManager.AcquireLockAsync(path).ConfigureAwait(false))
var pathLock = await _lockManager.AcquireLockAsync(path).ConfigureAwait(false);
await using var configuredPathLock = pathLock.ConfigureAwait(false);
try
{
try
{
var content = _serializer.Serialize(value);
var content = _serializer.Serialize(value);
// 先写入临时文件
await using (var fs = new FileStream(
tempPath,
FileMode.Create,
FileAccess.Write,
FileShare.None,
_bufferSize,
useAsync: true))
{
await using var sw = new StreamWriter(fs, Encoding.UTF8);
await sw.WriteAsync(content).ConfigureAwait(false);
await sw.FlushAsync().ConfigureAwait(false);
}
// 原子性替换目标文件
File.Move(tempPath, path, overwrite: true);
}
catch
// 先写入临时文件
{
// 清理临时文件
if (File.Exists(tempPath))
File.Delete(tempPath);
throw;
var fs = new FileStream(
tempPath,
FileMode.Create,
FileAccess.Write,
FileShare.None,
_bufferSize,
useAsync: true);
await using var configuredFileStream = fs.ConfigureAwait(false);
var sw = new StreamWriter(fs, Encoding.UTF8, leaveOpen: true);
await using var configuredStreamWriter = sw.ConfigureAwait(false);
await sw.WriteAsync(content).ConfigureAwait(false);
await sw.FlushAsync().ConfigureAwait(false);
}
// 原子性替换目标文件
File.Move(tempPath, path, overwrite: true);
}
catch
{
// 清理临时文件
if (File.Exists(tempPath))
File.Delete(tempPath);
throw;
}
}
#endregion
}
}

View File

@ -100,9 +100,9 @@ public sealed class ScopedStorage(IStorage inner, string prefix) : IScopedStorag
/// </summary>
/// <param name="key">要删除的键</param>
/// <returns>异步操作任务</returns>
public async Task DeleteAsync(string key)
public Task DeleteAsync(string key)
{
await inner.DeleteAsync(Key(key));
return inner.DeleteAsync(Key(key));
}
/// <summary>
@ -166,4 +166,4 @@ public sealed class ScopedStorage(IStorage inner, string prefix) : IScopedStorag
{
return new ScopedStorage(inner, Key(scope));
}
}
}

View File

@ -101,10 +101,10 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await DoPushPageInternalAsync(uiKey, param, policy);
await AfterChangeAsync(@event);
});
await BeforeChangeAsync(@event).ConfigureAwait(true);
await DoPushPageInternalAsync(uiKey, param, policy).ConfigureAwait(true);
await AfterChangeAsync(@event).ConfigureAwait(true);
}).ConfigureAwait(true);
}
/// <summary>
@ -129,10 +129,10 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await BeforeChangeAsync(@event).ConfigureAwait(true);
DoPushPageInternal(page, param, policy);
await AfterChangeAsync(@event);
});
await AfterChangeAsync(@event).ConfigureAwait(true);
}).ConfigureAwait(true);
}
/// <summary>
@ -149,7 +149,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
var leavingUiKey = Stack.Peek().Key;
if (!await ExecuteLeaveGuardsAsync(leavingUiKey))
if (!await ExecuteLeaveGuardsAsync(leavingUiKey).ConfigureAwait(true))
{
Log.Warn("Pop blocked by guard: {0}", leavingUiKey);
return;
@ -160,10 +160,10 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await BeforeChangeAsync(@event).ConfigureAwait(true);
DoPopInternal(policy);
await AfterChangeAsync(@event);
});
await AfterChangeAsync(@event).ConfigureAwait(true);
}).ConfigureAwait(true);
}
/// <summary>
@ -182,15 +182,15 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await BeforeChangeAsync(@event).ConfigureAwait(true);
DoClearInternal(popPolicy);
var page = _factory.Create(uiKey);
Log.Debug("Get/Create UI Page instance for Replace: {0}", page.GetType().Name);
DoPushPageInternal(page, param, pushPolicy);
await AfterChangeAsync(@event);
});
await AfterChangeAsync(@event).ConfigureAwait(true);
}).ConfigureAwait(true);
}
/// <summary>
@ -211,12 +211,12 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await BeforeChangeAsync(@event).ConfigureAwait(true);
DoClearInternal(popPolicy);
Log.Debug("Use existing UI Page instance for Replace: {0}", page.GetType().Name);
DoPushPageInternal(page, param, pushPolicy);
await AfterChangeAsync(@event);
});
await AfterChangeAsync(@event).ConfigureAwait(true);
}).ConfigureAwait(true);
}
/// <summary>
@ -229,10 +229,10 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
await _pipeline.ExecuteAroundAsync(@event, async () =>
{
await BeforeChangeAsync(@event);
await BeforeChangeAsync(@event).ConfigureAwait(true);
DoClearInternal(UiPopPolicy.Destroy);
await AfterChangeAsync(@event);
});
await AfterChangeAsync(@event).ConfigureAwait(true);
}).ConfigureAwait(true);
}
/// <summary>
@ -650,7 +650,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
private async Task BeforeChangeAsync(UiTransitionEvent @event)
{
Log.Debug("BeforeChange phases started: {0}", @event.TransitionType);
await _pipeline.ExecuteAsync(@event, UiTransitionPhases.BeforeChange);
await _pipeline.ExecuteAsync(@event, UiTransitionPhases.BeforeChange).ConfigureAwait(true);
Log.Debug("BeforeChange phases completed: {0}", @event.TransitionType);
}
@ -661,7 +661,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
private async Task AfterChangeAsync(UiTransitionEvent @event)
{
Log.Debug("AfterChange phases started: {0}", @event.TransitionType);
await _pipeline.ExecuteAsync(@event, UiTransitionPhases.AfterChange);
await _pipeline.ExecuteAsync(@event, UiTransitionPhases.AfterChange).ConfigureAwait(true);
Log.Debug("AfterChange phases completed: {0}", @event.TransitionType);
}
@ -673,7 +673,7 @@ public abstract class UiRouterBase : RouterBase<IUiPageBehavior, IUiPageEnterPar
/// <param name="policy">过渡策略</param>
private async Task DoPushPageInternalAsync(string uiKey, IUiPageEnterParam? param, UiTransitionPolicy policy)
{
if (!await ExecuteEnterGuardsAsync(uiKey, param))
if (!await ExecuteEnterGuardsAsync(uiKey, param).ConfigureAwait(true))
{
Log.Warn("Push blocked by guard: {0}", uiKey);
return;

View File

@ -133,7 +133,7 @@ public class UiTransitionPipeline
foreach (var handler in sortedHandlers)
{
var options = _options[handler];
await ExecuteSingleHandlerAsync(handler, options, @event, cancellationToken);
await ExecuteSingleHandlerAsync(handler, options, @event, cancellationToken).ConfigureAwait(false);
}
Log.Debug("Pipeline execution completed for phases: {0}", phases);
@ -158,7 +158,7 @@ public class UiTransitionPipeline
if (handlers.Count == 0)
{
await coreAction();
await coreAction().ConfigureAwait(false);
return;
}
@ -176,11 +176,11 @@ public class UiTransitionPipeline
var options = _aroundOptions[handler];
var next = pipeline;
pipeline = async () => await ExecuteSingleAroundHandlerAsync(
pipeline = () => ExecuteSingleAroundHandlerAsync(
handler, options, @event, next, cancellationToken);
}
await pipeline();
await pipeline().ConfigureAwait(false);
}
private List<IUiTransitionHandler> FilterAndSortHandlers(
@ -268,7 +268,7 @@ public class UiTransitionPipeline
? CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token)
: null;
await handler.HandleAsync(@event, next, linkedCts?.Token ?? cancellationToken);
await handler.HandleAsync(@event, next, linkedCts?.Token ?? cancellationToken).ConfigureAwait(false);
Log.Debug("Around handler completed: {0}", handler.GetType().Name);
}
@ -281,4 +281,4 @@ public class UiTransitionPipeline
throw;
}
}
}
}

View File

@ -190,7 +190,7 @@ public partial class MainMenu : Control
- `AutoScene`[AutoScene 生成器](../docs/zh-CN/source-generators/auto-scene-generator.md)
- `AutoUiPage`[AutoUiPage 生成器](../docs/zh-CN/source-generators/auto-ui-page-generator.md)
- `AutoRegisterExportedCollections`[AutoRegisterExportedCollections 生成器](../docs/zh-CN/source-generators/auto-register-exported-collections-generator.md)
- Godot 运行时入口:[../GFramework.Godot/README.md](../GFramework.Godot/README.md)
- Godot 运行时入口:[Godot 运行时说明](../GFramework.Godot/README.md)
- 集成教程:[Godot 集成教程](../docs/zh-CN/tutorials/godot-integration.md)
## 什么时候不该先看这个包

View File

@ -19,8 +19,8 @@ public sealed class AbstractArchitectureModuleInstallationTests
var architecture = new TestArchitecture();
var module = new RecordingGodotModule();
var exception = Assert.ThrowsAsync<InvalidOperationException>(async () =>
await architecture.InstallGodotModuleForTestAsync(module).ConfigureAwait(false));
var exception = Assert.ThrowsAsync<InvalidOperationException>(() =>
architecture.InstallGodotModuleForTestAsync(module));
Assert.Multiple(() =>
{

View File

@ -147,7 +147,7 @@ Godot 上。
- 信号系统:[Godot 信号系统](../docs/zh-CN/godot/signal.md)
- 日志系统:[Godot 日志系统](../docs/zh-CN/godot/logging.md)
- 集成教程:[Godot 集成教程](../docs/zh-CN/tutorials/godot-integration.md)
- 生成器入口:[../GFramework.Godot.SourceGenerators/README.md](../GFramework.Godot.SourceGenerators/README.md)
- 生成器入口:[Godot 源码生成器说明](../GFramework.Godot.SourceGenerators/README.md)
## 什么时候不该把它当成主入口

View File

@ -13,25 +13,25 @@
- 第一次接触框架:[入门指南](docs/zh-CN/getting-started/index.md)
- 想先跑一个最小例子:[快速开始](docs/zh-CN/getting-started/quick-start.md)
- 已经知道要用哪个模块:直接进入对应模块目录下的 `README.md`
- 已经知道要用哪个模块:直接进入对应模块的说明页
## 模块地图
| 模块 | 作用 | 入口 |
| --- | --- | --- |
| `GFramework.Core` | 架构、命令、查询、事件、状态、日志、资源、协程等基础运行时 | [README](GFramework.Core/README.md) |
| `GFramework.Core.Abstractions` | `Core` 对应的契约层,适合面向接口开发或做模块拆分 | [README](GFramework.Core.Abstractions/README.md) |
| `GFramework.Cqrs` | 新版 CQRS runtime提供 request dispatcher、notification publish 与 handler 注册 | [README](GFramework.Cqrs/README.md) |
| `GFramework.Cqrs.Abstractions` | CQRS 消息、处理器、pipeline 行为等契约 | [README](GFramework.Cqrs.Abstractions/README.md) |
| `GFramework.Game` | 面向游戏项目的配置、数据、路由、场景、UI、设置和存储运行时 | [README](GFramework.Game/README.md) |
| `GFramework.Game.Abstractions` | `Game` 对应的契约层 | [README](GFramework.Game.Abstractions/README.md) |
| `GFramework.Godot` | Godot 集成层负责把框架能力接入节点、场景、UI、设置与存储 | [README](GFramework.Godot/README.md) |
| `GFramework.Ecs.Arch` | Arch ECS 集成 | [README](GFramework.Ecs.Arch/README.md) |
| `GFramework.Ecs.Arch.Abstractions` | Arch ECS 集成对应的契约层,适合共享宿主循环与 ECS 模块边界 | [README](GFramework.Ecs.Arch.Abstractions/README.md) |
| `GFramework.Core.SourceGenerators` | Core 侧通用源码生成器与分析器 | [README](GFramework.Core.SourceGenerators/README.md) |
| `GFramework.Game.SourceGenerators` | 游戏内容配置 schema 生成器 | [README](GFramework.Game.SourceGenerators/README.md) |
| `GFramework.Cqrs.SourceGenerators` | CQRS handler registry 生成器 | [README](GFramework.Cqrs.SourceGenerators/README.md) |
| `GFramework.Godot.SourceGenerators` | Godot 项目元数据、节点注入、信号绑定与 Scene/UI 辅助生成器 | [README](GFramework.Godot.SourceGenerators/README.md) |
| `GFramework.Core` | 架构、命令、查询、事件、状态、日志、资源、协程等基础运行时 | [模块说明](GFramework.Core/README.md) |
| `GFramework.Core.Abstractions` | `Core` 对应的契约层,适合面向接口开发或做模块拆分 | [模块说明](GFramework.Core.Abstractions/README.md) |
| `GFramework.Cqrs` | 新版 CQRS runtime提供 request dispatcher、notification publish 与 handler 注册 | [模块说明](GFramework.Cqrs/README.md) |
| `GFramework.Cqrs.Abstractions` | CQRS 消息、处理器、pipeline 行为等契约 | [模块说明](GFramework.Cqrs.Abstractions/README.md) |
| `GFramework.Game` | 面向游戏项目的配置、数据、路由、场景、UI、设置和存储运行时 | [模块说明](GFramework.Game/README.md) |
| `GFramework.Game.Abstractions` | `Game` 对应的契约层 | [模块说明](GFramework.Game.Abstractions/README.md) |
| `GFramework.Godot` | Godot 集成层负责把框架能力接入节点、场景、UI、设置与存储 | [模块说明](GFramework.Godot/README.md) |
| `GFramework.Ecs.Arch` | Arch ECS 集成 | [模块说明](GFramework.Ecs.Arch/README.md) |
| `GFramework.Ecs.Arch.Abstractions` | Arch ECS 集成对应的契约层,适合共享宿主循环与 ECS 模块边界 | [模块说明](GFramework.Ecs.Arch.Abstractions/README.md) |
| `GFramework.Core.SourceGenerators` | Core 侧通用源码生成器与分析器 | [模块说明](GFramework.Core.SourceGenerators/README.md) |
| `GFramework.Game.SourceGenerators` | 游戏内容配置 schema 生成器 | [模块说明](GFramework.Game.SourceGenerators/README.md) |
| `GFramework.Cqrs.SourceGenerators` | CQRS handler registry 生成器 | [模块说明](GFramework.Cqrs.SourceGenerators/README.md) |
| `GFramework.Godot.SourceGenerators` | Godot 项目元数据、节点注入、信号绑定与 Scene/UI 辅助生成器 | [模块说明](GFramework.Godot.SourceGenerators/README.md) |
## 内部支撑模块
@ -145,7 +145,7 @@ GFramework.sln
提交功能或行为变更时,请把代码、测试和文档一起更新:
1. 先阅读对应模块目录下的 `README.md`
1. 先阅读对应模块的说明页
2. 如果改动影响采用路径、安装方式、公共 API 或目录结构,同时更新受影响的中文文档页面,必要时同步调整 [中文文档入口](docs/zh-CN/index.md)
3. 对跨模块或多阶段任务,维护 `ai-plan/public/README.md` 与对应主题目录下的 tracking / trace

View File

@ -6,41 +6,85 @@
## 当前恢复点
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-058`
- 当前阶段:`Phase 58`
- 恢复点编号:`ANALYZER-WARNING-REDUCTION-RP-064`
- 当前阶段:`Phase 64`
- 当前焦点:
- `2026-04-24` 使用 `$gframework-pr-review` 复核当前分支 PR #286 的 latest-head review threads、MegaLinter 与测试状态
- 已确认最新 head 上唯一未解决的实质代码线程指向 `GFramework.Godot/Scene/SceneBehaviorBase.cs``OnPauseAsync` 的缩进异常,并顺带对齐 `OnResumeAsync``OnUnloadAsync`
- `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release` 通过,结果为 `565 Warning(s)``0 Error(s)`;当前跟进只处理 PR review 指向的格式问题,不扩散到既有 warning 基线
- `dotnet format GFramework.Godot/GFramework.Godot.csproj --verify-no-changes --no-restore --include GFramework.Godot/Scene/SceneBehaviorBase.cs` 已通过,当前文件不再残留格式差异
- `2026-04-25` 当前 turn 先执行 `$gframework-pr-review`,复核 PR #288 的 latest-head unresolved 线程与折叠评论
- 已收敛一批经本地复核后仍成立的 review 建议,包括 `ThrowIfNull` 回退、测试桩 XML 注释修正、`FileStorage` 资源所有权、`SceneRouterBase` 线程亲和语义与若干测试噪音
- 已确认用户在 WSL 下直接执行的标准 `dotnet build -c Release` 路径可用;前一轮失败主要来自主线程附加的 workaround 参数而非仓库本身不可构建
- 基线 `origin/main` 仍为 `9964962``2026-04-24T23:05:53+08:00`
- 当前累计 branch diff 相对 `origin/main``75` 个文件、`2098` 行,已触达本轮 `75 files` 阈值
- `RP-061` 之后已接受 2 个批次提交:`03c73a8``9ce1fa6`
- 当前默认恢复入口不再继续扩写集;若要继续 analyzer reduction优先重新抓取 PR #288 的 unresolved 线程并按最新 head 再做一轮收口
## 当前活跃事实
- 之前记录的 plain `dotnet build` `0 Warning(s)` 属于增量构建假阴性,不能再作为 warning 检查真值
- 仓库根目录 `dotnet clean GFramework.sln -c Release` 仍在 `ValidateSolutionConfiguration` 阶段失败,项目级 `dotnet clean GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release` 也未能稳定提供 clean 基线
- 当前整仓最近一次直接观测值仍是 `dotnet build GFramework.sln -c Release``116 warning(s)`
- `RP-056` 已验证 `GeneratedConfigConsumerIntegrationTests.cs` 不再出现在项目 build warning 输出中
- `RP-057` 已验证 `PersistenceTests.cs` 不再出现在 `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --no-incremental` 的 warning 输出中
- 本轮已验证 `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~UnifiedSettingsDataRepository_SaveAsync_When_Persist_Fails_Should_Keep_Cache_Consistent|FullyQualifiedName~UnifiedSettingsDataRepository_DeleteAsync_When_Persist_Fails_Should_Keep_Cache_Consistent"`,结果为 `Passed: 2`
- `GFramework.Game.Tests` 当前剩余热点已经几乎完全集中到 `YamlConfigLoaderTests.cs` 这一高上下文文件
- PR #286 当前标题为 `Fix/analyzer warning reduction batch`;最新抓取时间点的 PR 状态仍为 `OPEN`
- 最新 reviewed commit 为 `2b707343577193fc9904517e6078149653e95698`CodeRabbit 于 `2026-04-24T12:44:12Z` 给出 `CHANGES_REQUESTED`
- latest-head review threads 中只有 `1` 个未解决线程,内容是 `SceneBehaviorBase.OnPauseAsync` 的缩进不一致;本地源码已修复并扩展到同段的 `OnResumeAsync` / `OnUnloadAsync`
- MegaLinter 的 `dotnet-format` 详细问题与上述格式异常一致;本地 `dotnet format --verify-no-changes` 已通过
- PR 上其余 nitpick 仅为可选建议或已明确留待后续批次处理,当前没有额外需要立即修复的 latest-head 代码线程
- 当前 `origin/main` 基线提交为 `9964962``2026-04-24T23:05:53+08:00`)。
- 本轮 `Core.Tests` 低风险机械型清理已落地到:
- `ArchitectureAdditionalCqrsHandlersTests.cs`
- `RegistryInitializationHookBaseTests.cs`
- `CommandCoroutineExtensionsTests.cs`
- `TaskCoroutineExtensionsTests.cs`
- `WaitForTaskTTests.cs`
- `AsyncExtensionsTests.cs`
- `LogContextTests.cs`
- `PauseStackManagerTests.cs`
- 本 turn 结合 PR #288 latest-head review 额外收敛了以下仍然成立的问题:
- `AsyncExtensionsTests.cs`:修复 `WithTimeoutAsync` 无返回值测试中错误返回 `ConfiguredTaskAwaitable` 导致的 `CS0029` / `CS1662`
- `ContextAwareCommandExtensions.cs`
- `ContextAwareQueryExtensions.cs`
- `ContextAwareEventExtensions.cs`
- `AsyncExtensions.cs`
- `AsyncKeyLockManagerTests.cs`:去掉两处不会产生额外价值的 `Assert.DoesNotThrowAsync(() => Task.WhenAll(...))` 包装,并把取消断言改为直接消费 `ValueTask.AsTask()`
- `AsyncArchitectureTests.cs`
- `ArchitectureLifecycleBehaviorTests.cs`
- `StateMachineSystemTests.cs`
- `RegistryInitializationHookBaseTests.cs`
- `NumericExtensions.cs`
- `StringExtensions.cs`
- `StoreBuilder.cs`
- `StoreSelection.cs`
- `ArchitectureServicesTests.cs`
- `GameContextTests.cs`
- `RollingFileAppenderTests.cs`
- `TaskCoroutineExtensionsTests.cs`
- `WaitForTaskTests.cs`
- `ScopedStorage.cs`
- `FileStorage.cs`
- `SceneRouterBase.cs`
- 当前 PR review 观察:
- PR`#288`
- latest reviewed commit`70c42b579f70c90ab5461a02e611c0fbd8d8a6f2`
- 抓取时 `coderabbitai[bot]``6` 个 open threads`greptile-apps[bot]``2` 个 open threads
- `Actionable comments posted: 7``outside diff + nitpick = 19` 并不等于必须全收;本 turn 仅接受经本地复核后仍成立且不与仓库约束冲突的建议
- 本轮 `Core` runtime 低风险机械型清理已落地到:
- `AsyncExtensions.cs`
- `CollectionExtensions.cs`
- `ContextAwareCommandExtensions.cs`
- `ContextAwareEnvironmentExtensions.cs`
- `ContextAwareEventExtensions.cs`
- `ContextAwareQueryExtensions.cs`
- `ContextAwareServiceExtensions.cs`
- `GuardExtensions.cs`
- `NumericExtensions.cs`
- `StoreEventBusExtensions.cs`
- `StringExtensions.cs`
- `StoreBuilder.cs`
- `StoreSelection.cs`
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal` 当前结果为 `0 Warning(s)``0 Error(s)`,可作为本轮 runtime 变更的最终最小 Release build 验证。
- `GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-incremental``03c73a8` 提交前的最近一次可信主线程结果为 `198 Warning(s)``0 Error(s)`;该观测值覆盖了 `ArchitectureContextTests``ArchitectureServicesTests``GameContextTests``ResultTests``AsyncTestModel``AsyncTestSystem``ContextAwareEnvironmentExtensionsTests` 的 7 文件批次。
- 当前累计 branch diff 相对 `origin/main``75` 个文件、`2098` 行;本轮主停止条件已经达到。
## 当前风险
- 如果后续继续依赖增量 `dotnet build`,容易再次把 warning 数量误判为 0
- 缓解措施:每轮 warning 检查前先执行 `dotnet clean`,再执行目标 `dotnet build`
- 仓库根目录与 `GFramework.Game.Tests``dotnet clean` 目前都无法给出新的 clean 基线
- 缓解措施:后续若继续整仓 warning reduction需要单独定位 clean 失败原因,或明确继续沿用 direct build 观测值作为临时真值
- 当前 worktree 仍存在未跟踪的 `.codex` 目录
- 缓解措施:提交当前批次时只暂存 analyzer-warning-reduction 相关源码与 `ai-plan` 文件,避免把工作目录辅助文件混入提交
- 下一轮若继续深入 `GFramework.Game.Tests`,很可能需要进入 `YamlConfigLoaderTests.cs` 这种高上下文大文件
- 缓解措施:把它单独作为一个明确的新批次处理,不与其它 warning family 混批
- PR 标题检查当前仍显示 `Inconclusive`
- 缓解措施:如需让该检查转绿,需要单独更新 GitHub PR 标题;这不属于本地代码修改范围
- `dotnet clean GFramework.sln -c Release``dotnet clean GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release` 仍无法稳定提供新的 clean 基线。
- 缓解措施:后续若继续整仓 warning reduction需要单独定位 clean 失败原因,或明确继续沿用 direct build 观测值作为临时真值。
- 当前 worktree 仍存在未跟踪的 `.codex` 目录。
- 缓解措施:提交当前批次时只暂存 analyzer-warning-reduction 相关源码与 `ai-plan` 文件,避免把工作目录辅助文件混入提交。
- 将分支继续推过 `75 files` 会明显降低本轮 reviewability。
- 缓解措施:当前恢复点默认停止;如需继续,建议在新 turn 明确新的文件阈值或先 rebase / refresh baseline。
- `GFramework.Core``GFramework.Game``GFramework.Core.Tests` 当前都仍存在模块级历史 warning 基线。
- 缓解措施:本 turn 已确保本次 touched files 不再引入新的编译错误,并消化了当前 PR review 中仍成立的高信号问题;若要继续 warning reduction应开新批次按模块系统化收敛。
## 活跃文档
@ -56,31 +100,35 @@
## 验证说明
- `dotnet clean GFramework.sln -c Release`
- 结果:失败;停在 solution `ValidateSolutionConfiguration``0 Warning(s)``0 Error(s)`,未输出更具体的 error 文本
- `dotnet build GFramework.sln -c Release`
- 结果:成功;`116 Warning(s)``0 Error(s)`
- `dotnet clean GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release`
- 结果失败clean 阶段在 MSBuild 清理路径结束前返回 `0 Warning(s)``0 Error(s)`,未输出额外错误文本
- `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release`
- `RP-055` 收尾结果:成功;`63 Warning(s)``0 Error(s)`
- `RP-056` 当前结果:成功;`59 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --no-incremental`
- `RP-057` 热点重排前:成功;`253 Warning(s)``0 Error(s)`
- `RP-057` 当前结果:成功;`249 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureConfigIntegrationTests|FullyQualifiedName~GameConfigBootstrapTests|FullyQualifiedName~JsonSerializerTests"`
- 结果:成功;`Passed: 19``Failed: 0`
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~GeneratedConfigConsumerIntegrationTests"`
- 结果:成功;`Passed: 4``Failed: 0`
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~UnifiedSettingsDataRepository_SaveAsync_When_Persist_Fails_Should_Keep_Cache_Consistent|FullyQualifiedName~UnifiedSettingsDataRepository_DeleteAsync_When_Persist_Fails_Should_Keep_Cache_Consistent"`
- 结果:成功;`Passed: 2``Failed: 0`
- `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release`
- 结果:成功;`565 Warning(s)``0 Error(s)`
- `dotnet format GFramework.Godot/GFramework.Godot.csproj --verify-no-changes --no-restore --include GFramework.Godot/Scene/SceneBehaviorBase.cs`
- 首次运行失败restore 阶段异常退出,未进入格式验证
- 第二次运行(同命令追加 sandbox 提权成功workspace 仅提示加载 warning无格式差异
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 历史结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-incremental --no-restore -p:RestoreFallbackFolders= -v:diag`
- 历史结果:失败;`MSB4276`,默认 SDK resolver 无法解析 `Microsoft.NET.SDK.WorkloadAutoImportPropsLocator`,属于当前 WSL / dotnet 10 环境阻塞
- `DOTNET_CLI_HOME=/tmp/dotnet-home MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`MSB4018``ResolvePackageAssets` 命中失效 Windows fallback package folder `D:\Tool\Development Tools\Microsoft Visual Studio\Shared\NuGetPackages`
- `DOTNET_CLI_HOME=/tmp/dotnet-home MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net9.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`MSB4018`,原因同上
- `DOTNET_CLI_HOME=/tmp/dotnet-home MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore -p:TargetFramework=net10.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`MSB4018`,原因同上
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
- 结果:成功;定位到 PR `#288`,提取 latest-head unresolved AI review threads、MegaLinter 与 Docstring Coverage 信号
- `dotnet restore GFramework.sln -p:RestoreFallbackFolders="" -v minimal`
- 结果:成功;已刷新 WSL 原生 restore 元数据,清除先前的 stale fallback package folder 阻塞
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- 结果:成功;`28 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果:成功;`329 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 结果:成功;`137 Warning(s)``0 Error(s)`
- `dotnet restore GFramework.Core.Tests/GFramework.Core.Tests.csproj -p:TestTargetFrameworks=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`NU1201``GFramework.Tests.Common` 仅支持 `net10.0`,因此不能用 `net8.0` 旁路验证 `Core.Tests`
- `git diff --name-only origin/main...HEAD | wc -l`
- 当前结果:`75`
- `git diff --numstat origin/main...HEAD`
- 当前结果:累计 `1083` added、`1015` deleted`2098` changed lines
## 下一步建议
1. 提交 `SceneBehaviorBase.cs``RP-058` tracking/trace 更新,清掉 PR #286 当前 latest-head 上的格式类 review thread
2. 若继续 warning reduction 主线,应回到 `GFramework.Game.Tests/Config/YamlConfigLoaderTests.cs`,把它作为独立高上下文批次处理
1. 当前 turn 已按标准 WSL `dotnet build` 路径完成 `Core` / `Game` / `Core.Tests` Release build 验证;后续若继续 PR #288 收尾,优先重新抓取 unresolved threads确认哪些线程已可直接 resolve。
2. 若后续要继续 `Core` / `Core.Tests` / `Game` warning reduction应以当前标准 build 输出为新真值,而不是继续沿用上一轮带 workaround 参数的失败命令。
3. 若要开启下一轮批处理,优先选择新的 stop-condition例如新的 file 阈值、warning 目标或限定到单模块)后再继续。

View File

@ -1,251 +1,120 @@
# Analyzer Warning Reduction 追踪
# Analyzer Warning Reduction 追踪
## 2026-04-25 — RP-064
## 2026-04-24 — RP-058
### 阶段PR #286 latest-head review 格式跟进
### 阶段:按标准 WSL build 路径复核 PR #288 建议并完成本轮收口
- 触发背景:
- 用户要求执行 `$gframework-pr-review`,需要以当前分支 PR 页面而不是本地记忆为准,重新核对 CodeRabbit、MegaLinter 和测试状态
- 抓取脚本当前解析到的 PR 是 `#286`,最新 reviewed commit 为 `2b707343577193fc9904517e6078149653e95698`
- 最新 head 上真正未解决的代码线程只剩 `GFramework.Godot/Scene/SceneBehaviorBase.cs:148` 的缩进问题;其余 nitpick 为可选建议或已留待后续批次
- 用户指出“在 WSL 里直接执行 `dotnet build` 可以成功”,要求主线程按普通路径重新验证,而不是继续使用带 `MSBuildEnableWorkloadResolver=false``--no-restore`、手工 `TargetFramework` 的 workaround 命令
- 当前任务仍属于 PR #288 review follow-up因此本轮重点改为“区分哪些 AI 建议值得采纳”以及“用真实 WSL build 结果验证”
- 主线程实施:
- 运行 `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`,确认 PR `OPEN`、测试 `2156/2156` 通过、MegaLinter 仅剩 `dotnet-format` 警告
- 复核 `SceneBehaviorBase.cs` 后确认 `OnPauseAsync` 的方法签名与方法体缩进异常仍存在于本地源码;同段的 `OnResumeAsync``OnUnloadAsync` 也有同类偏差
- 在不改变行为的前提下统一修正三个方法的缩进,保持现有 XML 注释、`ConfigureAwait(true)` 语义与 Godot 主线程说明不变
- 更新 active tracking / trace记录当前 PR review follow-up 已完成,本地剩余外部信号只剩 PR 标题检查
- 验证里程碑:
- `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release`
- 结果:成功;`565 Warning(s)``0 Error(s)`
- 结论:当前格式修复未引入编译错误;模块既有 warning 基线仍存在,但不属于本次 PR review 跟进范围
- `dotnet format GFramework.Godot/GFramework.Godot.csproj --verify-no-changes --no-restore --include GFramework.Godot/Scene/SceneBehaviorBase.cs`
- 首次运行失败sandbox 环境下在 build host / pipe 建立阶段报错,未进入真实格式比较
- 提权复验:成功;仅提示 workspace load warning无格式差异
- 当前结论:
- PR #286 当前 latest-head 上唯一未解决的实质代码 review thread 已在本地修复
- MegaLinter 暴露的 `dotnet-format` 问题已被本地 `verify-no-changes` 复验覆盖
- `Title check: Inconclusive` 仍然存在,但属于 GitHub PR 标题元数据问题,不能通过本地代码提交直接消除
## 2026-04-24 — RP-057
### 阶段:清理 `PersistenceTests.cs` 残余 `MA0004`
- 触发背景:
- `RP-056` 提交后重新做非增量热点排序时,`GFramework.Game.Tests` 的剩余测试项目 warning 已明显收敛,只剩 `PersistenceTests.cs` 少量 `MA0004``YamlConfigLoaderTests.cs` 大量 warning
- 为避免在同一轮直接进入 `YamlConfigLoaderTests.cs` 的大文件高上下文批次,先吃掉 `PersistenceTests.cs` 这个独立小切片
- 主线程实施:
- 在 `PersistenceTests.cs` 中为统一设置仓库失败缓存一致性相关测试补齐剩余 `.ConfigureAwait(false)`
- 覆盖保存失败与删除失败两个测试场景中的缓存读取、存在性检查、后续保存和最终验证读取
- 更新 active tracking / trace明确下一批若继续推进应单独进入 `YamlConfigLoaderTests.cs`
- 验证里程碑:
- `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --no-incremental`
- 热点重排前:成功;`253 Warning(s)``0 Error(s)`
- 修复后:成功;`249 Warning(s)``0 Error(s)`
- 结论:`PersistenceTests.cs` 不再出现在 warning 输出中
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~UnifiedSettingsDataRepository_SaveAsync_When_Persist_Fails_Should_Keep_Cache_Consistent|FullyQualifiedName~UnifiedSettingsDataRepository_DeleteAsync_When_Persist_Fails_Should_Keep_Cache_Consistent"`
- 结果:成功;`Passed: 2``Failed: 0`
- 当前结论:
- `PersistenceTests.cs` 的残余 warning 已清零,`GFramework.Game.Tests` 剩余热点几乎全部压缩到了 `YamlConfigLoaderTests.cs`
- 当前工作树投影下,分支体积为 `27` 个文件、`991` 行,仍低于 `$gframework-batch-boot 75`
- 按 batch skill 的低风险边界,这一轮应在提交后收口;下一轮再把 `YamlConfigLoaderTests.cs` 作为单独批次处理
## 2026-04-24 — RP-056
### 阶段:修复 `GeneratedConfigConsumerIntegrationTests` 编译错误并清零该文件 warning
- 触发背景:
- `RP-055` 继续推进时,`GeneratedConfigConsumerIntegrationTests.cs` 在 raw string `invalidYaml` 段落附近出现 `CS8999`,导致 `GFramework.Game.Tests` 暂时无法编译
- 该文件同时仍是项目内少数残留 warning 热点之一,因此适合作为同一批次中的单文件收尾
- 主线程实施:
- 修复 `GeneratedConfigConsumerIntegrationTests.cs` 中损坏的 `CreateMonsterFiles` raw string 与方法边界,恢复文件可编译状态
- 保留并整理上一轮已开始的 `.ConfigureAwait(false)` 与断言 helper 抽取
- 继续将 `AssertGeneratedBindingsLoadResults` 再拆分为 catalog / monster / item 三个辅助方法,清除该文件剩余 `MA0051`
- 更新 active tracking / trace沿用 `merge-base(origin/main, HEAD)` 作为 `$gframework-batch-boot 75` 的唯一 stop-condition 口径
- 验证里程碑:
- `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release`
- 结果:成功;`59 Warning(s)``0 Error(s)`
- 结论:`GeneratedConfigConsumerIntegrationTests.cs` 不再出现在 warning 输出中
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~GeneratedConfigConsumerIntegrationTests"`
- 结果:成功;`Passed: 4``Failed: 0`
- 当前结论:
- `GFramework.Game.Tests` 已从 `RP-055` 收尾时的 `63 warning(s)` 进一步收敛到 `59 warning(s)`
- 当前工作树投影下,分支体积为 `27` 个文件、`943` 行,仍低于 `$gframework-batch-boot 75`
- 后续若继续自动推进,最自然的下一批将进入 `YamlConfigLoaderTests.cs` 这类高上下文大文件
## 2026-04-24 — RP-055
### 阶段:修正 stop-condition 口径并继续 `GFramework.Game.Tests` 小热点
- 触发背景:
- `RP-054` 之后复核 batch stop-condition 时,发现之前一度把工作树 diff 错当成了 skill 要求的 branch diff
- 按正确口径 `merge-base(origin/main, HEAD)` 计算,`RP-054` 提交后的真实分支体积是 `23` 个文件、`603` 行,因此仍可继续下一批
- 当前剩余 warning 里,`ArchitectureConfigIntegrationTests``GameConfigBootstrapTests``JsonSerializerTests` 属于独立且低风险的小切片
- 主线程实施:
- 在 `ArchitectureConfigIntegrationTests.cs` 中补齐异步架构初始化 / 销毁和异常断言的 `.ConfigureAwait(false)`
- 在 `GameConfigBootstrapTests.cs` 中补齐启动流程、并发初始化断言与 `WaitForTaskWithinAsync``.ConfigureAwait(false)`
- 在 `JsonSerializerTests.cs` 中将坐标解析改为 `CultureInfo.InvariantCulture`
- 顺手清理 `YamlConfigLoaderAllOfTests.cs``PersistenceTests.cs` 中上一批遗漏的字段态状态检查和异步等待 warning
- 纠正 active tracking明确 stop-condition 必须使用 `origin/main...HEAD` 的 merge-base 分支 diff而不是工作树 diff
- 验证里程碑:
- `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release`
- 并行误用 build/test 时:出现 `MSB3026` / `CS2012` 文件占用噪声,不计入代码结论
- 串行复验:成功;`63 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~ArchitectureConfigIntegrationTests|FullyQualifiedName~GameConfigBootstrapTests|FullyQualifiedName~JsonSerializerTests"`
- 结果:成功;`Passed: 19``Failed: 0`
- 当前结论:
- `GFramework.Game.Tests` 已从上一批收尾时的 `71 warning(s)` 进一步降到 `63 warning(s)`
- 这次提交后的分支体积投影为 `26` 个文件、`691` 行,仍低于 `$gframework-batch-boot 75`
- 剩余热点越来越集中到 `YamlConfigLoaderTests.cs``GeneratedConfigConsumerIntegrationTests.cs`,后续继续时应把它们视为高上下文批次
## 2026-04-24 — RP-054
### 阶段:`GFramework.Game.Tests` 低风险测试 warning 批次(触发文件数停止阈值)
- 触发背景:
- 用户要求“直接进入下一批”,继续沿 `$gframework-batch-boot 75` 自动推进 warning reduction
- 以 `origin/main` 为基线时,上一批提交后分支累计 diff 仍只有 `8` 个文件,足够再落一个独立批次
- 重新执行 `dotnet clean GFramework.sln -c Release` 仍停在 `ValidateSolutionConfiguration`,因此继续以直接 `dotnet build GFramework.sln -c Release` 的输出挑选低风险热点
- 主线程实施:
- 从整仓 `Release build``116 warning(s)` 入口观测值中,选择 `GFramework.Game.Tests` 的小型测试文件和 `PersistenceTestUtilities.cs` 作为当前批次,刻意避开 `YamlConfigLoaderTests.cs` 这类高上下文大文件
- 在 `YamlConfigLoaderIfThenElseTests.cs``YamlConfigLoaderDependentSchemasTests.cs``YamlConfigLoaderDependentRequiredTests.cs``YamlConfigLoaderNegationTests.cs``YamlConfigLoaderAllOfTests.cs``YamlConfigLoaderEnumTests.cs``YamlConfigTextValidatorTests.cs``PersistenceTests.cs` 中补齐 `.ConfigureAwait(false)`,并把字段态 `_rootPath``ThrowIfNull` 改为显式 `InvalidOperationException`
- 将 `PersistenceTestUtilities.cs` 拆分为 `TestDataLocation.cs``TestSaveData.cs``TestVersionedSaveData.cs``TestSimpleData.cs``TestNamedData.cs`,消除 `MA0048` 并对齐仓库的一文件一主类型风格
- 在 `YamlConfigSchemaValidatorTests.cs` 中把字段态 `_rootPath` 的校验改成显式状态异常,避免继续触发 `MA0015`
- 验证里程碑:
- `dotnet clean GFramework.sln -c Release`
- 结果:失败;停在 `ValidateSolutionConfiguration``0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.sln -c Release`
- 结果:成功;`116 Warning(s)``0 Error(s)`
- `dotnet clean GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release`
- 结果失败clean 阶段提前结束,`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release`
- 第一轮批次后:成功;`80 Warning(s)``0 Error(s)`
- 收尾修正后:成功;`71 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Game.Tests/GFramework.Game.Tests.csproj -c Release --filter "FullyQualifiedName~YamlConfigLoaderIfThenElseTests|FullyQualifiedName~YamlConfigLoaderDependentSchemasTests|FullyQualifiedName~YamlConfigLoaderDependentRequiredTests|FullyQualifiedName~YamlConfigLoaderNegationTests|FullyQualifiedName~YamlConfigLoaderAllOfTests|FullyQualifiedName~YamlConfigLoaderEnumTests|FullyQualifiedName~YamlConfigTextValidatorTests|FullyQualifiedName~YamlConfigSchemaValidatorTests|FullyQualifiedName~PersistenceTests"`
- 结果:成功;`Passed: 63``Failed: 0`
- 当前结论:
- `GFramework.Game.Tests` 本轮入口热点已从 `116 warning(s)` 收敛到 `71 warning(s)`,且本轮 touched files 不再出现在 warning 输出中
- 当前工作树相对 `origin/main` 的累计 diff 已达到 `76` 个文件、`986` 行,超过 `$gframework-batch-boot 75` 的主停止阈值
- 按批处理技能规则,本轮必须在提交当前批次后停止;剩余候选应在新一轮里单独评估,尤其是 `YamlConfigLoaderTests.cs`
## 2026-04-24 — RP-053
### 阶段:`GFramework.Godot` / `GFramework.Godot.Tests` 小批次 warning 清理
- 触发背景:
- 用户以 `$gframework-batch-boot 75` 要求继续按批次推进 analyzer warning reduction并以 `origin/main` 作为累计分支 diff 基线
- 当前 worktree `fix/analyzer-warning-reduction-batch` 相对 `origin/main` 的已提交分支 diff 为 `0` 个文件,具备继续落一个低风险 warning batch 的空间
- solution-level `dotnet clean GFramework.sln -c Release` 仍在 `ValidateSolutionConfiguration` 阶段失败,因此本轮继续用直接 `dotnet build GFramework.sln -c Release` 建立热点观察值
- 主线程实施:
- 运行 `dotnet build GFramework.sln -c Release`,确认当前整仓观测值为 `1122 warning(s)`,并从输出中挑选 `GFramework.Godot` 的小范围热点作为本轮批次
- 在 `GodotYamlConfigEnvironment.cs` 中按“普通文件系统 / Godot 路径”拆分目录枚举 helper消除 `MA0051`
- 在 `AbstractArchitecture.cs``SceneBehaviorBase.cs` 中将必须保留 Godot 主线程上下文的 await 显式改为 `.ConfigureAwait(true)`,清理 `MA0004` 并把线程意图写入注释
- 在 `GFramework.Godot.Tests` 中补齐异步断言的 `.ConfigureAwait(false)`,并让 `RichTextMarkupTests` 的测试字典显式指定 `StringComparer.Ordinal`
- 验证里程碑:
- `dotnet clean GFramework.sln -c Release`
- 结果:失败;停在 `ValidateSolutionConfiguration``0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.sln -c Release`
- 结果:成功;`1122 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot/GFramework.Godot.csproj -c Release`
- 第一轮修复后:成功;`12 Warning(s)``0 Error(s)`,仅剩 `MA0004`
- 第二轮修复后:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release --filter "FullyQualifiedName~AbstractArchitectureModuleInstallationTests|FullyQualifiedName~GodotYamlConfigLoaderTests|FullyQualifiedName~RichTextMarkupTests"`
- 结果:成功;`Passed: 15``Failed: 0`
- `dotnet build GFramework.Godot.Tests/GFramework.Godot.Tests.csproj -c Release`
- 并行验证时:成功;`1 Warning(s)``0 Error(s)``MSB3026` 为与并行 `dotnet test` 竞争输出 DLL 的文件占用
- 串行复验:成功;`0 Warning(s)``0 Error(s)`
- 当前结论:
- `GFramework.Godot``GFramework.Godot.Tests` 本轮直接涉及的 warning 已全部清零
- 当前待提交代码批次相对 `origin/main` 的源码 diff 为 `6` 个文件、`107` 行,距离 `$gframework-batch-boot 75` 主停止阈值仍有充足余量
- 继续推进的下一批候选将主要落在 `GFramework.Game` 等高 warning 基线模块,已不再属于当前同等级低风险切片,因此本轮在这里收口并进入提交
## 2026-04-24 — RP-052
### 阶段PR review follow-upcomparer 契约 + `ConfigureAwait(false)` 收尾)
- 触发背景:
- 当前分支 PR #283 的最新 review 中,`greptile-apps[bot]` 仍有一个未解决线程,指出 `UnifiedSettingsDataRepository.CloneFile` fallback 会静默丢失原 comparer
- CodeRabbit 另指出 `AutoRegisterExportedCollectionsGeneratorTests.cs` 中还残留 5 处 `await test.RunAsync();`,与同项目其他测试文件的 `.ConfigureAwait(false)` 风格不一致
- 主线程实施:
- 复核 PR review JSON、`UnifiedSettingsDataRepository.cs``UnifiedSettingsFile.cs``AutoRegisterExportedCollectionsGeneratorTests.cs` 的当前代码,确认只有 comparer 契约线程仍属最新 head 上的实质问题
- 将 `UnifiedSettingsFile.Sections` 的 XML 注释补充为显式 comparer 契约,并把默认字典初始化改为 `StringComparer.Ordinal`
- 将 `CloneFile` fallback 从隐式默认 comparer 改为显式 `StringComparer.Ordinal`,并同步修正文档注释,避免继续暗含“保留原语义”的错误表述
- 把 `AutoRegisterExportedCollectionsGeneratorTests` 中剩余的 5 处 `await test.RunAsync();` 统一为 `.ConfigureAwait(false)`,同时让 `VerifyDiagnosticsAsync` 内部也消费 `ConfigureAwait(false)`
- 重新抓取 PR #288 review确认 latest-head open threads 为 `CodeRabbit 6 + Greptile 2`
- 复核 `outside diff + nitpick` 的 19 条建议,只采纳本地仍成立的建议;拒绝把“评论总数”机械等同于“必须全改”
- 完成以下高信号修复:
- `ContextAware*` / `AsyncExtensions` / `NumericExtensions` / `StringExtensions` / `StoreBuilder`:回退为 `ArgumentNullException.ThrowIfNull(...)`
- `ArchitectureServicesTests` / `GameContextTests`:同步 XML `<exception>``NotSupportedException`
- `RegistryInitializationHookBaseTests`:修复 override 可空签名实现,避免再次引入编译错误
- `RollingFileAppenderTests` / `TaskCoroutineExtensionsTests` / `WaitForTaskTests` / `ScopedStorage`:移除无收益噪音代码
- `FileStorage`:通过 `leaveOpen: true` 修正 `FileStream` 的双重释放语义
- `SceneRouterBase`:统一显式 `ConfigureAwait(true)` 并补齐引擎线程亲和说明
- `StoreSelection`:保留 `net9.0+``System.Threading.Lock`,同时修正条件编译旁的注释写法,避免 `CS1587`
- 验证里程碑:
- `dotnet restore GFramework.sln -p:RestoreFallbackFolders="" -v minimal`
- 结果:成功;证明先前 `MSB4018` 来自 stale restore 元数据,而不是当前 WSL 默认 build 路径本身不可用
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
- 结果:成功;`28 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Game/GFramework.Game.csproj -c Release`
- 结果:成功;`533 Warning(s)``0 Error(s)``GFramework.Game` 仍有既有 warning 基线,本轮 follow-up 仅处理 PR review 指向的 comparer 契约与测试异步等待一致性
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`
- 首次并行复验:失败;`FileNotFoundException`,原因是 `--no-build` 测试在 Release DLL 落盘前启动
- 串行复验:成功;`Passed: 48``Failed: 0`
- 结果:成功;`329 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release`
- 结果:成功;`137 Warning(s)``0 Error(s)`
- 当前结论:
- PR #283 当前仍打开的 comparer review thread 已在本地代码与 XML 注释层面得到对应修复
- `AutoRegisterExportedCollectionsGeneratorTests` 的异步等待风格已与同项目其他测试保持一致
- 当前改动已通过直接受影响测试项目的 Release build 与串行 Release test 复验,可进入提交阶段
- 用户关于“WSL 里直接 `dotnet build` 可行”的判断正确
- 前一轮失败的核心原因不是仓库不可构建,而是主线程附加的 workaround 参数改变了 MSBuild 行为
- 本轮已完成 PR #288 中一组仍成立的建议修复,并重新拿到标准 WSL 路径下的 Release build 验证
- 剩余 review 线程需要在新 head 上重新抓取后再决定是否逐条 resolve
## 2026-04-24 — RP-051
## 2026-04-25 — RP-063
### 阶段:`GFramework.Godot.SourceGenerators.Tests` warning 清零
### 阶段:先收口 PR #288 latest-head 编译错误,再暂停在环境阻塞点并准备提交
- 触发背景:
- 用户要求直接运行 `dotnet clean`,不再添加额外 shell 包装solution-level `dotnet clean` 仍然在 `ValidateSolutionConfiguration` 阶段失败
- 直接执行仓库根目录 `dotnet build` 成功,并输出 `1184 warning(s)`,说明当前真实热点已从 `GFramework.Godot.SourceGenerators` 转移到对应测试项目
- 用户显式要求先执行 `$gframework-pr-review`,并指出 `AsyncExtensionsTests.cs(126,23)` 当前存在 `CS0029` / `CS1662` 构建错误
- 当前 worktree 仍是 `fix/analyzer-warning-reduction-batch`,因此本 turn 继续沿用 `analyzer-warning-reduction` 的 active recovery 文档
- 主线程实施:
- 以 `GFramework.Godot.SourceGenerators.Tests` 为独立批次,先确认该项目本地基线为 `24 warning(s)`
- 在 `BindNodeSignalGeneratorTests.cs``AutoSceneGeneratorTests.cs``AutoUiPageGeneratorTests.cs``GetNodeGeneratorTests.cs``AutoRegisterExportedCollectionsGeneratorTests.cs``GodotProjectMetadataGeneratorTests.cs` 中抽取共享 source / diagnostic helper压缩重复长方法
- 在 `Core/GeneratorTest.cs` 中补充 `ConfigureAwait(false)`,清除项目内唯一 `MA0004`
- 把 `GFramework.Godot.SourceGenerators.Tests` 项目 warning 从 `24` 降到 `0`
- 运行 PR review 抓取脚本,确认当前分支对应 PR `#288`
- 核对 latest-head unresolved review threads 后,优先修复 `AsyncExtensionsTests.cs``ct => Task.Delay(...).ConfigureAwait(false)` 错误返回 `ConfiguredTaskAwaitable` 的问题
- 顺手收敛多处已被 latest review 点名且本地仍成立的低风险残留:
- 测试中的 `async``await`
- `ValueTask` 断言包装
- `RegistryInitializationHookBaseTests.cs` 的可空返回签名
- `NumericExtensions.cs``StringExtensions.cs``StoreBuilder.cs` 的 Allman 花括号残留
- `StoreSelection.cs``net9.0+` 下切到 `System.Threading.Lock`,同时保留 `net8.0` 兼容分支
- 验证里程碑:
- `dotnet build`
- 结果:成功;`1184 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj`
- 初始结果:成功;`24 Warning(s)``0 Error(s)`
- 第一批(`BindNodeSignal` + `GeneratorTest`)后:`16 Warning(s)`
- 第二批(`AutoScene` / `AutoUiPage` / `GetNode`)后:`8 Warning(s)`
- 第三批(`Registration` / `Project`)后:`1 Warning(s)`
- 收尾修复后:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet test GFramework.Godot.SourceGenerators.Tests/GFramework.Godot.SourceGenerators.Tests.csproj -c Release --no-build`
- 结果:成功;`Passed: 48``Failed: 0`
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
- 结果:成功;确认 PR `#288` 的 latest-head unresolved AI review threads 共 `9` 个,其中 `AsyncExtensionsTests.cs:126` 为 critical 编译错误
- `DOTNET_CLI_HOME=/tmp/dotnet-home MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`MSB4018``ResolvePackageAssets` 仍读取失效 Windows fallback package folder `D:\Tool\Development Tools\Microsoft Visual Studio\Shared\NuGetPackages`
- `DOTNET_CLI_HOME=/tmp/dotnet-home MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net9.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;原因同上
- `DOTNET_CLI_HOME=/tmp/dotnet-home MSBuildEnableWorkloadResolver=false dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-restore -p:TargetFramework=net10.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;原因同上
- 当前结论:
- `GFramework.Godot.SourceGenerators.Tests` 已在 `Debug` / `Release` 构建下达到 `0 warning(s)`
- 按 `origin/main` merge-base 计算并只纳入当前暂存批次时,累计分支 diff 为 `23` 个文件,低于 `$gframework-batch-boot 75` 的主停止阈值
- 仓库根目录 `dotnet clean` 仍无法稳定产出新的 clean 基线,需要在下一轮单独排查
- 当前 worktree 已有与本批次无关的既有改动;提交时必须只暂存 analyzer warning reduction 相关文件
- 用户点名的 `AsyncExtensionsTests.cs` 编译错误已在源码层修复
- 本 turn 未能拿到新的可通过 Release build阻塞点已从先前记录的 `MSB4276` 收敛为当前 `obj/*.csproj.nuget.g.props` 中 stale Windows fallback package folder 导致的 `MSB4018`
- 用户随后要求“先不管这个了,先提交吧”,因此本 turn 在记录环境阻塞后先执行提交收口
## 2026-04-24 — RP-050
## 2026-04-25 — RP-062
### 阶段:clean-build 基线修正与 `GFramework.Godot.SourceGenerators` 切片清零
### 阶段:触达 `$gframework-batch-boot 75` 停止阈值并收口到 `75 files / 2098 lines`
- 触发背景:
- 用户确认之前的 `0 Warning(s)` 来自增量构建假阴性;只有先 `dotnet clean``dotnet build`warning 才会重新出现
- 用户给出 clean solution build 的真实结果:`Build succeeded with 1193 warning(s)`
- `RP-061` 收尾时分支相对 `origin/main` 仍只有 `48` 个已提交文件,距离本轮 `75 files` 停止条件还有明显空间
- 用户明确允许继续委派 subagent因此主线程继续把低风险机械型写集拆成互不重叠的 test / runtime 小批次
- 本轮主目标不是继续深挖单个高上下文热点,而是用新的低风险文件精确把 branch diff 推到阈值后停止
- 主线程实施:
- 纠正当前 topic 的 active todo / trace把 clean build 作为新的 warning 检查真值
- 在 `BindNodeSignalGenerator.cs``GetNodeGenerator.cs``GodotProjectMetadataGenerator.cs` 中完成分阶段方法抽取与字符串比较修正
- 在 `Registration/AutoRegisterExportedCollectionsGenerator.cs` 中拆分 `TryCreateRegistration`,清除最后一个 `MA0051`
- 更新 `AGENTS.md`,明确 warning 检查必须先 `dotnet clean``dotnet build`
- 先接受并提交 7 文件 `Core.Tests` 收尾批次为 `03c73a8` `test(core-tests): 收敛测试桩与辅助类型 warning`
- 随后主线程与多个 worker 并行收口以下新增文件:
- `ArchitectureAdditionalCqrsHandlersTests.cs`
- `RegistryInitializationHookBaseTests.cs`
- `CommandCoroutineExtensionsTests.cs`
- `TaskCoroutineExtensionsTests.cs`
- `WaitForTaskTTests.cs`
- `AsyncExtensionsTests.cs`
- `LogContextTests.cs`
- `PauseStackManagerTests.cs`
- `AsyncExtensions.cs`
- `CollectionExtensions.cs`
- `ContextAwareCommandExtensions.cs`
- `ContextAwareEnvironmentExtensions.cs`
- `ContextAwareEventExtensions.cs`
- `ContextAwareQueryExtensions.cs`
- `ContextAwareServiceExtensions.cs`
- `GuardExtensions.cs`
- `NumericExtensions.cs`
- `StoreEventBusExtensions.cs`
- `StringExtensions.cs`
- `StoreBuilder.cs`
- `StoreSelection.cs`
- 将上述 22 文件批次收口为 `9ce1fa6` `refactor(core): 收敛 Core 扩展与测试的机械 warning`
- 验证里程碑:
- `dotnet clean GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release`
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release --no-restore -p:TargetFramework=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Godot.SourceGenerators/GFramework.Godot.SourceGenerators.csproj -c Release`
- 首次验证:成功;`1 Warning(s)`,剩余 `Registration/AutoRegisterExportedCollectionsGenerator.cs(182,25)` `MA0051`
- 修复后复验:成功;`0 Warning(s)``0 Error(s)`
- `dotnet build GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --no-incremental --no-restore -p:RestoreFallbackFolders= -v:diag`
- 结果:失败;`MSB4276`,默认 SDK resolver 缺少 `Microsoft.NET.SDK.WorkloadAutoImportPropsLocator`
- `dotnet restore GFramework.Core.Tests/GFramework.Core.Tests.csproj -p:TestTargetFrameworks=net8.0 -p:RestoreFallbackFolders="" -v minimal`
- 结果:失败;`NU1201``GFramework.Tests.Common` 仅支持 `net10.0`,不能作为 `Core.Tests` 的 net8 旁路验证
- `git diff --name-only origin/main...HEAD | wc -l`
- 结果:`75`
- `git diff --numstat origin/main...HEAD`
- 结果:累计 `1083` added、`1015` deleted`2098` changed lines
- 当前结论:
- `GFramework.Godot.SourceGenerators` 已在 clean `Release` build 下从 9 个 warning 降到 0 个 warning
- 整仓库 warning 基线仍以用户确认的 clean solution build `1193 warning(s)` 为准
- 下一轮应继续从 clean solution build 输出中选择新的低风险热点
- 本轮 `$gframework-batch-boot 75` 已精确达到主停止条件,默认恢复点应停止在 `9ce1fa6`
- `Core` runtime 的本轮机械型改动已有可通过的最小 Release build 验证
- `Core.Tests` 的继续推进当前首先受 `MSB4276` 环境阻塞影响;下一轮若要继续,应先修复构建环境,再重新建立 warning 基线
## Archive Context
## 历史归档指针
- 当前轮次归档:
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/todos/analyzer-warning-reduction-history-rp042-rp048.md)
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)
- 历史跟踪归档:
- [analyzer-warning-reduction-history-rp001.md](../archive/todos/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/todos/analyzer-warning-reduction-history-rp002-rp041.md)
- 历史 trace 归档:
- 早期 trace 归档:
- [analyzer-warning-reduction-history-rp001.md](../archive/traces/analyzer-warning-reduction-history-rp001.md)
- [analyzer-warning-reduction-history-rp002-rp041.md](../archive/traces/analyzer-warning-reduction-history-rp002-rp041.md)
- [analyzer-warning-reduction-history-rp042-rp048.md](../archive/traces/analyzer-warning-reduction-history-rp042-rp048.md)

View File

@ -12,33 +12,23 @@
## 当前恢复点
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-031`
- 恢复点编号:`DOCUMENTATION-FULL-COVERAGE-GOV-RP-034`
- 当前阶段:`Phase 5 - Governance Maintenance`
- 当前焦点:
- 继续按 `$gframework-batch-boot 75``origin/main` 分支 diff 阈值做小批量文档治理;当前 baseline 已回到 `origin/main`,本批只继续处理新的低风险 reader-facing 缺口
- 保持 `README.md``docs/**` 公开页面只承载读者需要的采用信息,不再混入 XML inventory、覆盖基线、恢复点或治理批次说明
- 继续优先处理低风险 metadata 缺口、坏链、README 文档入口对齐、Reader-friendly 链接标签与 Markdown 结构问题,避免跨模块语义改写
- 保持 `Game` persistence docs surface 与当前 `README`、源码、`PersistenceTests` 使用同一套 owner / adoption path 叙述
- 保持 `GFramework.Godot.SourceGenerators/README.md``docs/zh-CN/tutorials/godot-integration.md` 在生命周期接法上的一致性
- 保持 active tracking / trace 只承载当前恢复入口,把阶段细节留在 `archive/`
- 按本轮 `$gframework-batch-boot 50` 约束继续使用 `origin/main``984fb21``2026-04-25 11:11:56 +08:00`)作为唯一 baseline只推进低风险、可切片的文档治理批次
- 本轮已收口三类目标5 个模块 README 的语义化链接标签、7 个 `Core` 热点页的代码块语言标记、7 个基础教程页的代码块语言标记
- 当前已接收 worker A 的 README 切片结果;其余代码块标记批次由主线程统一复核并补齐
- 本轮 `19` 个文档文件连同 active tracking / trace 已落地;当前 branch diff 已确认达到 `21 / 50` 个 changed files仍处于当前批次阈值安全区间
- 下一轮若继续批处理,优先挑选新的低风险 reader-facing 缺口,并保持单批次预计落地规模不超过剩余 headroom
## 当前状态摘要
- `Core``Ecs.Arch``Cqrs``Game``Godot` 五个模块族当前都已有 README / landing / topic / API 参考层级的已验证入口。
- `2026-04-24` 当前本地 `docs/sdk-update-documentation``origin/main` 同步到 `4c2994e`,相对 `$gframework-batch-boot 75` 的 baseline 当前为 `0 / 75` 个 changed files后续批次可以继续但仍应保持小 write set。
- `2026-04-24` 使用 `$gframework-pr-review` 抓取当前 PR `#284` 后,确认 latest head commit
`77540c07f0890cc05b10a849722c87b8bed8f561` 仍有 `3` 条 CodeRabbit 与 `1` 条 Greptile open thread本轮仅继续收口本地复核后仍成立的 reader-facing 文档入口与 active tracking 精简问题。
- 本轮 PR follow-up 仅收口仍然成立的 review 项:
- 将过长的 active tracking / trace 瘦身,并把 `RP-023``RP-025` 的细节迁入 `archive/`
- 将 `docs/zh-CN/core/context.md` 的标题本地化为中文读者友好的写法
- 统一 `docs/zh-CN/troubleshooting.md``/zh-CN/core/architecture``/zh-CN/faq``.md` 链接写法
- 本批次将根 `README.md` 中两个仍直接暴露文件路径的内部支撑模块入口改为 reader-friendly 链接标签,避免目录表格继续把路径本身当成入口名称。
- 本批次继续将 `Core``Game``Source Generators` 和三篇 `Abstractions` 落地页的纯英文 `title` / H1 改为中文读者友好的入口标题,减少首页与侧边栏扫描成本。
- 本批次继续将 `core/architecture.md``command.md``events.md``logging.md``property.md``query.md` 的纯英文 `title` / H1 本地化为中英对照入口标题,保持 Core 子栏目扫描体验一致。
- 当前批次完成后,纯英文 `title` 扫描只剩 `docs/zh-CN/core/cqrs.md``CQRS``docs/zh-CN/index.md``GFramework`;它们分别属于通用缩写与品牌名,不再作为本轮优先本地化对象。
- 本批次补齐了 `docs/zh-CN/index.md``description`,以及 `docs/zh-CN/tutorials/basic/01-07.md``title` / `description`,让首页和基础教程章节页拥有完整 frontmatter metadata。
- 本批次统一将教程、最佳实践、Core、Godot 页面里缺显式扩展名的站内 Markdown 链接补齐为 `.md``index.md`,避免目录链接、绝对路径旧写法与 VitePress 构建解析分叉。
- 本批次把模块 README、仓库根 README、`docs/index.md` 及多组中文落地页里直接暴露文件路径的入口调整为读者友好的可点击标签,同时补齐语言落地页 metadata 与 README 指向。
- `2026-04-25` worker A 已完成并提交 5 个模块 README 的 reader-facing 链接标签修正,提交为 `bd5cdb5``docs(readme): 优化链接标签`);当前批次已接受该切片结果。
- `2026-04-25` 主线程补齐了 `docs/zh-CN/core/configuration.md``extensions.md``ioc.md``localization.md``pause.md``pool.md``system.md` 的裸 fenced code block opening 语言标记。
- `2026-04-25` 教程批次当前覆盖 `docs/zh-CN/tutorials/basic/01-environment.md``07-summary.md`,补齐的内容以目录树、流程图和控制台输出为主,统一显式标注为 `text`
- `2026-04-25` 当前实际 branch diff 已更新为 `21 / 50` 个 changed files其中 `5` 个文件来自已提交的 README 标签切片,`16` 个文件来自本轮代码块标记与 active tracking / trace 更新。
- `2026-04-25` 本轮目录级验证已覆盖 `docs/zh-CN/core``docs/zh-CN/tutorials/basic`README 目标文件链接校验和 `docs/` 站点构建也都已通过。
- `Game` persistence docs surface 当前以 `docs/zh-CN/game/data.md``storage.md``serialization.md``setting.md`
作为最小巡检集合;若后续 README、runtime public API 或 `PersistenceTests` 变动,应优先复核这一组页面。
- `Godot` runtime 与 generator 入口当前以 `GFramework.Godot/README.md`
@ -53,7 +43,8 @@
- `GFramework.Cqrs` 在当前 WSL / dotnet 环境下仍会读取失效的 fallback package folder并在标准 build 中触发
`MSB4276` / `MSB4018`;这是已知环境阻塞,不属于本轮文档回归。
- 当前 WSL 会话里 `git.exe` 可解析但不能执行,应继续使用显式 `--git-dir` / `--work-tree` 绑定作为默认 Git 策略。
- PR `#282``Title check` 仍可能提示标题过泛;这是 GitHub PR 元数据问题,不属于本地文件缺陷。
- README 链接标签与 `Core` / 教程代码块标记这两类低风险批次已经消化完本轮目标文件,但 `docs/zh-CN` 其他目录仍可能保留未显式标语言的历史代码块。
- PR `#287` 的 latest-head review 是否还有 open thread 尚未在本轮重新抓取;若继续下一轮,应先复核远端 review 状态再扩批。
## 归档指针
@ -70,6 +61,24 @@
## 最新验证
- `2026-04-25` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Core/README.md`
- 结果通过README 链接目标有效。
- `2026-04-25` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Core.SourceGenerators/README.md`
- 结果通过README 链接目标有效。
- `2026-04-25` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Cqrs.SourceGenerators/README.md`
- 结果通过README 链接目标有效。
- `2026-04-25` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Ecs.Arch/README.md`
- 结果通过README 链接目标有效。
- `2026-04-25` `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh GFramework.Game.SourceGenerators/README.md`
- 结果通过README 链接目标有效。
- `2026-04-25` `rg -n '\\[[^\\]]*(README\\.md|\\.md|\\.md/|/zh-CN/[^\\]]*)\\]\\([^)]*\\)' GFramework.Core/README.md GFramework.Core.SourceGenerators/README.md GFramework.Cqrs.SourceGenerators/README.md GFramework.Ecs.Arch/README.md GFramework.Game.SourceGenerators/README.md`
- 结果:无命中;本轮 5 个 README 已无可见路径式 / 文件名式 Markdown 链接标签残留。
- `2026-04-25` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core`
- 结果:通过;`Core` 栏目本轮触达页面的 frontmatter、链接与代码块校验均通过。
- `2026-04-25` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/basic`
- 结果:通过;基础教程栏目本轮触达页面的 frontmatter、链接与代码块校验均通过。
- `2026-04-25` `bun run build`(工作目录:`docs/`
- 结果通过README 标签修正与 `Core` / 基础教程代码块语言标记补齐后站点仍可构建,仅保留既有大 chunk warning。
- `2026-04-24` `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
- 结果通过PR `#284` 处于 `OPEN`latest head commit `77540c07f0890cc05b10a849722c87b8bed8f561``3` 条 CodeRabbit 与 `1` 条 Greptile open thread测试汇总为 `2156 passed`,仅剩 `Title check` 的 inconclusive PR 元数据提示。
- `2026-04-24` `rg -n --pcre2 '\\]\\(/zh-CN/[^)]+(?<!\\.md)\\)' docs/zh-CN/troubleshooting.md`
@ -90,11 +99,20 @@
- 结果:通过;经过三轮标题本地化后,仅剩 `CQRS``GFramework` 两个品牌/缩写型标题。
- `2026-04-24` `bun run build`(工作目录:`docs/`
- 结果:通过;根 `README.md` reader-friendly 链接标签修正与 `docs/zh-CN` 多页标题本地化落地后站点仍可正常构建,仅保留既有大 chunk warning。
- `2026-04-25` `python3 .agents/skills/gframework-doc-refresh/scripts/scan_module_evidence.py Core`
- 结果:通过;技能仍能正常解析 `Core` 模块证据面,说明新增的 reader-facing 输出约束未破坏模块扫描主流程。
- `2026-04-25` `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
- 结果通过PR `#287` 处于 `OPEN`latest head commit `8209d7a29f35d969fca6258b9817da9b33a203a3` 仅剩
`1` 条 Greptile open thread无 failed checks测试汇总为 `2156 passed`
- `2026-04-25` `bun run build`(工作目录:`docs/`
- 结果:通过;`docs/zh-CN/api-reference/index.md` 的站内入口标签统一为语义化写法后站点仍可正常构建,仅保留既有大 chunk warning。
- `2026-04-25` `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN`
- 结果:通过;本轮触达页面的 frontmatter、链接与代码块校验均通过脚本仅继续报告仓库中既有页面的“代码块缺少语言标记”警告。
## 下一步
1. 当前基线已回到 `origin/main`,本轮变更仍是小 write set后续若继续执行 `$gframework-batch-boot 75`,仍优先选择 `5``10` 个文件以内的小批次。
2. 若继续处理 reader-facing 文档问题,优先筛查剩余 README / landing page 中是否还有路径式链接标签或不必要的内部治理措辞;纯英文标题方面仅剩品牌名与缩写,不再是当前高优先级切片。
1. 若继续下一轮 `$gframework-batch-boot 50`,优先重新抓取 `$gframework-pr-review` 确认 PR `#287` 的 latest-head review 是否还有 open thread当前相对阈值仍有 `29` 个 changed files 的 headroom
2. 后续若继续处理 reader-facing 文档问题,优先筛查剩余页面里的维护者视角限制说明、模块 README 中仍可能存在的裸路径标签,以及 `docs/zh-CN` 其他目录里的代码块语言标记缺口
3. 若后续分支继续调整 `Game` persistence runtime、README 或公共 API优先复核 `docs/zh-CN/game/data.md`
`storage.md``serialization.md``setting.md` 与 landing page 是否仍保持同一套职责边界。
4. 若后续分支继续调整 `Godot` generator 接法,优先复核 `GFramework.Godot.SourceGenerators/README.md`

View File

@ -1,40 +1,37 @@
# Documentation Full Coverage Governance Trace
## 2026-04-24
## 2026-04-25
### 当前恢复点RP-031
### 当前恢复点RP-034
- 当前批次聚焦新的低风险 reader-facing README 缺口,只处理根 `README.md` 的路径式链接标签和对应的 active tracking / trace 基线更新。
- 以 `origin/main``4c2994e``2026-04-24 17:57:23 +0800`)为 `$gframework-batch-boot 75` baseline当前本地 `docs/sdk-update-documentation` 与该基线同步branch cumulative diff 起始值为 `0 / 75`
- 本批次随后扩展到 `6` 个中文 landing / abstraction 页面标题本地化,总 write set 仍保持在 reader-facing 文案层。
- 本轮按 `$gframework-batch-boot 50` 执行baseline 固定为 `origin/main``984fb21``2026-04-25 11:11:56 +08:00`);开始时 committed branch diff 为 `5 / 50` 个 changed files。
- 已接受 worker A 的 README 切片结果5 个模块 README 的 reader-facing 链接标签修正已落在提交 `bd5cdb5``docs(readme): 优化链接标签`)。
- 主线程补齐了 `docs/zh-CN/core` 下 7 个热点页面与 `docs/zh-CN/tutorials/basic` 下 7 个教程页面的裸 fenced code block opening 语言标记,按内容分别落为 `csharp``text`
- 当前批次已提交为 `9dfee75``docs(documentation): 补齐文档代码块标记`);提交后实际 branch diff 为 `21 / 50` 个 changed files仍有后续小批次空间。
### 当前决策RP-031
### 当前决策RP-034
- 当当前分支重新与 `origin/main` 对齐后,`$gframework-batch-boot 75` 可以继续推进,但仍只接受低风险、读者可见、易验证的小批次
- 公开 README 的表格入口不应继续把文件路径本身暴露成链接标签;入口名称应直接告诉读者要进入哪个模块 README
- active tracking / trace 里的 stop-condition 指标必须反映当前真实基线,不再沿用已经过时的 `58 / 75` 历史值
- README 批次只改 reader-facing 可见标签,不改链接目标;复核结果通过后直接接受 worker A 的独立提交,避免主线程重复改写同一组文件
- 代码块语言标记批次以 opening fence 为唯一修正点,不重写示例内容;目录树、流程图、控制台输出统一标 `text`,可执行或 API 示例标 `csharp`
- 教程 `01``07` 当前未发现额外裸 opening fence 之外的高风险文案问题,因此本轮不扩展到结构性重写,保持在低语义风险范围内
### 当前验证RP-031
### 当前验证RP-034
- 基线确认:
- `git --git-dir=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework/.git/worktrees/GFramework-update-documentation --work-tree=/mnt/f/gewuyou/System/Documents/WorkSpace/GameDev/GFramework-WorkTree/GFramework-update-documentation for-each-ref --format='%(refname:short) %(objectname:short) %(committerdate:iso8601)' refs/heads/main refs/remotes/origin/main refs/heads/docs/sdk-update-documentation HEAD`
- 结果:通过;`docs/sdk-update-documentation``origin/main` 当前同为 `4c2994e`,本地 `main` 仍停留在 `84b40a2`,因此本轮按 skill 规则选择更新的 `origin/main` 作为 baseline。
- 当前 stop-condition metric
- `git ... diff --name-only origin/main...HEAD | wc -l`
- 结果:通过;当前 branch cumulative diff 为 `0 / 75`
- 热点扫描:
- `rg -n '\\[[^]]*(?:README\\.md|docs/[^]]+|GFramework\\.[^]]+/README\\.md|/zh-CN/[^]]+)\\]\\((?:README\\.md|docs/[^)]+|GFramework\\.[^)]+/README\\.md|/zh-CN/[^)]+)\\)' README.md docs GFramework.*/README.md`
- 结果:通过;当前仅在根 `README.md` 的内部支撑模块表格中命中 `2` 处路径式链接标签,适合作为本批次 reader-facing 修正切片。
- 英文标题扫描:
- `python3 - <<'PY' ...`(扫描 `docs/zh-CN/**/*.md` 中纯英文 `title` / H1
- 结果:通过;当前 landing / abstraction 页仍有一组纯英文入口标题,其中 `core/index.md``game/index.md``source-generators/index.md``abstractions/*.md``title` / H1 适合作为同批次标题本地化切片。
- 第二轮标题本地化:
- 同一轮扫描显示 `docs/zh-CN/core/architecture.md``command.md``events.md``logging.md``property.md``query.md` 仍保留纯英文 `title` / H1这些页面只需做中英对照标题调整不涉及正文结构或链接变更适合作为后续同批次切片。
- 停批判断:
- 再次扫描后,剩余纯英文 `title` 只剩 `docs/zh-CN/core/cqrs.md``CQRS``docs/zh-CN/index.md``GFramework`;它们属于缩写或品牌名,不再作为当前 reader-facing 本地化批次的优先对象。
- 构建验证:
- README 链接校验:
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-links.sh` 逐个验证 5 个目标 README
- 结果:通过;目标链接有效。
- README 标签复扫:
- `rg -n '\\[[^\\]]*(README\\.md|\\.md|\\.md/|/zh-CN/[^\\]]*)\\]\\([^)]*\\)' GFramework.Core/README.md GFramework.Core.SourceGenerators/README.md GFramework.Cqrs.SourceGenerators/README.md GFramework.Ecs.Arch/README.md GFramework.Game.SourceGenerators/README.md`
- 结果:无命中;本轮目标 README 已无可见路径式 / 文件名式标签残留。
- `Core` 校验:
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/core`
- 结果:通过;`Core` 栏目 frontmatter、链接与代码块校验通过。
- 教程校验:
- `bash .agents/skills/gframework-doc-refresh/scripts/validate-all.sh docs/zh-CN/tutorials/basic`
- 结果:通过;基础教程栏目 frontmatter、链接与代码块校验通过。
- 站点构建:
- `bun run build`(工作目录:`docs/`
- 结果:通过;`README.md` 链接标签修正与 `docs/zh-CN` 标题本地化后站点仍可构建,仅保留既有大 chunk warning。
- 结果:通过;仅保留既有大 chunk warning。
### 归档指针
@ -46,5 +43,5 @@
### 下一步
1. 提交当前批次,保留根 `README.md` 入口标签修正、`docs/zh-CN` 标题本地化和 `ai-plan` 恢复点同步更新
2. 若继续下一轮 `$gframework-batch-boot 75`,优先重新扫描剩余 README / landing page 的路径式链接标签和内部治理措辞,而不是继续本地化品牌名或缩写标题
1. 若继续下一轮 `$gframework-batch-boot 50`,优先重新抓取 `$gframework-pr-review`,再选择新的低风险 reader-facing 文档切片
2. 当前 branch diff 为 `21 / 50`,后续单批次仍应控制在剩余 `29` 个 changed files 的 headroom 内

View File

@ -96,8 +96,8 @@ public sealed class DiagnosticsFeature
## 阅读顺序
1. 先读本页,确认你是否真的只需要契约层
2. 再看 [`../core/index.md`](../core/index.md) 了解默认运行时怎么组织这些契约
2. 再看 [Core 模块总览](../core/index.md) 了解默认运行时怎么组织这些契约
3. 回到模块 README
- [`GFramework.Core.Abstractions README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md)
- [`GFramework.Core README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)
4. 需要统一导航时,再看 [`../api-reference/index.md`](../api-reference/index.md)
- [Core 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md)
- [Core 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)
4. 需要统一导航时,再看 [API 参考](../api-reference/index.md)

View File

@ -88,7 +88,7 @@ var options = new ArchOptions
## 阅读顺序
1. 先读本页,确认你是否真的只需要契约层
2. 如果需要默认实现,再看 [`../ecs/arch.md`](../ecs/arch.md)
2. 如果需要默认实现,再看 [Arch ECS 集成](../ecs/arch.md)
3. 回到对应模块 README
- [`GFramework.Ecs.Arch.Abstractions README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch.Abstractions/README.md)
- [`GFramework.Ecs.Arch README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch/README.md)
- [ECS 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch.Abstractions/README.md)
- [Ecs.Arch 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch/README.md)

View File

@ -113,9 +113,9 @@ public sealed class ContinueGameCommandHandler
3. 继续读具体专题页:
- [`../game/config-system.md`](../game/config-system.md)
- [`../game/data.md`](../game/data.md)
- [`../game/setting.md`](../game/setting.md)
- [`../game/scene.md`](../game/scene.md)
- [`../game/ui.md`](../game/ui.md)
- [设置系统](../game/setting.md)
- [场景系统](../game/scene.md)
- [UI 系统](../game/ui.md)
4. 需要仓库侧入口时,回到:
- [`GFramework.Game.Abstractions README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
- [`GFramework.Game README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)
- [Game 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)
- [Game 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)

View File

@ -20,8 +20,8 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
先读模块 README再读对应栏目入口页
- 入门入口:[`../getting-started/index.md`](../getting-started/index.md)
- 根模块地图:仓库根 [`README.md`](https://github.com/GeWuYou/GFramework/blob/main/README.md)
- 入门入口:[入门指南](../getting-started/index.md)
- 根模块地图:[仓库总览](https://github.com/GeWuYou/GFramework/blob/main/README.md)
### 想确认“这个功能属于哪个模块”
@ -29,11 +29,11 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
| 模块族 | 模块 README | 站内入口 | XML 文档关注点 |
| --- | --- | --- | --- |
| `Core` / `Core.Abstractions` | [`GFramework.Core README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)、[`GFramework.Core.Abstractions README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md) | [`Core 栏目`](../core/index.md)、[`Core 抽象层说明`](../abstractions/core-abstractions.md) | 架构入口、生命周期、命令 / 查询 / 事件 / 状态 / 资源 / 日志 / 配置 / 并发契约 |
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | [`GFramework.Cqrs README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs/README.md)、[`GFramework.Cqrs.Abstractions README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.Abstractions/README.md)、[`GFramework.Cqrs.SourceGenerators README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.SourceGenerators/README.md) | [`CQRS 栏目`](../core/cqrs.md)、[`CQRS Handler Registry 生成器`](../source-generators/cqrs-handler-registry-generator.md) | request / notification / handler / pipeline / registry / fallback contract |
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | [`GFramework.Game README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)、[`GFramework.Game.Abstractions README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)、[`GFramework.Game.SourceGenerators README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.SourceGenerators/README.md) | [`Game 模块总览`](../game/index.md)、[`Game 抽象层说明`](../abstractions/game-abstractions.md) | 配置、数据、设置、场景、UI、存储、序列化契约 |
| `Godot` / `Godot.SourceGenerators` | [`GFramework.Godot README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot/README.md)、[`GFramework.Godot.SourceGenerators README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md) | [`Godot 模块总览`](../godot/index.md)、[`Godot 项目生成器`](../source-generators/godot-project-generator.md)、[`GetNode 生成器`](../source-generators/get-node-generator.md)、[`BindNodeSignal 生成器`](../source-generators/bind-node-signal-generator.md) | 节点扩展、场景 / UI 适配、配置 / 存储 / 设置接线、Godot 生成器入口 |
| `Ecs.Arch` / `Ecs.Arch.Abstractions` | [`GFramework.Ecs.Arch README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch/README.md)、[`GFramework.Ecs.Arch.Abstractions README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch.Abstractions/README.md) | [`ECS 模块总览`](../ecs/index.md)、[`Arch ECS 集成`](../ecs/arch.md)、[`Ecs.Arch 抽象层说明`](../abstractions/ecs-arch-abstractions.md) | ECS 模块契约、系统适配、配置对象和运行时装配边界 |
| `Core` / `Core.Abstractions` | [Core 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)、[Core 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md) | [Core 栏目](../core/index.md)、[Core 抽象层说明](../abstractions/core-abstractions.md) | 架构入口、生命周期、命令 / 查询 / 事件 / 状态 / 资源 / 日志 / 配置 / 并发契约 |
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | [CQRS 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs/README.md)、[CQRS 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.Abstractions/README.md)、[CQRS 源码生成器说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs.SourceGenerators/README.md) | [CQRS 栏目](../core/cqrs.md)、[CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md) | request / notification / handler / pipeline / registry / fallback contract |
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | [Game 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game/README.md)、[Game 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.Abstractions/README.md)、[Game 源码生成器说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Game.SourceGenerators/README.md) | [Game 模块总览](../game/index.md)、[Game 抽象层说明](../abstractions/game-abstractions.md) | 配置、数据、设置、场景、UI、存储、序列化契约 |
| `Godot` / `Godot.SourceGenerators` | [Godot 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot/README.md)、[Godot 源码生成器说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Godot.SourceGenerators/README.md) | [Godot 模块总览](../godot/index.md)、[Godot 项目生成器](../source-generators/godot-project-generator.md)、[GetNode 生成器](../source-generators/get-node-generator.md)、[BindNodeSignal 生成器](../source-generators/bind-node-signal-generator.md) | 节点扩展、场景 / UI 适配、配置 / 存储 / 设置接线、Godot 生成器入口 |
| `Ecs.Arch` / `Ecs.Arch.Abstractions` | [Ecs.Arch 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch/README.md)、[ECS 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Ecs.Arch.Abstractions/README.md) | [ECS 模块总览](../ecs/index.md)、[Arch ECS 集成](../ecs/arch.md)、[Ecs.Arch 抽象层说明](../abstractions/ecs-arch-abstractions.md) | ECS 模块契约、系统适配、配置对象和运行时装配边界 |
## 先看 XML还是先看教程
@ -57,9 +57,9 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
优先入口:
- 教程概览:[`../tutorials/index.md`](../tutorials/index.md)
- 最佳实践:[`../best-practices/index.md`](../best-practices/index.md)
- 故障排查:[`../troubleshooting.md`](../troubleshooting.md)
- 教程概览:[教程总览](../tutorials/index.md)
- 最佳实践:[最佳实践](../best-practices/index.md)
- 故障排查:[故障排查](../troubleshooting.md)
## 当前边界

View File

@ -13,7 +13,7 @@ description: 说明 GFramework.Core 的 Architecture 入口、生命周期职责
当前版本的 `Architecture` 已经是协调器外观。对外仍保留稳定的注册与生命周期 API但内部职责已经拆给专门协作者处理。
## 你真正会用到的公开入口
## 常用公开入口
最常见的成员只有这些:
@ -158,9 +158,9 @@ public sealed class MetricsHook : IArchitectureLifecycleHook
}
```
## 什么时候看别的页面
## 相关主题
- 想看上下文 API转到 [context](./context.md)
- 想看阶段和销毁语义:转到 [lifecycle](./lifecycle.md)
- 想看旧命令 / 查询兼容层:转到 [command](./command.md) 和 [query](./query.md)
- 想看推荐的新请求模型:转到 [cqrs](./cqrs.md)
- 上下文访问与 `ArchitectureContext`:阅读[上下文](./context.md)
- 初始化阶段、事件与销毁流程:阅读[生命周期](./lifecycle.md)
- 旧版命令 / 查询执行器兼容入口:阅读[命令执行](./command.md)与[查询执行](./query.md)
- 新项目的请求 / 通知模型:阅读[CQRS 运行时](./cqrs.md)

View File

@ -706,7 +706,7 @@ if (volume == 0) volume = 1.0f; // 需要额外的检查
将配置文件按环境和用途分类:
```
```text
config/
├── default.json # 默认配置
├── dev.json # 开发环境配置

View File

@ -132,7 +132,7 @@ protected override void OnInitialize()
4. 如果程序集带有 `CqrsReflectionFallbackAttribute`,只补扫剩余 handler
5. 同一程序集按稳定键去重,避免重复注册
`Cqrs.SourceGenerators` 的专题入口见 [../source-generators/cqrs-handler-registry-generator.md](../source-generators/cqrs-handler-registry-generator.md)。
`Cqrs.SourceGenerators` 的专题入口见[CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)。
## Pipeline Behavior
@ -185,5 +185,5 @@ RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
- 架构入口:[architecture](./architecture.md)
- 上下文入口:[context](./context.md)
- 生成器专题:[../source-generators/cqrs-handler-registry-generator.md](../source-generators/cqrs-handler-registry-generator.md)
- 模块 README[`GFramework.Cqrs README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs/README.md)
- 生成器专题:[CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)
- 模块说明:[CQRS 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Cqrs/README.md)

View File

@ -86,7 +86,7 @@ public class GameModel : AbstractModel, IContextAware
#### SendCommand 扩展方法
```
```csharp
// 发送无返回值的命令
public static void SendCommand(this IContextAware contextAware, ICommand command)
@ -96,7 +96,7 @@ public static TResult SendCommand<TResult>(this IContextAware contextAware, ICom
**使用示例:**
```
```csharp
public class GameController : IController
{
public void OnStartButtonClicked()
@ -112,13 +112,13 @@ public class GameController : IController
#### SendQuery 扩展方法
```
```csharp
public static TResult SendQuery<TResult>(this IContextAware contextAware, IQuery<TResult> query)
```
**使用示例:**
```
```csharp
public class InventoryController : IController
{
public void ShowInventory()
@ -134,7 +134,7 @@ public class InventoryController : IController
#### SendEvent 扩展方法
```
```csharp
// 发送无参事件
public static void SendEvent<T>(this IContextAware contextAware) where T : new()
@ -144,7 +144,7 @@ public static void SendEvent<T>(this IContextAware contextAware, T e) where T :
**使用示例:**
```
```csharp
public class PlayerModel : AbstractModel, IContextAware
{
public void TakeDamage(int damage)
@ -169,13 +169,13 @@ public class PlayerModel : AbstractModel, IContextAware
#### RegisterEvent 扩展方法
```
```csharp
public static IUnRegister RegisterEvent<TEvent>(this IContextAware contextAware, Action<TEvent> handler)
```
**使用示例:**
```
```csharp
public class GameController : IController
{
private IUnRegisterList _unregisterList = new UnRegisterList();
@ -197,13 +197,13 @@ public class GameController : IController
#### UnRegisterEvent 扩展方法
```
```csharp
public static void UnRegisterEvent<TEvent>(this IContextAware contextAware, Action<TEvent> onEvent)
```
### GetEnvironment 扩展方法
```
```csharp
public static T? GetEnvironment<T>(this IContextAware contextAware) where T : class
public static IEnvironment GetEnvironment(this IContextAware contextAware)
```
@ -214,7 +214,7 @@ public static IEnvironment GetEnvironment(this IContextAware contextAware)
#### IfType 扩展方法
```
```csharp
// 最简单的类型判断
public static bool IfType<T>(this object obj, Action<T> action)
@ -235,7 +235,7 @@ public static void IfType<T>(
**使用示例:**
```
```csharp
object obj = new MyRule();
// 简单类型判断
@ -259,7 +259,7 @@ obj.IfType<IRule>(
#### IfType`<T, TResult>` 扩展方法
```
```csharp
public static TResult? IfType<T, TResult>(
this object obj,
Func<T, TResult> func
@ -268,7 +268,7 @@ public static TResult? IfType<T, TResult>(
**使用示例:**
```
```csharp
object obj = new MyRule { Name = "TestRule" };
string? name = obj.IfType<MyRule, string>(r => r.Name);
@ -276,7 +276,7 @@ string? name = obj.IfType<MyRule, string>(r => r.Name);
#### As 和 Do 扩展方法
```
```csharp
// 安全类型转换
public static T? As<T>(this object obj) where T : class
@ -286,7 +286,7 @@ public static T Do<T>(this T obj, Action<T> action)
**使用示例:**
```
```csharp
// 安全类型转换
obj.As<MyRule>()
?.Execute();
@ -307,7 +307,7 @@ obj.As<MyRule>()
#### SwitchType 扩展方法
```
```csharp
public static void SwitchType(
this object obj,
params (Type type, Action<object> action)[] handlers
@ -316,7 +316,7 @@ public static void SwitchType(
**使用示例:**
```
```csharp
obj.SwitchType(
(typeof(IRule), o => HandleRule((IRule)o)),
(typeof(ISystem), o => HandleSystem((ISystem)o)),
@ -330,13 +330,13 @@ obj.SwitchType(
#### OrEventExtensions
```
```csharp
public static OrEvent Or(this IEvent self, IEvent e)
```
**使用示例:**
```
```csharp
// 组合多个事件:当任意一个触发时执行
var onAnyInput = onKeyPressed.Or(onMouseClicked).Or(onTouchDetected);
@ -357,7 +357,7 @@ var onAnyDamage = onPhysicalDamage
#### UnRegisterListExtension
```
```csharp
// 添加到注销列表
public static void AddToUnregisterList(this IUnRegister self,
IUnRegisterList unRegisterList)
@ -368,7 +368,7 @@ public static void UnRegisterAll(this IUnRegisterList self)
**使用示例:**
```
```csharp
public class ComplexController : IController
{
private IUnRegisterList _unregisterList = new UnRegisterList();
@ -401,7 +401,7 @@ public class ComplexController : IController
### Controller 示例
```
```csharp
public partial class GameplayController : IController
{
private IUnRegisterList _unregisterList = new UnRegisterList();
@ -453,7 +453,7 @@ public partial class GameplayController : IController
### Command 示例
```
```csharp
public class ComplexGameCommand : AbstractCommand
{
protected override void OnExecute()
@ -478,7 +478,7 @@ public class ComplexGameCommand : AbstractCommand
### System 示例
```
```csharp
public class AchievementSystem : AbstractSystem
{
protected override void OnInit()

View File

@ -10,7 +10,7 @@ description: GFramework.Core 与 GFramework.Core.Abstractions 的运行时入口
如果你第一次接入框架,建议先把这里当作“运行时底座说明”,再按需进入 `Game``Godot` 或 Source Generators 栏目。
## 先理解包关系
## 模块与包关系
- `GeWuYou.GFramework.Core`
- 基础运行时实现,包含 `Architecture`、上下文、生命周期、事件、属性、状态、资源、日志、协程、IoC 等能力。
@ -30,48 +30,48 @@ dotnet add package GeWuYou.GFramework.Core
dotnet add package GeWuYou.GFramework.Core.Abstractions
```
## 这个栏目应该回答什么
## 栏目覆盖范围
`Core` 栏目不是旧版“完整框架教程”的镜像,而是当前实现的入口导航。这里的页面按能力域组织:
- 架构与生命周期
- [architecture](./architecture.md)
- [context](./context.md)
- [lifecycle](./lifecycle.md)
- [async-initialization](./async-initialization.md)
- [架构](./architecture.md)
- [上下文](./context.md)
- [生命周期](./lifecycle.md)
- [异步初始化](./async-initialization.md)
- 组件角色与运行时接入
- [model](./model.md)
- [system](./system.md)
- [utility](./utility.md)
- [environment](./environment.md)
- [extensions](./extensions.md)
- [模型](./model.md)
- [系统](./system.md)
- [工具](./utility.md)
- [环境](./environment.md)
- [扩展方法](./extensions.md)
- 旧版命令 / 查询执行器与迁移入口
- [command](./command.md)
- [query](./query.md)
- [cqrs](./cqrs.md)
- [命令执行](./command.md)
- [查询执行](./query.md)
- [CQRS 运行时](./cqrs.md)
- 状态、事件与规则
- [events](./events.md)
- [property](./property.md)
- [rule](./rule.md)
- [logging](./logging.md)
- [state-machine](./state-machine.md)
- [state-management](./state-management.md)
- [事件系统](./events.md)
- [可绑定属性](./property.md)
- [规则系统](./rule.md)
- [日志系统](./logging.md)
- [状态机](./state-machine.md)
- [状态管理](./state-management.md)
- 运行时支撑能力
- [resource](./resource.md)
- [pool](./pool.md)
- [coroutine](./coroutine.md)
- [pause](./pause.md)
- [localization](./localization.md)
- [configuration](./configuration.md)
- [ioc](./ioc.md)
- [资源管理](./resource.md)
- [对象池](./pool.md)
- [协程系统](./coroutine.md)
- [暂停系统](./pause.md)
- [本地化](./localization.md)
- [配置管理](./configuration.md)
- [IoC 容器](./ioc.md)
- 通用辅助能力
- [functional](./functional.md)
- [函数式辅助](./functional.md)
## XML 与 API 阅读入口
如果你已经知道模块归属,但想确认公开类型的契约边界,建议按下面顺序阅读:
1. 先看模块 README [`GFramework.Core`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md),确认包关系和目录边界
1. 先看[Core 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md),确认包关系和目录边界
2. 再看本栏目对应专题页,确认采用顺序、生命周期与推荐接线方式
3. 最后回到源码中的 XML 文档,重点核对这些类型族:
- `Architecture` / `IArchitectureContext`
@ -80,7 +80,7 @@ dotnet add package GeWuYou.GFramework.Core.Abstractions
- `IResourceManager` / `IConfigurationManager`
- `IAsyncKeyLockManager` / `ITimeProvider`
统一入口见 [`../api-reference/index.md`](../api-reference/index.md)。
统一入口见[API 参考](../api-reference/index.md)。
## 源码阅读入口
@ -130,25 +130,25 @@ public sealed class CounterArchitecture : Architecture
- 只需要基础架构、事件、日志、资源、协程:
- 先停留在 `Core`
- 要写新的请求/通知处理流:
- 优先阅读 [cqrs](./cqrs.md)
- 优先阅读[CQRS 运行时](./cqrs.md)
- 要接入游戏内容配置、设置、数据仓库、Scene 或 UI
- 转到 [Game](../game/index.md)
- 转到[Game 模块](../game/index.md)
- 要接入 Godot 节点、场景和项目元数据生成:
- 转到 [Godot](../godot/index.md) 与 [Source Generators](../source-generators/index.md) 栏目
- 转到[Godot 模块](../godot/index.md)与[源码生成器](../source-generators/index.md)栏目
## 推荐阅读顺序
## 阅读顺序
1. [快速开始](../getting-started/quick-start.md)
2. [architecture](./architecture.md)
3. [context](./context.md)
4. [lifecycle](./lifecycle.md)
5. [cqrs](./cqrs.md)
2. [架构](./architecture.md)
3. [上下文](./context.md)
4. [生命周期](./lifecycle.md)
5. [CQRS 运行时](./cqrs.md)
之后再按实际需要进入具体专题页,而不是把 `Core` 当成一次性读完的大杂烩。
## 对应模块入口
- [`GFramework.Core README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)
- [`GFramework.Core.Abstractions README`](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md)
- [Core 运行时说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core/README.md)
- [Core 抽象层说明](https://github.com/GeWuYou/GFramework/blob/main/GFramework.Core.Abstractions/README.md)
- [API 参考入口](../api-reference/index.md)
- 仓库根 [`README.md`](https://github.com/GeWuYou/GFramework/blob/main/README.md)
- [仓库总览](https://github.com/GeWuYou/GFramework/blob/main/README.md)

View File

@ -274,7 +274,7 @@ var sortedSystems = container.GetAllSorted<ISystem>((a, b) =>
## IoC 容器架构
```
```text
Architecture (架构层)
├── IocContainer (IoC容器)
│ ├── Register<T>() // 泛型注册
@ -341,7 +341,7 @@ public abstract class Architecture : IArchitecture
### 注册组件到容器
```
```csharp
public class GameArchitecture : Architecture
{
protected override void Init()
@ -365,7 +365,7 @@ public class GameArchitecture : Architecture
### 从容器获取组件
```
```csharp
// 通过扩展方法间接使用 IoC 容器
public class PlayerController : IController
{
@ -385,7 +385,7 @@ public class PlayerController : IController
### 内部实现
```
```csharp
public class IocContainer
{
// 使用字典存储类型到实例集合的映射
@ -450,7 +450,7 @@ public class IocContainer
### 注册流程
```
```text
用户代码
RegisterSystem<T>(system)
@ -462,7 +462,7 @@ IocContainer.Register<T>(system)
### 获取流程
```
```text
用户代码
this.GetSystem<T>()
@ -480,7 +480,7 @@ IocContainer.Get<T>()
### 基础使用
```
```csharp
// 1. 创建容器
var container = new IocContainer();
@ -495,7 +495,7 @@ service.DoSomething();
### 接口和实现分离
```
```csharp
// 定义接口
public interface IDataService
{
@ -532,7 +532,7 @@ dataService.SaveData("game data");
### 注册多个实现
```
```csharp
var container = new IocContainer();
// 注册多个相同接口的不同实现
@ -552,7 +552,7 @@ var allServices = container.GetAll<IDataService>(); // 返回两个实例的列
检查容器中是否包含指定类型的实例。
```
```csharp
public bool Contains<T>() where T : class
```
@ -566,7 +566,7 @@ public bool Contains<T>() where T : class
**使用示例:**
```
```csharp
var container = new IocContainer();
// 检查服务是否已注册
@ -592,7 +592,7 @@ if (!container.Contains<ISettingsService>())
判断容器中是否包含某个具体的实例对象。
```
```csharp
public bool ContainsInstance(object instance)
```
@ -606,7 +606,7 @@ public bool ContainsInstance(object instance)
**使用示例:**
```
```csharp
var container = new IocContainer();
var service = new MyService();
@ -636,13 +636,13 @@ if (!container.ContainsInstance(anotherService))
清空容器中的所有实例。
```
```csharp
public void Clear()
```
**使用示例:**
```
```csharp
var container = new IocContainer();
// 注册多个服务
@ -711,7 +711,7 @@ Console.WriteLine($"Contains IService2: {container.Contains<IService2>()}"); //
### 本框架的 IocContainer
```
```csharp
// 简单直接
var container = new IocContainer();
container.Register(new MyService());
@ -730,7 +730,7 @@ var service = container.Get<MyService>();
### 完整的 IoC 框架(如 Autofac、Zenject
```
```csharp
// 复杂但功能强大
var builder = new ContainerBuilder();
builder.RegisterType<MyService>().As<IMyService>().SingleInstance();
@ -753,7 +753,7 @@ var controller = container.Resolve<MyController>();
### 1. 在架构初始化时注册
```
```csharp
public class GameArchitecture : Architecture
{
protected override void Init()
@ -776,7 +776,7 @@ public class GameArchitecture : Architecture
### 2. 使用接口类型注册
```
```csharp
// ❌ 不推荐:直接使用实现类
RegisterSystem(new ConcreteSystem());
var system = GetSystem<ConcreteSystem>();
@ -788,7 +788,7 @@ var system = GetSystem<IGameSystem>();
### 3. 避免运行时频繁注册
```
```csharp
// ❌ 不好:游戏运行时频繁注册
void Update()
{
@ -804,7 +804,7 @@ protected override void Init()
### 4. 检查 null 返回值
```
```csharp
// 获取可能不存在的服务
var service = container.Get<IOptionalService>();
if (service != null)
@ -819,7 +819,7 @@ else
### 5. 合理使用容器冻结
```
```csharp
// 在架构初始化完成后冻结容器,防止意外修改
protected override void OnInit()
{

Some files were not shown because too many files have changed in this diff Show More