From d0b4946bbad5dad9c02cc42c30d1efa7d0eb83c4 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 17 Apr 2026 07:40:36 +0800 Subject: [PATCH 1/7] =?UTF-8?q?test:=20=E5=A2=9E=E5=BC=BA=E5=BF=AB?= =?UTF-8?q?=E7=85=A7=E6=B5=8B=E8=AF=95=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD?= =?UTF-8?q?=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=AE=89=E5=85=A8=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E5=92=8C=E8=A6=86=E7=9B=96=E7=8E=87=E6=94=B9=E8=BF=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 EnumExtensionsGeneratorSnapshotTests.cs 中补充 snapshotFileNameSelector 的 null 分支覆盖,新增默认快照文件名选择器用例及对应快照资产 - 强化 GeneratorSnapshotTest.cs 的快照路径校验,拒绝空白文件名、绝对路径和目录遍历攻击;将辅助器改为通过 Roslyn GeneratorDriver 读取真实生成结果并验证编译,消除仅依赖 TestState.GeneratedSources 导致的空跑风险 - 新增 GeneratorSnapshotTestSecurityTests.cs 安全回归测试,覆盖绝对路径拒绝和目录逃逸防护两个分支 - 将 Priority、Logger、ContextAware 三组生成器测试统一指向仓库内快照目录,并补齐缺失的快照资产以支持现在强制执行的生成验证 --- .../Bases/PriorityGeneratorSnapshotTests.cs | 39 ++++---- .../TestApp_MySystem.Priority.g.cs | 12 +++ .../TestApp_GenericSystem_T_.Priority.g.cs | 12 +++ .../TestApp_CriticalSystem.Priority.g.cs | 12 +++ .../TestApp_HighPrioritySystem.Priority.g.cs | 12 +++ .../Core/GeneratorSnapshotTest.cs | 92 ++++++++++++++++--- .../GeneratorSnapshotTestSecurityTests.cs | 91 ++++++++++++++++++ .../EnumExtensionsGeneratorSnapshotTests.cs | 20 ++++ .../Status.EnumExtensions.g.cs | 21 +++++ .../Logging/LoggerGeneratorSnapshotTests.cs | 53 +++++------ .../MyService.Logger.g.cs | 11 +++ .../CustomName_Class/MyService.Logger.g.cs | 11 +++ .../MyService.Logger.g.cs | 11 +++ .../GenericClass/MyService.Logger.g.cs | 11 +++ .../InstanceField_Class/MyService.Logger.g.cs | 11 +++ .../PublicField_Class/MyService.Logger.g.cs | 11 +++ .../ContextAwareGeneratorSnapshotTests.cs | 15 ++- .../MyRule.ContextAware.g.cs | 55 +++++++++++ 18 files changed, 435 insertions(+), 65 deletions(-) create mode 100644 GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/BasicPriority/TestApp_MySystem.Priority.g.cs create mode 100644 GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/GenericClass/TestApp_GenericSystem_T_.Priority.g.cs create mode 100644 GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/NegativePriority/TestApp_CriticalSystem.Priority.g.cs create mode 100644 GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/PriorityGroup/TestApp_HighPrioritySystem.Priority.g.cs create mode 100644 GFramework.SourceGenerators.Tests/Core/GeneratorSnapshotTestSecurityTests.cs create mode 100644 GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods_DefaultSnapshotFileNameSelector/Status.EnumExtensions.g.cs create mode 100644 GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/CustomFieldName_Class/MyService.Logger.g.cs create mode 100644 GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/CustomName_Class/MyService.Logger.g.cs create mode 100644 GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/DefaultConfiguration_Class/MyService.Logger.g.cs create mode 100644 GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/GenericClass/MyService.Logger.g.cs create mode 100644 GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/InstanceField_Class/MyService.Logger.g.cs create mode 100644 GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/PublicField_Class/MyService.Logger.g.cs create mode 100644 GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs diff --git a/GFramework.SourceGenerators.Tests/Bases/PriorityGeneratorSnapshotTests.cs b/GFramework.SourceGenerators.Tests/Bases/PriorityGeneratorSnapshotTests.cs index 79da21bb..be019c6f 100644 --- a/GFramework.SourceGenerators.Tests/Bases/PriorityGeneratorSnapshotTests.cs +++ b/GFramework.SourceGenerators.Tests/Bases/PriorityGeneratorSnapshotTests.cs @@ -50,12 +50,7 @@ public class PriorityGeneratorSnapshotTests await GeneratorSnapshotTest.RunAsync( source, - Path.Combine( - TestContext.CurrentContext.TestDirectory, - "bases", - "snapshots", - "PriorityGenerator", - "BasicPriority")); + GetSnapshotFolder("BasicPriority")); } /// @@ -98,12 +93,7 @@ public class PriorityGeneratorSnapshotTests await GeneratorSnapshotTest.RunAsync( source, - Path.Combine( - TestContext.CurrentContext.TestDirectory, - "bases", - "snapshots", - "PriorityGenerator", - "NegativePriority")); + GetSnapshotFolder("NegativePriority")); } /// @@ -156,12 +146,7 @@ public class PriorityGeneratorSnapshotTests await GeneratorSnapshotTest.RunAsync( source, - Path.Combine( - TestContext.CurrentContext.TestDirectory, - "bases", - "snapshots", - "PriorityGenerator", - "PriorityGroup")); + GetSnapshotFolder("PriorityGroup")); } /// @@ -204,11 +189,25 @@ public class PriorityGeneratorSnapshotTests await GeneratorSnapshotTest.RunAsync( source, + GetSnapshotFolder("GenericClass")); + } + + /// + /// 将运行时测试目录映射回仓库内已提交的 Priority 生成器快照目录。 + /// + /// 快照场景名称。 + /// 场景对应的绝对快照目录。 + private static string GetSnapshotFolder(string scenarioName) + { + return Path.GetFullPath( Path.Combine( TestContext.CurrentContext.TestDirectory, - "bases", + "..", + "..", + "..", + "Bases", "snapshots", "PriorityGenerator", - "GenericClass")); + scenarioName)); } } diff --git a/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/BasicPriority/TestApp_MySystem.Priority.g.cs b/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/BasicPriority/TestApp_MySystem.Priority.g.cs new file mode 100644 index 00000000..2a6756fe --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/BasicPriority/TestApp_MySystem.Priority.g.cs @@ -0,0 +1,12 @@ +// +#nullable enable + +namespace TestApp; + +partial class MySystem : global::GFramework.Core.Abstractions.Bases.IPrioritized +{ + /// + /// 获取优先级值: 10 + /// + public int Priority => 10; +} \ No newline at end of file diff --git a/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/GenericClass/TestApp_GenericSystem_T_.Priority.g.cs b/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/GenericClass/TestApp_GenericSystem_T_.Priority.g.cs new file mode 100644 index 00000000..a0ce6997 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/GenericClass/TestApp_GenericSystem_T_.Priority.g.cs @@ -0,0 +1,12 @@ +// +#nullable enable + +namespace TestApp; + +partial class GenericSystem : global::GFramework.Core.Abstractions.Bases.IPrioritized +{ + /// + /// 获取优先级值: 20 + /// + public int Priority => 20; +} \ No newline at end of file diff --git a/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/NegativePriority/TestApp_CriticalSystem.Priority.g.cs b/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/NegativePriority/TestApp_CriticalSystem.Priority.g.cs new file mode 100644 index 00000000..bd6b4af3 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/NegativePriority/TestApp_CriticalSystem.Priority.g.cs @@ -0,0 +1,12 @@ +// +#nullable enable + +namespace TestApp; + +partial class CriticalSystem : global::GFramework.Core.Abstractions.Bases.IPrioritized +{ + /// + /// 获取优先级值: -100 + /// + public int Priority => -100; +} \ No newline at end of file diff --git a/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/PriorityGroup/TestApp_HighPrioritySystem.Priority.g.cs b/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/PriorityGroup/TestApp_HighPrioritySystem.Priority.g.cs new file mode 100644 index 00000000..737acd1a --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/PriorityGroup/TestApp_HighPrioritySystem.Priority.g.cs @@ -0,0 +1,12 @@ +// +#nullable enable + +namespace TestApp; + +partial class HighPrioritySystem : global::GFramework.Core.Abstractions.Bases.IPrioritized +{ + /// + /// 获取优先级值: -50 + /// + public int Priority => -50; +} \ No newline at end of file diff --git a/GFramework.SourceGenerators.Tests/Core/GeneratorSnapshotTest.cs b/GFramework.SourceGenerators.Tests/Core/GeneratorSnapshotTest.cs index fcc0df3e..f464cb74 100644 --- a/GFramework.SourceGenerators.Tests/Core/GeneratorSnapshotTest.cs +++ b/GFramework.SourceGenerators.Tests/Core/GeneratorSnapshotTest.cs @@ -21,25 +21,45 @@ public static class GeneratorSnapshotTest string snapshotFolder, Func? snapshotFileNameSelector = null) { - var test = new CSharpSourceGeneratorTest - { - TestState = - { - Sources = { source } - }, - TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck, - DisabledDiagnostics = { "GF_Common_Trace_001" } - }; + var syntaxTree = CSharpSyntaxTree.ParseText(source); + var compilation = CSharpCompilation.Create( + $"{typeof(TGenerator).Name}SnapshotTests", + [syntaxTree], + MetadataReferenceTestBuilder.GetRuntimeMetadataReferences(), + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + GeneratorDriver driver = CSharpGeneratorDriver.Create( + generators: [CreateGenerator()], + parseOptions: (CSharpParseOptions)syntaxTree.Options); + driver = driver.RunGeneratorsAndUpdateCompilation( + compilation, + out var updatedCompilation, + out _); - await test.RunAsync(); + var compilationErrors = updatedCompilation.GetDiagnostics() + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + Assert.That( + compilationErrors, + Is.Empty, + () => + $"编译生成的代码时出现错误:{Environment.NewLine}{string.Join(Environment.NewLine, compilationErrors.Select(static diagnostic => diagnostic.ToString()))}"); - var generated = test.TestState.GeneratedSources; + var runResult = driver.GetRunResult(); + var generated = runResult.Results + .SelectMany(static result => result.GeneratedSources) + .OrderBy(static source => source.HintName, StringComparer.Ordinal) + .Select(static source => (filename: source.HintName, content: source.SourceText.ToString())) + .ToArray(); + Assert.That( + generated, + Is.Not.Empty, + $"Generator '{typeof(TGenerator).FullName}' did not produce any sources."); foreach (var (filename, content) in generated) { // 不同测试套件可能需要将生成文件映射到非 .cs 快照,以避免测试资产被当作可编译源码参与构建。 var snapshotFileName = snapshotFileNameSelector?.Invoke(filename) ?? filename; - var path = Path.Combine( + var path = ResolveSnapshotPath( snapshotFolder, snapshotFileName); @@ -71,4 +91,52 @@ public static class GeneratorSnapshotTest { return text.Replace("\r\n", "\n").Trim(); } + + /// + /// 创建可由 Roslyn 驱动直接执行的源生成器实例,并统一兼容经典与增量生成器。 + /// + /// 适配后的源生成器实例。 + /// 当测试类型既不是源生成器也不是增量生成器时抛出。 + private static ISourceGenerator CreateGenerator() + { + var generator = new TGenerator(); + return generator switch + { + ISourceGenerator sourceGenerator => sourceGenerator, + IIncrementalGenerator incrementalGenerator => incrementalGenerator.AsSourceGenerator(), + _ => throw new InvalidOperationException( + $"Generator type '{typeof(TGenerator).FullName}' must implement {nameof(ISourceGenerator)} or {nameof(IIncrementalGenerator)}.") + }; + } + + /// + /// 解析并验证快照路径,确保文件名映射不会逃逸出当前快照根目录。 + /// + /// 快照根目录。 + /// 映射后的快照文件名。 + /// 可安全访问的快照绝对路径。 + /// + /// 当映射结果为空白、为绝对路径,或通过相对路径越界到快照目录之外时抛出。 + /// + private static string ResolveSnapshotPath(string snapshotFolder, string snapshotFileName) + { + if (string.IsNullOrWhiteSpace(snapshotFileName) || Path.IsPathRooted(snapshotFileName)) + { + throw new InvalidOperationException($"Invalid snapshot file name: {snapshotFileName}"); + } + + // 先规范化根目录再做包含关系判断,避免 `..` 或平台大小写差异导致的目录逃逸。 + var snapshotRoot = Path.TrimEndingDirectorySeparator(Path.GetFullPath(snapshotFolder)); + var snapshotPath = Path.GetFullPath(Path.Combine(snapshotRoot, snapshotFileName)); + var comparison = OperatingSystem.IsWindows() + ? StringComparison.OrdinalIgnoreCase + : StringComparison.Ordinal; + + if (!snapshotPath.StartsWith(snapshotRoot + Path.DirectorySeparatorChar, comparison)) + { + throw new InvalidOperationException($"Snapshot path escapes root folder: {snapshotFileName}"); + } + + return snapshotPath; + } } diff --git a/GFramework.SourceGenerators.Tests/Core/GeneratorSnapshotTestSecurityTests.cs b/GFramework.SourceGenerators.Tests/Core/GeneratorSnapshotTestSecurityTests.cs new file mode 100644 index 00000000..b52fa027 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Core/GeneratorSnapshotTestSecurityTests.cs @@ -0,0 +1,91 @@ +using System.IO; +using GFramework.Core.SourceGenerators.Enums; + +namespace GFramework.SourceGenerators.Tests.Core; + +/// +/// 验证快照测试辅助器对快照文件路径映射的安全约束。 +/// +[TestFixture] +public class GeneratorSnapshotTestSecurityTests +{ + private const string EnumAttributeNamespace = "GFramework.Core.SourceGenerators.Abstractions.Enums"; + + /// + /// 验证快照文件名映射返回绝对路径时,会在访问文件系统前被拒绝。 + /// + [Test] + public void RunAsync_SnapshotFileNameSelectorReturnsAbsolutePath_ThrowsInvalidOperationException() + { + var snapshotRoot = CreateSnapshotRoot(); + var source = BuildSource(); + + Assert.ThrowsAsync(async () => + await GeneratorSnapshotTest.RunAsync( + source, + snapshotRoot, + _ => Path.Combine(snapshotRoot, "Status.EnumExtensions.g.cs"))); + } + + /// + /// 验证快照文件名映射尝试通过父级目录片段逃逸根目录时,会在访问文件系统前被拒绝。 + /// + [Test] + public void RunAsync_SnapshotFileNameSelectorEscapesSnapshotRoot_ThrowsInvalidOperationException() + { + var snapshotRoot = CreateSnapshotRoot(); + var source = BuildSource(); + + Assert.ThrowsAsync(async () => + await GeneratorSnapshotTest.RunAsync( + source, + snapshotRoot, + _ => Path.Combine("..", "escaped", "Status.EnumExtensions.g.cs"))); + } + + /// + /// 为安全测试创建隔离的快照根目录路径,避免不同用例共享状态。 + /// + /// 当前用例专属的快照根目录绝对路径。 + private static string CreateSnapshotRoot() + { + return Path.Combine( + TestContext.CurrentContext.WorkDirectory, + "temp-snapshots", + TestContext.CurrentContext.Test.ID, + Guid.NewGuid().ToString("N")); + } + + /// + /// 构造可稳定触发枚举扩展生成器输出的最小测试源码。 + /// + /// 包含测试属性与目标枚举的完整源码。 + private static string BuildSource() + { + return $$""" + using System; + + namespace {{EnumAttributeNamespace}} + { + [AttributeUsage(AttributeTargets.Enum)] + public sealed class GenerateEnumExtensionsAttribute : Attribute + { + public bool GenerateIsMethods { get; set; } = true; + public bool GenerateIsInMethod { get; set; } = true; + } + } + + namespace TestApp + { + using {{EnumAttributeNamespace}}; + + [GenerateEnumExtensions] + public enum Status + { + Active, + Inactive + } + } + """; + } +} diff --git a/GFramework.SourceGenerators.Tests/Enums/EnumExtensionsGeneratorSnapshotTests.cs b/GFramework.SourceGenerators.Tests/Enums/EnumExtensionsGeneratorSnapshotTests.cs index 459f4640..f5fa2157 100644 --- a/GFramework.SourceGenerators.Tests/Enums/EnumExtensionsGeneratorSnapshotTests.cs +++ b/GFramework.SourceGenerators.Tests/Enums/EnumExtensionsGeneratorSnapshotTests.cs @@ -34,6 +34,26 @@ public class EnumExtensionsGeneratorSnapshotTests GetSnapshotFileName); } + /// + /// 验证未提供快照文件名映射时,会直接按生成文件名进行快照比对。 + /// + [Test] + public async Task Snapshot_BasicEnum_IsMethods_DefaultSnapshotFileNameSelector() + { + var source = BuildSource( + """ + public enum Status + { + Active, + Inactive + } + """); + + await GeneratorSnapshotTest.RunAsync( + source, + GetSnapshotFolder("BasicEnum_IsMethods_DefaultSnapshotFileNameSelector")); + } + /// /// 验证默认配置在较小枚举上仍会生成集合判断方法。 /// diff --git a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods_DefaultSnapshotFileNameSelector/Status.EnumExtensions.g.cs b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods_DefaultSnapshotFileNameSelector/Status.EnumExtensions.g.cs new file mode 100644 index 00000000..2897d09c --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods_DefaultSnapshotFileNameSelector/Status.EnumExtensions.g.cs @@ -0,0 +1,21 @@ +// +using System; +namespace TestApp +{ + public static partial class StatusExtensions + { + /// 是否为 Active + public static bool IsActive(this TestApp.Status value) => value == TestApp.Status.Active; + + /// 是否为 Inactive + public static bool IsInactive(this TestApp.Status value) => value == TestApp.Status.Inactive; + + /// 判断是否属于指定集合 + public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values) + { + if (values == null) return false; + foreach (var v in values) if (value == v) return true; + return false; + } + } +} diff --git a/GFramework.SourceGenerators.Tests/Logging/LoggerGeneratorSnapshotTests.cs b/GFramework.SourceGenerators.Tests/Logging/LoggerGeneratorSnapshotTests.cs index 07bb60a2..83a11648 100644 --- a/GFramework.SourceGenerators.Tests/Logging/LoggerGeneratorSnapshotTests.cs +++ b/GFramework.SourceGenerators.Tests/Logging/LoggerGeneratorSnapshotTests.cs @@ -96,12 +96,7 @@ public class LoggerGeneratorSnapshotTests await GeneratorSnapshotTest.RunAsync( source, - Path.Combine( - TestContext.CurrentContext.TestDirectory, - "logging", - "snapshots", - "LoggerGenerator", - "DefaultConfiguration_Class")); + GetSnapshotFolder("DefaultConfiguration_Class")); } [Test] @@ -193,12 +188,7 @@ public class LoggerGeneratorSnapshotTests await GeneratorSnapshotTest.RunAsync( source, - Path.Combine( - TestContext.CurrentContext.TestDirectory, - "logging", - "snapshots", - "LoggerGenerator", - "CustomName_Class")); + GetSnapshotFolder("CustomName_Class")); } [Test] @@ -290,12 +280,7 @@ public class LoggerGeneratorSnapshotTests await GeneratorSnapshotTest.RunAsync( source, - Path.Combine( - TestContext.CurrentContext.TestDirectory, - "logging", - "snapshots", - "LoggerGenerator", - "CustomFieldName_Class")); + GetSnapshotFolder("CustomFieldName_Class")); } [Test] @@ -387,12 +372,7 @@ public class LoggerGeneratorSnapshotTests await GeneratorSnapshotTest.RunAsync( source, - Path.Combine( - TestContext.CurrentContext.TestDirectory, - "logging", - "snapshots", - "LoggerGenerator", - "InstanceField_Class")); + GetSnapshotFolder("InstanceField_Class")); } [Test] @@ -484,12 +464,7 @@ public class LoggerGeneratorSnapshotTests await GeneratorSnapshotTest.RunAsync( source, - Path.Combine( - TestContext.CurrentContext.TestDirectory, - "logging", - "snapshots", - "LoggerGenerator", - "PublicField_Class")); + GetSnapshotFolder("PublicField_Class")); } [Test] @@ -581,11 +556,25 @@ public class LoggerGeneratorSnapshotTests await GeneratorSnapshotTest.RunAsync( source, + GetSnapshotFolder("GenericClass")); + } + + /// + /// 将运行时测试目录映射回仓库内已提交的日志生成器快照目录。 + /// + /// 快照场景名称。 + /// 场景对应的绝对快照目录。 + private static string GetSnapshotFolder(string scenarioName) + { + return Path.GetFullPath( Path.Combine( TestContext.CurrentContext.TestDirectory, - "logging", + "..", + "..", + "..", + "Logging", "snapshots", "LoggerGenerator", - "GenericClass")); + scenarioName)); } } diff --git a/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/CustomFieldName_Class/MyService.Logger.g.cs b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/CustomFieldName_Class/MyService.Logger.g.cs new file mode 100644 index 00000000..bc4a50ae --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/CustomFieldName_Class/MyService.Logger.g.cs @@ -0,0 +1,11 @@ +// +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Logging; + +namespace TestApp; + +partial class MyService +{ + /// Auto-generated logger + private static readonly ILogger MyLogger = LoggerFactoryResolver.Provider.CreateLogger("MyService"); +} diff --git a/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/CustomName_Class/MyService.Logger.g.cs b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/CustomName_Class/MyService.Logger.g.cs new file mode 100644 index 00000000..02bcfad9 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/CustomName_Class/MyService.Logger.g.cs @@ -0,0 +1,11 @@ +// +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Logging; + +namespace TestApp; + +partial class MyService +{ + /// Auto-generated logger + private static readonly ILogger _log = LoggerFactoryResolver.Provider.CreateLogger("MyService"); +} diff --git a/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/DefaultConfiguration_Class/MyService.Logger.g.cs b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/DefaultConfiguration_Class/MyService.Logger.g.cs new file mode 100644 index 00000000..02bcfad9 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/DefaultConfiguration_Class/MyService.Logger.g.cs @@ -0,0 +1,11 @@ +// +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Logging; + +namespace TestApp; + +partial class MyService +{ + /// Auto-generated logger + private static readonly ILogger _log = LoggerFactoryResolver.Provider.CreateLogger("MyService"); +} diff --git a/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/GenericClass/MyService.Logger.g.cs b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/GenericClass/MyService.Logger.g.cs new file mode 100644 index 00000000..f06bd74d --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/GenericClass/MyService.Logger.g.cs @@ -0,0 +1,11 @@ +// +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Logging; + +namespace TestApp; + +partial class MyService +{ + /// Auto-generated logger + private static readonly ILogger _log = LoggerFactoryResolver.Provider.CreateLogger("MyService"); +} diff --git a/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/InstanceField_Class/MyService.Logger.g.cs b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/InstanceField_Class/MyService.Logger.g.cs new file mode 100644 index 00000000..05e566e4 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/InstanceField_Class/MyService.Logger.g.cs @@ -0,0 +1,11 @@ +// +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Logging; + +namespace TestApp; + +partial class MyService +{ + /// Auto-generated logger + private readonly ILogger _log = LoggerFactoryResolver.Provider.CreateLogger("MyService"); +} diff --git a/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/PublicField_Class/MyService.Logger.g.cs b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/PublicField_Class/MyService.Logger.g.cs new file mode 100644 index 00000000..19326d76 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/PublicField_Class/MyService.Logger.g.cs @@ -0,0 +1,11 @@ +// +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Logging; + +namespace TestApp; + +partial class MyService +{ + /// Auto-generated logger + public static readonly ILogger _log = LoggerFactoryResolver.Provider.CreateLogger("MyService"); +} diff --git a/GFramework.SourceGenerators.Tests/Rule/ContextAwareGeneratorSnapshotTests.cs b/GFramework.SourceGenerators.Tests/Rule/ContextAwareGeneratorSnapshotTests.cs index e7b352bf..6b84bb12 100644 --- a/GFramework.SourceGenerators.Tests/Rule/ContextAwareGeneratorSnapshotTests.cs +++ b/GFramework.SourceGenerators.Tests/Rule/ContextAwareGeneratorSnapshotTests.cs @@ -86,9 +86,22 @@ public class ContextAwareGeneratorSnapshotTests // 执行生成器快照测试,将生成的代码与预期快照进行比较 await GeneratorSnapshotTest.RunAsync( source, + GetSnapshotFolder()); + } + + /// + /// 将运行时测试目录映射回仓库内已提交的上下文感知生成器快照目录。 + /// + /// 快照目录的绝对路径。 + private static string GetSnapshotFolder() + { + return Path.GetFullPath( Path.Combine( TestContext.CurrentContext.TestDirectory, - "rule", + "..", + "..", + "..", + "Rule", "snapshots", "ContextAwareGenerator")); } diff --git a/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs b/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs new file mode 100644 index 00000000..54df8881 --- /dev/null +++ b/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs @@ -0,0 +1,55 @@ +// +#nullable enable + +namespace TestApp; + +partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware +{ + private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _context; + private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _contextProvider; + + /// + /// 自动获取的架构上下文(懒加载,默认使用 GameContextProvider) + /// + protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context + { + get + { + if (_context == null) + { + _contextProvider ??= new global::GFramework.Core.Architectures.GameContextProvider(); + _context = _contextProvider.GetContext(); + } + + return _context; + } + } + + /// + /// 配置上下文提供者(用于测试或多架构场景) + /// + /// 上下文提供者实例 + public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider) + { + _contextProvider = provider; + } + + /// + /// 重置上下文提供者为默认值(用于测试清理) + /// + public static void ResetContextProvider() + { + _contextProvider = null; + } + + void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context) + { + _context = context; + } + + global::GFramework.Core.Abstractions.Architectures.IArchitectureContext global::GFramework.Core.Abstractions.Rule.IContextAware.GetContext() + { + return Context; + } + +} \ No newline at end of file From 35a163469729d1c6bff4dabf83f8df6a43cce823 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 17 Apr 2026 09:26:01 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat(cqrs):=20=E6=B7=BB=E5=8A=A0=20CQRS=20?= =?UTF-8?q?=E5=A4=84=E7=90=86=E5=99=A8=E6=B3=A8=E5=86=8C=E5=99=A8=E6=BA=90?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了 CqrsHandlerRegistryGenerator 源代码生成器 - 减少运行时程序集反射扫描成本提高性能 - 支持 IRequestHandler、INotificationHandler 和 IStreamRequestHandler 接口 - 生成静态注册代码避免运行时动态发现处理器 - 实现精确的运行时类型引用解析机制 - 支持跨程序集类型的反射查找功能 - 添加了日志记录和错误处理机制 - 实现了类型安全的依赖注入注册过程 --- .../Cqrs/CqrsHandlerRegistryGenerator.cs | 299 ++++-------------- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 6 + 2 files changed, 76 insertions(+), 229 deletions(-) diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index ba63ea11..0d2c621c 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -19,6 +19,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator private const string CqrsHandlerRegistryAttributeMetadataName = $"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute"; + private const string CqrsReflectionFallbackAttributeMetadataName = + $"{CqrsRuntimeNamespace}.CqrsReflectionFallbackAttribute"; + private const string ILoggerMetadataName = $"{LoggingNamespace}.ILogger"; private const string IServiceCollectionMetadataName = "Microsoft.Extensions.DependencyInjection.IServiceCollection"; private const string GeneratedNamespace = "GFramework.Generated.Cqrs"; @@ -53,8 +56,10 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator CqrsHandlerRegistryAttributeMetadataName) is not null && compilation.GetTypeByMetadataName(ILoggerMetadataName) is not null && compilation.GetTypeByMetadataName(IServiceCollectionMetadataName) is not null; + var supportsReflectionFallbackAttribute = + compilation.GetTypeByMetadataName(CqrsReflectionFallbackAttributeMetadataName) is not null; - return new GenerationEnvironment(generationEnabled); + return new GenerationEnvironment(generationEnabled, supportsReflectionFallbackAttribute); } private static bool IsHandlerCandidate(SyntaxNode node) @@ -92,9 +97,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator ImmutableArray.CreateBuilder(handlerInterfaces.Length); var preciseReflectedRegistrations = ImmutableArray.CreateBuilder(handlerInterfaces.Length); - var runtimeDiscoveredHandlerInterfaceLogNames = - ImmutableArray.CreateBuilder(handlerInterfaces.Length); - var requiresRuntimeInterfaceDiscovery = false; + string? reflectionFallbackHandlerTypeMetadataName = null; foreach (var handlerInterface in handlerInterfaces) { var canReferenceHandlerInterface = @@ -126,11 +129,12 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator continue; } - // 某些关闭 handler interface 仍包含只能在实现类型运行时语义里解析的类型形态。 - // 对这些边角场景保留“已知接口静态注册 + 剩余接口运行时补洞”的组合路径, - // 避免单个未知接口把同实现上的其它已知注册全部拖回整实现反射发现。 - requiresRuntimeInterfaceDiscovery = true; - runtimeDiscoveredHandlerInterfaceLogNames.Add(GetLogDisplayName(handlerInterface)); + // Concrete closed handler contracts should now always map to either direct registrations, + // reflected implementation registrations, or precise runtime type references. + // If a future Roslyn type shape still slips through this net, keep the generator conservative: + // preserve the static registrations we do understand, and let the runtime recover the remaining + // interfaces via the existing assembly-level targeted reflection fallback contract. + reflectionFallbackHandlerTypeMetadataName ??= GetReflectionTypeMetadataName(type); } return new HandlerCandidateAnalysis( @@ -140,8 +144,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator reflectedImplementationRegistrations.ToImmutable(), preciseReflectedRegistrations.ToImmutable(), canReferenceImplementation ? null : GetReflectionTypeMetadataName(type), - requiresRuntimeInterfaceDiscovery, - runtimeDiscoveredHandlerInterfaceLogNames.ToImmutable()); + reflectionFallbackHandlerTypeMetadataName); } private static void Execute(SourceProductionContext context, GenerationEnvironment generationEnvironment, @@ -155,9 +158,22 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator if (registrations.Count == 0) return; + var fallbackHandlerTypeMetadataNames = registrations + .Select(static registration => registration.ReflectionFallbackHandlerTypeMetadataName) + .Where(static typeMetadataName => !string.IsNullOrWhiteSpace(typeMetadataName)) + .Distinct(StringComparer.Ordinal) + .Cast() + .ToArray(); + + if (fallbackHandlerTypeMetadataNames.Length > 0 && + !generationEnvironment.SupportsReflectionFallbackAttribute) + { + return; + } + context.AddSource( HintName, - GenerateSource(registrations)); + GenerateSource(generationEnvironment, registrations, fallbackHandlerTypeMetadataNames)); } private static List CollectRegistrations( @@ -186,8 +202,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator candidate.ReflectedImplementationRegistrations, candidate.PreciseReflectedRegistrations, candidate.ReflectionTypeMetadataName, - candidate.RequiresRuntimeInterfaceDiscovery, - candidate.RuntimeDiscoveredHandlerInterfaceLogNames)); + candidate.ReflectionFallbackHandlerTypeMetadataName)); } registrations.Sort(static (left, right) => @@ -493,14 +508,14 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator } private static string GenerateSource( - IReadOnlyList registrations) + GenerationEnvironment generationEnvironment, + IReadOnlyList registrations, + IReadOnlyList fallbackHandlerTypeMetadataNames) { var hasReflectedImplementationRegistrations = registrations.Any(static registration => !registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty); var hasPreciseReflectedRegistrations = registrations.Any(static registration => !registration.PreciseReflectedRegistrations.IsDefaultOrEmpty); - var hasRuntimeInterfaceDiscovery = registrations.Any(static registration => - registration.RequiresRuntimeInterfaceDiscovery); var hasExternalAssemblyTypeLookups = registrations.Any(static registration => registration.PreciseReflectedRegistrations.Any(static preciseRegistration => preciseRegistration.ServiceTypeArguments.Any(ContainsExternalAssemblyTypeLookup))); @@ -508,6 +523,25 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine("// "); builder.AppendLine("#nullable enable"); builder.AppendLine(); + if (generationEnvironment.SupportsReflectionFallbackAttribute && fallbackHandlerTypeMetadataNames.Count > 0) + { + builder.Append("[assembly: global::"); + builder.Append(CqrsRuntimeNamespace); + builder.Append(".CqrsReflectionFallbackAttribute("); + for (var index = 0; index < fallbackHandlerTypeMetadataNames.Count; index++) + { + if (index > 0) + builder.Append(", "); + + builder.Append('"'); + builder.Append(EscapeStringLiteral(fallbackHandlerTypeMetadataNames[index])); + builder.Append('"'); + } + + builder.AppendLine(")]"); + builder.AppendLine(); + } + builder.Append("[assembly: global::"); builder.Append(CqrsRuntimeNamespace); builder.Append(".CqrsHandlerRegistryAttribute(typeof(global::"); @@ -555,8 +589,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator { var registration = registrations[registrationIndex]; if (!registration.ReflectedImplementationRegistrations.IsDefaultOrEmpty || - !registration.PreciseReflectedRegistrations.IsDefaultOrEmpty || - registration.RequiresRuntimeInterfaceDiscovery) + !registration.PreciseReflectedRegistrations.IsDefaultOrEmpty) { AppendOrderedImplementationRegistrations(builder, registration, registrationIndex); } @@ -568,13 +601,10 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine(" }"); - if (hasRuntimeInterfaceDiscovery || hasExternalAssemblyTypeLookups) + if (hasExternalAssemblyTypeLookups) { builder.AppendLine(); - AppendReflectionHelpers( - builder, - hasRuntimeInterfaceDiscovery, - hasExternalAssemblyTypeLookups); + AppendReflectionHelpers(builder, hasExternalAssemblyTypeLookups); } builder.AppendLine("}"); @@ -647,7 +677,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator StringComparer.Ordinal.Compare(left.HandlerInterfaceLogName, right.HandlerInterfaceLogName)); var implementationVariableName = $"implementationType{registrationIndex}"; - var knownServiceTypesVariableName = $"knownServiceTypes{registrationIndex}"; if (string.IsNullOrWhiteSpace(registration.ReflectionTypeMetadataName)) { builder.Append(" var "); @@ -670,35 +699,12 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine(" is not null)"); builder.AppendLine(" {"); - if (registration.RequiresRuntimeInterfaceDiscovery) - { - builder.Append(" var "); - builder.Append(knownServiceTypesVariableName); - builder.AppendLine(" = new global::System.Collections.Generic.HashSet();"); - foreach (var runtimeDiscoveredHandlerInterfaceLogName in registration - .RuntimeDiscoveredHandlerInterfaceLogNames) - { - builder.Append(" // Remaining runtime interface discovery target: "); - builder.Append(runtimeDiscoveredHandlerInterfaceLogName); - builder.AppendLine(); - } - } - foreach (var orderedRegistration in orderedRegistrations) { switch (orderedRegistration.Kind) { case OrderedRegistrationKind.Direct: var directRegistration = registration.DirectRegistrations[orderedRegistration.Index]; - if (registration.RequiresRuntimeInterfaceDiscovery) - { - builder.Append(" "); - builder.Append(knownServiceTypesVariableName); - builder.Append(".Add(typeof("); - builder.Append(directRegistration.HandlerInterfaceDisplayName); - builder.AppendLine("));"); - } - builder.AppendLine( " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); builder.AppendLine(" services,"); @@ -717,15 +723,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator case OrderedRegistrationKind.ReflectedImplementation: var reflectedRegistration = registration.ReflectedImplementationRegistrations[orderedRegistration.Index]; - if (registration.RequiresRuntimeInterfaceDiscovery) - { - builder.Append(" "); - builder.Append(knownServiceTypesVariableName); - builder.Append(".Add(typeof("); - builder.Append(reflectedRegistration.HandlerInterfaceDisplayName); - builder.AppendLine("));"); - } - builder.AppendLine( " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); builder.AppendLine(" services,"); @@ -752,8 +749,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator preciseRegistration.OpenHandlerTypeDisplayName, registration.ImplementationLogName, preciseRegistration.HandlerInterfaceLogName, - knownServiceTypesVariableName, - registration.RequiresRuntimeInterfaceDiscovery, 3); break; default: @@ -762,15 +757,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator } } - if (registration.RequiresRuntimeInterfaceDiscovery) - { - builder.Append(" RegisterRemainingReflectedHandlerInterfaces(services, logger, "); - builder.Append(implementationVariableName); - builder.Append(", "); - builder.Append(knownServiceTypesVariableName); - builder.AppendLine(");"); - } - builder.AppendLine(" }"); } @@ -782,8 +768,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator string openHandlerTypeDisplayName, string implementationLogName, string handlerInterfaceLogName, - string knownServiceTypesVariableName, - bool trackKnownServiceTypes, int indentLevel) { var indent = new string(' ', indentLevel * 4); @@ -848,15 +832,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.Append(" "); builder.Append(implementationVariableName); builder.AppendLine(");"); - if (trackKnownServiceTypes) - { - builder.Append(indent); - builder.Append(knownServiceTypesVariableName); - builder.Append(".Add("); - builder.Append(registrationVariablePrefix); - builder.AppendLine(");"); - } - builder.Append(indent); builder.Append("logger.Debug(\"Registered CQRS handler "); builder.Append(EscapeStringLiteral(implementationLogName)); @@ -945,7 +920,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator private static void AppendReflectionHelpers( StringBuilder builder, - bool includeRuntimeInterfaceDiscoveryHelpers, bool includeExternalAssemblyTypeLookupHelpers) { if (includeExternalAssemblyTypeLookupHelpers) @@ -991,123 +965,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator builder.AppendLine(" }"); builder.AppendLine(" }"); } - - if (!includeRuntimeInterfaceDiscoveryHelpers) - return; - - if (includeExternalAssemblyTypeLookupHelpers) - builder.AppendLine(); - - // Emit the runtime helper methods only when at least one handler still needs implementation-scoped - // interface discovery after all direct / precise registrations have been emitted. - builder.AppendLine( - " private static void RegisterRemainingReflectedHandlerInterfaces(global::Microsoft.Extensions.DependencyInjection.IServiceCollection services, global::GFramework.Core.Abstractions.Logging.ILogger logger, global::System.Type implementationType, global::System.Collections.Generic.ISet knownServiceTypes)"); - builder.AppendLine(" {"); - builder.AppendLine(" var handlerInterfaces = implementationType.GetInterfaces();"); - builder.AppendLine(" global::System.Array.Sort(handlerInterfaces, CompareTypes);"); - builder.AppendLine(); - builder.AppendLine(" foreach (var handlerInterface in handlerInterfaces)"); - builder.AppendLine(" {"); - builder.AppendLine(" if (!IsSupportedHandlerInterface(handlerInterface))"); - builder.AppendLine(" continue;"); - builder.AppendLine(); - builder.AppendLine(" if (knownServiceTypes.Contains(handlerInterface))"); - builder.AppendLine(" continue;"); - builder.AppendLine(); - builder.AppendLine( - " global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddTransient("); - builder.AppendLine(" services,"); - builder.AppendLine(" handlerInterface,"); - builder.AppendLine(" implementationType);"); - builder.AppendLine( - " logger.Debug($\"Registered CQRS handler {GetRuntimeTypeDisplayName(implementationType)} as {GetRuntimeTypeDisplayName(handlerInterface)}.\");"); - builder.AppendLine(" knownServiceTypes.Add(handlerInterface);"); - builder.AppendLine(" }"); - builder.AppendLine(" }"); - builder.AppendLine(); - builder.AppendLine(" private static int CompareTypes(global::System.Type left, global::System.Type right)"); - builder.AppendLine(" {"); - builder.AppendLine( - " return global::System.StringComparer.Ordinal.Compare(GetRuntimeTypeDisplayName(left), GetRuntimeTypeDisplayName(right));"); - builder.AppendLine(" }"); - builder.AppendLine(); - builder.AppendLine(" private static bool IsSupportedHandlerInterface(global::System.Type interfaceType)"); - builder.AppendLine(" {"); - builder.AppendLine(" if (!interfaceType.IsGenericType)"); - builder.AppendLine(" return false;"); - builder.AppendLine(); - builder.AppendLine(" var definitionFullName = interfaceType.GetGenericTypeDefinition().FullName;"); - builder.AppendLine( - $" return global::System.StringComparer.Ordinal.Equals(definitionFullName, \"{IRequestHandlerMetadataName}\")"); - builder.AppendLine( - $" || global::System.StringComparer.Ordinal.Equals(definitionFullName, \"{INotificationHandlerMetadataName}\")"); - builder.AppendLine( - $" || global::System.StringComparer.Ordinal.Equals(definitionFullName, \"{IStreamRequestHandlerMetadataName}\");"); - builder.AppendLine(" }"); - builder.AppendLine(); - builder.AppendLine(" private static string GetRuntimeTypeDisplayName(global::System.Type type)"); - builder.AppendLine(" {"); - builder.AppendLine(" if (type == typeof(string))"); - builder.AppendLine(" return \"string\";"); - builder.AppendLine(" if (type == typeof(int))"); - builder.AppendLine(" return \"int\";"); - builder.AppendLine(" if (type == typeof(long))"); - builder.AppendLine(" return \"long\";"); - builder.AppendLine(" if (type == typeof(short))"); - builder.AppendLine(" return \"short\";"); - builder.AppendLine(" if (type == typeof(byte))"); - builder.AppendLine(" return \"byte\";"); - builder.AppendLine(" if (type == typeof(bool))"); - builder.AppendLine(" return \"bool\";"); - builder.AppendLine(" if (type == typeof(object))"); - builder.AppendLine(" return \"object\";"); - builder.AppendLine(" if (type == typeof(void))"); - builder.AppendLine(" return \"void\";"); - builder.AppendLine(" if (type == typeof(uint))"); - builder.AppendLine(" return \"uint\";"); - builder.AppendLine(" if (type == typeof(ulong))"); - builder.AppendLine(" return \"ulong\";"); - builder.AppendLine(" if (type == typeof(ushort))"); - builder.AppendLine(" return \"ushort\";"); - builder.AppendLine(" if (type == typeof(sbyte))"); - builder.AppendLine(" return \"sbyte\";"); - builder.AppendLine(" if (type == typeof(float))"); - builder.AppendLine(" return \"float\";"); - builder.AppendLine(" if (type == typeof(double))"); - builder.AppendLine(" return \"double\";"); - builder.AppendLine(" if (type == typeof(decimal))"); - builder.AppendLine(" return \"decimal\";"); - builder.AppendLine(" if (type == typeof(char))"); - builder.AppendLine(" return \"char\";"); - builder.AppendLine(); - builder.AppendLine(" if (type.IsArray)"); - builder.AppendLine(" return GetRuntimeTypeDisplayName(type.GetElementType()!) + \"[]\";"); - builder.AppendLine(); - builder.AppendLine(" if (!type.IsGenericType)"); - builder.AppendLine(" return (type.FullName ?? type.Name).Replace('+', '.');"); - builder.AppendLine(); - builder.AppendLine(" var genericTypeName = type.GetGenericTypeDefinition().FullName ?? type.Name;"); - builder.AppendLine(" var arityIndex = genericTypeName.IndexOf('`');"); - builder.AppendLine(" if (arityIndex >= 0)"); - builder.AppendLine(" genericTypeName = genericTypeName[..arityIndex];"); - builder.AppendLine(); - builder.AppendLine(" genericTypeName = genericTypeName.Replace('+', '.');"); - builder.AppendLine(" var arguments = type.GetGenericArguments();"); - builder.AppendLine(" var builder = new global::System.Text.StringBuilder();"); - builder.AppendLine(" builder.Append(genericTypeName);"); - builder.AppendLine(" builder.Append('<');"); - builder.AppendLine(); - builder.AppendLine(" for (var index = 0; index < arguments.Length; index++)"); - builder.AppendLine(" {"); - builder.AppendLine(" if (index > 0)"); - builder.AppendLine(" builder.Append(\", \");"); - builder.AppendLine(); - builder.AppendLine(" builder.Append(GetRuntimeTypeDisplayName(arguments[index]));"); - builder.AppendLine(" }"); - builder.AppendLine(); - builder.AppendLine(" builder.Append('>');"); - builder.AppendLine(" return builder.ToString();"); - builder.AppendLine(" }"); } private static string EscapeStringLiteral(string value) @@ -1218,8 +1075,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator ImmutableArray ReflectedImplementationRegistrations, ImmutableArray PreciseReflectedRegistrations, string? ReflectionTypeMetadataName, - bool RequiresRuntimeInterfaceDiscovery, - ImmutableArray RuntimeDiscoveredHandlerInterfaceLogNames); + string? ReflectionFallbackHandlerTypeMetadataName); private readonly struct HandlerCandidateAnalysis : IEquatable { @@ -1230,8 +1086,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator ImmutableArray reflectedImplementationRegistrations, ImmutableArray preciseReflectedRegistrations, string? reflectionTypeMetadataName, - bool requiresRuntimeInterfaceDiscovery, - ImmutableArray runtimeDiscoveredHandlerInterfaceLogNames) + string? reflectionFallbackHandlerTypeMetadataName) { ImplementationTypeDisplayName = implementationTypeDisplayName; ImplementationLogName = implementationLogName; @@ -1239,8 +1094,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator ReflectedImplementationRegistrations = reflectedImplementationRegistrations; PreciseReflectedRegistrations = preciseReflectedRegistrations; ReflectionTypeMetadataName = reflectionTypeMetadataName; - RequiresRuntimeInterfaceDiscovery = requiresRuntimeInterfaceDiscovery; - RuntimeDiscoveredHandlerInterfaceLogNames = runtimeDiscoveredHandlerInterfaceLogNames; + ReflectionFallbackHandlerTypeMetadataName = reflectionFallbackHandlerTypeMetadataName; } public string ImplementationTypeDisplayName { get; } @@ -1255,9 +1109,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator public string? ReflectionTypeMetadataName { get; } - public bool RequiresRuntimeInterfaceDiscovery { get; } - - public ImmutableArray RuntimeDiscoveredHandlerInterfaceLogNames { get; } + public string? ReflectionFallbackHandlerTypeMetadataName { get; } public bool Equals(HandlerCandidateAnalysis other) { @@ -1266,12 +1118,13 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator !string.Equals(ImplementationLogName, other.ImplementationLogName, StringComparison.Ordinal) || !string.Equals(ReflectionTypeMetadataName, other.ReflectionTypeMetadataName, StringComparison.Ordinal) || - RequiresRuntimeInterfaceDiscovery != other.RequiresRuntimeInterfaceDiscovery || + !string.Equals( + ReflectionFallbackHandlerTypeMetadataName, + other.ReflectionFallbackHandlerTypeMetadataName, + StringComparison.Ordinal) || Registrations.Length != other.Registrations.Length || ReflectedImplementationRegistrations.Length != other.ReflectedImplementationRegistrations.Length || - PreciseReflectedRegistrations.Length != other.PreciseReflectedRegistrations.Length || - RuntimeDiscoveredHandlerInterfaceLogNames.Length != - other.RuntimeDiscoveredHandlerInterfaceLogNames.Length) + PreciseReflectedRegistrations.Length != other.PreciseReflectedRegistrations.Length) { return false; } @@ -1295,17 +1148,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return false; } - for (var index = 0; index < RuntimeDiscoveredHandlerInterfaceLogNames.Length; index++) - { - if (!string.Equals( - RuntimeDiscoveredHandlerInterfaceLogNames[index], - other.RuntimeDiscoveredHandlerInterfaceLogNames[index], - StringComparison.Ordinal)) - { - return false; - } - } - return true; } @@ -1324,7 +1166,10 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator (ReflectionTypeMetadataName is null ? 0 : StringComparer.Ordinal.GetHashCode(ReflectionTypeMetadataName)); - hashCode = (hashCode * 397) ^ RequiresRuntimeInterfaceDiscovery.GetHashCode(); + hashCode = (hashCode * 397) ^ + (ReflectionFallbackHandlerTypeMetadataName is null + ? 0 + : StringComparer.Ordinal.GetHashCode(ReflectionFallbackHandlerTypeMetadataName)); foreach (var registration in Registrations) { hashCode = (hashCode * 397) ^ registration.GetHashCode(); @@ -1340,16 +1185,12 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator hashCode = (hashCode * 397) ^ preciseReflectedRegistration.GetHashCode(); } - foreach (var runtimeDiscoveredHandlerInterfaceLogName in RuntimeDiscoveredHandlerInterfaceLogNames) - { - hashCode = (hashCode * 397) ^ - StringComparer.Ordinal.GetHashCode(runtimeDiscoveredHandlerInterfaceLogName); - } - return hashCode; } } } - private readonly record struct GenerationEnvironment(bool GenerationEnabled); + private readonly record struct GenerationEnvironment( + bool GenerationEnabled, + bool SupportsReflectionFallbackAttribute); } diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 5d97e998..28e80f6d 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -1001,6 +1001,12 @@ public class CqrsHandlerRegistryGeneratorTests contractsReference, dependencyReference); + Assert.That( + generatedSource, + Does.Not.Contain("RegisterRemainingReflectedHandlerInterfaces(")); + Assert.That( + generatedSource, + Does.Not.Contain("Remaining runtime interface discovery target:")); Assert.That( generatedSource, Is.EqualTo(ExternalAssemblyPreciseLookupExpected)); From b19877f970477c89651462d99c5b8c7217c6882a Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:07:57 +0800 Subject: [PATCH 3/7] =?UTF-8?q?feat(source-generators):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=A4=9A=E4=B8=AA=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=99=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 PriorityGenerator 为标记 Priority 特性的类自动生成 IPrioritized 接口实现 - 新增 EnumExtensionsGenerator 为枚举自动生成 Is 和 IsIn 扩展方法 - 新增 LoggerGenerator 为标记 Log 特性的类自动生成日志字段 - 新增 ContextAwareGenerator 为标记 ContextAware 特性的类自动生成 IContextAware 接口实现 - 新增 CqrsHandlerRegistryGenerator 为 CQRS 处理器生成编译时注册器减少运行时反射开销 --- .../Bases/PriorityGenerator.cs | 3 ++ .../Enums/EnumExtensionsGenerator.cs | 19 +++++++++-- .../Logging/LoggerGenerator.cs | 7 +++- .../Rule/ContextAwareGenerator.cs | 28 +++++++++++---- .../Cqrs/CqrsHandlerRegistryGenerator.cs | 25 ++++++++++++-- .../TestApp_MySystem.Priority.g.cs | 5 ++- .../TestApp_GenericSystem_T_.Priority.g.cs | 5 ++- .../TestApp_CriticalSystem.Priority.g.cs | 5 ++- .../TestApp_HighPrioritySystem.Priority.g.cs | 5 ++- .../Core/GeneratorSnapshotTest.cs | 8 ++--- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 26 ++++++++++++++ .../EnumExtensionsGeneratorSnapshotTests.cs | 7 ++++ .../Status.EnumExtensions.g.txt | 22 ++++++++++-- .../Status.EnumExtensions.g.txt | 28 ++++++++++++--- .../Status.EnumExtensions.g.cs | 22 ++++++++++-- .../Status.EnumExtensions.g.txt | 3 ++ .../Status.EnumExtensions.g.txt | 15 ++++++-- .../Status.EnumExtensions.g.txt | 10 +++++- .../Permissions.EnumExtensions.g.txt | 34 ++++++++++++++++--- .../MyService.Logger.g.cs | 7 +++- .../CustomName_Class/MyService.Logger.g.cs | 7 +++- .../MyService.Logger.g.cs | 7 +++- .../GenericClass/MyService.Logger.g.cs | 7 +++- .../InstanceField_Class/MyService.Logger.g.cs | 7 +++- .../PublicField_Class/MyService.Logger.g.cs | 7 +++- .../MyRule.ContextAware.g.cs | 30 ++++++++++++---- 26 files changed, 299 insertions(+), 50 deletions(-) diff --git a/GFramework.Core.SourceGenerators/Bases/PriorityGenerator.cs b/GFramework.Core.SourceGenerators/Bases/PriorityGenerator.cs index ee534f8a..5c415e4a 100644 --- a/GFramework.Core.SourceGenerators/Bases/PriorityGenerator.cs +++ b/GFramework.Core.SourceGenerators/Bases/PriorityGenerator.cs @@ -118,6 +118,9 @@ public sealed class PriorityGenerator : MetadataAttributeClassGeneratorBase ? $"<{string.Join(", ", symbol.TypeParameters.Select(tp => tp.Name))}>" : string.Empty; + sb.AppendLine("/// "); + sb.AppendLine("/// 为当前分部类型补充自动生成的优先级契约实现。"); + sb.AppendLine("/// "); sb.AppendLine( $"partial class {symbol.Name}{typeParameters} : global::GFramework.Core.Abstractions.Bases.IPrioritized"); sb.AppendLine("{"); diff --git a/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs b/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs index a73d076d..32a4715a 100644 --- a/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs +++ b/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs @@ -95,6 +95,9 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase sb.AppendLine("{"); + sb.AppendLine(" /// "); + sb.AppendLine($" /// 为 提供自动生成的扩展方法。"); + sb.AppendLine(" /// "); sb.AppendLine($" public static partial class {enumName}Extensions"); sb.AppendLine(" {"); @@ -176,7 +179,13 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase builder.AppendLine(); } - builder.AppendLine($" /// 是否为 {memberName}"); + builder.AppendLine(" /// "); + builder.AppendLine( + $" /// 判断给定值是否为 。"); + builder.AppendLine(" /// "); + builder.AppendLine(" /// 要检查的枚举值。"); + builder.AppendLine( + $" /// 等于 时返回 ;否则返回 "); builder.AppendLine( $" public static bool Is{memberName}(this {fullEnumName} value) => value == {fullEnumName}.{memberName};"); hasGeneratedMembers = true; @@ -192,7 +201,13 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase /// 枚举的完整类型名。 private static void AppendIsInMethod(StringBuilder builder, string fullEnumName) { - builder.AppendLine(" /// 判断是否属于指定集合"); + builder.AppendLine(" /// "); + builder.AppendLine(" /// 判断给定值是否属于指定候选集合。"); + builder.AppendLine(" /// "); + builder.AppendLine(" /// 要检查的枚举值。"); + builder.AppendLine(" /// 用于匹配的候选枚举值集合。"); + builder.AppendLine( + " /// 命中任一候选值时返回 ;否则返回 "); builder.AppendLine( $" public static bool IsIn(this {fullEnumName} value, params {fullEnumName}[] values)"); builder.AppendLine(" {"); diff --git a/GFramework.Core.SourceGenerators/Logging/LoggerGenerator.cs b/GFramework.Core.SourceGenerators/Logging/LoggerGenerator.cs index 2be4a1d4..7d875d0c 100644 --- a/GFramework.Core.SourceGenerators/Logging/LoggerGenerator.cs +++ b/GFramework.Core.SourceGenerators/Logging/LoggerGenerator.cs @@ -71,13 +71,18 @@ public sealed class LoggerGenerator : TypeAttributeClassGeneratorBase .AppendLine($"namespace {ns};"); sb.AppendLine() + .AppendLine("/// ") + .AppendLine("/// 为当前分部类型提供自动生成的日志字段。") + .AppendLine("/// ") .AppendLine($"partial {typeKind} {className}{generics.Parameters}"); foreach (var c in generics.Constraints) sb.AppendLine($" {c}"); sb.AppendLine("{") - .AppendLine(" /// Auto-generated logger") + .AppendLine(" /// ") + .AppendLine(" /// 自动生成的日志字段。") + .AppendLine(" /// ") .AppendLine( $" {access} {staticKeyword}readonly ILogger {fieldName} = " + $"LoggerFactoryResolver.Provider.CreateLogger(\"{logName}\");") diff --git a/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs b/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs index 9bdeb3f9..0fd15fb7 100644 --- a/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs +++ b/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs @@ -96,6 +96,9 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase var interfaceName = iContextAware.ToDisplayString( SymbolDisplayFormat.FullyQualifiedFormat); + sb.AppendLine("/// "); + sb.AppendLine("/// 为当前规则类型补充自动生成的架构上下文访问实现。"); + sb.AppendLine("/// "); sb.AppendLine($"partial class {symbol.Name} : {interfaceName}"); sb.AppendLine("{"); @@ -128,6 +131,7 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine(" private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _context;"); sb.AppendLine( " private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _contextProvider;"); + sb.AppendLine(" private static readonly object _contextSync = new();"); sb.AppendLine(); sb.AppendLine(" /// "); sb.AppendLine(" /// 自动获取的架构上下文(懒加载,默认使用 GameContextProvider)"); @@ -136,14 +140,20 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine(" {"); sb.AppendLine(" get"); sb.AppendLine(" {"); - sb.AppendLine(" if (_context == null)"); + sb.AppendLine(" var context = _context;"); + sb.AppendLine(" if (context is not null)"); + sb.AppendLine(" {"); + sb.AppendLine(" return context;"); + sb.AppendLine(" }"); + sb.AppendLine(); + sb.AppendLine(" // 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。"); + sb.AppendLine(" lock (_contextSync)"); sb.AppendLine(" {"); sb.AppendLine( " _contextProvider ??= new global::GFramework.Core.Architectures.GameContextProvider();"); - sb.AppendLine(" _context = _contextProvider.GetContext();"); + sb.AppendLine(" _context ??= _contextProvider.GetContext();"); + sb.AppendLine(" return _context;"); sb.AppendLine(" }"); - sb.AppendLine(); - sb.AppendLine(" return _context;"); sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(); @@ -154,7 +164,10 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine( " public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)"); sb.AppendLine(" {"); - sb.AppendLine(" _contextProvider = provider;"); + sb.AppendLine(" lock (_contextSync)"); + sb.AppendLine(" {"); + sb.AppendLine(" _contextProvider = provider;"); + sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); @@ -162,7 +175,10 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine(" /// "); sb.AppendLine(" public static void ResetContextProvider()"); sb.AppendLine(" {"); - sb.AppendLine(" _contextProvider = null;"); + sb.AppendLine(" lock (_contextSync)"); + sb.AppendLine(" {"); + sb.AppendLine(" _contextProvider = null;"); + sb.AppendLine(" }"); sb.AppendLine(" }"); sb.AppendLine(); } diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 0d2c621c..a1cb10a6 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -165,8 +165,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator .Cast() .ToArray(); - if (fallbackHandlerTypeMetadataNames.Length > 0 && - !generationEnvironment.SupportsReflectionFallbackAttribute) + if (!CanEmitGeneratedRegistry( + generationEnvironment.SupportsReflectionFallbackAttribute, + fallbackHandlerTypeMetadataNames.Length)) { return; } @@ -176,6 +177,26 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator GenerateSource(generationEnvironment, registrations, fallbackHandlerTypeMetadataNames)); } + /// + /// 判断当前轮次是否允许输出生成注册器。 + /// + /// + /// runtime 合同中是否存在 CqrsReflectionFallbackAttribute,以承载生成器无法静态精确表达的 handler 回退元数据。 + /// + /// + /// 当前轮次需要依赖程序集级 reflection fallback 元数据恢复的 handler 数量。 + /// + /// + /// 当没有 handler 依赖 fallback,或 runtime 已提供承载该元数据的特性契约时返回 ; + /// 否则返回 ,调用方必须放弃生成以避免输出会静默漏注册的半成品注册器。 + /// + private static bool CanEmitGeneratedRegistry( + bool supportsReflectionFallbackAttribute, + int fallbackHandlerTypeCount) + { + return fallbackHandlerTypeCount == 0 || supportsReflectionFallbackAttribute; + } + private static List CollectRegistrations( ImmutableArray candidates) { diff --git a/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/BasicPriority/TestApp_MySystem.Priority.g.cs b/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/BasicPriority/TestApp_MySystem.Priority.g.cs index 2a6756fe..fec15e5d 100644 --- a/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/BasicPriority/TestApp_MySystem.Priority.g.cs +++ b/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/BasicPriority/TestApp_MySystem.Priority.g.cs @@ -3,10 +3,13 @@ namespace TestApp; +/// +/// 为当前分部类型补充自动生成的优先级契约实现。 +/// partial class MySystem : global::GFramework.Core.Abstractions.Bases.IPrioritized { /// /// 获取优先级值: 10 /// public int Priority => 10; -} \ No newline at end of file +} diff --git a/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/GenericClass/TestApp_GenericSystem_T_.Priority.g.cs b/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/GenericClass/TestApp_GenericSystem_T_.Priority.g.cs index a0ce6997..c339c989 100644 --- a/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/GenericClass/TestApp_GenericSystem_T_.Priority.g.cs +++ b/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/GenericClass/TestApp_GenericSystem_T_.Priority.g.cs @@ -3,10 +3,13 @@ namespace TestApp; +/// +/// 为当前分部类型补充自动生成的优先级契约实现。 +/// partial class GenericSystem : global::GFramework.Core.Abstractions.Bases.IPrioritized { /// /// 获取优先级值: 20 /// public int Priority => 20; -} \ No newline at end of file +} diff --git a/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/NegativePriority/TestApp_CriticalSystem.Priority.g.cs b/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/NegativePriority/TestApp_CriticalSystem.Priority.g.cs index bd6b4af3..03f57f91 100644 --- a/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/NegativePriority/TestApp_CriticalSystem.Priority.g.cs +++ b/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/NegativePriority/TestApp_CriticalSystem.Priority.g.cs @@ -3,10 +3,13 @@ namespace TestApp; +/// +/// 为当前分部类型补充自动生成的优先级契约实现。 +/// partial class CriticalSystem : global::GFramework.Core.Abstractions.Bases.IPrioritized { /// /// 获取优先级值: -100 /// public int Priority => -100; -} \ No newline at end of file +} diff --git a/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/PriorityGroup/TestApp_HighPrioritySystem.Priority.g.cs b/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/PriorityGroup/TestApp_HighPrioritySystem.Priority.g.cs index 737acd1a..49fb10aa 100644 --- a/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/PriorityGroup/TestApp_HighPrioritySystem.Priority.g.cs +++ b/GFramework.SourceGenerators.Tests/Bases/snapshots/PriorityGenerator/PriorityGroup/TestApp_HighPrioritySystem.Priority.g.cs @@ -3,10 +3,13 @@ namespace TestApp; +/// +/// 为当前分部类型补充自动生成的优先级契约实现。 +/// partial class HighPrioritySystem : global::GFramework.Core.Abstractions.Bases.IPrioritized { /// /// 获取优先级值: -50 /// public int Priority => -50; -} \ No newline at end of file +} diff --git a/GFramework.SourceGenerators.Tests/Core/GeneratorSnapshotTest.cs b/GFramework.SourceGenerators.Tests/Core/GeneratorSnapshotTest.cs index f464cb74..d8726a2f 100644 --- a/GFramework.SourceGenerators.Tests/Core/GeneratorSnapshotTest.cs +++ b/GFramework.SourceGenerators.Tests/Core/GeneratorSnapshotTest.cs @@ -42,7 +42,7 @@ public static class GeneratorSnapshotTest compilationErrors, Is.Empty, () => - $"编译生成的代码时出现错误:{Environment.NewLine}{string.Join(Environment.NewLine, compilationErrors.Select(static diagnostic => diagnostic.ToString()))}"); + $"编译生成的代码时出现错误:{Environment.NewLine}{string.Join(Environment.NewLine, compilationErrors.Select(static diagnostic => diagnostic.ToString()))}"); var runResult = driver.GetRunResult(); var generated = runResult.Results @@ -53,7 +53,7 @@ public static class GeneratorSnapshotTest Assert.That( generated, Is.Not.Empty, - $"Generator '{typeof(TGenerator).FullName}' did not produce any sources."); + $"生成器 '{typeof(TGenerator).FullName}' 未产生任何输出。"); foreach (var (filename, content) in generated) { @@ -70,7 +70,7 @@ public static class GeneratorSnapshotTest await File.WriteAllTextAsync(path, content.ToString()); Assert.Fail( - $"Snapshot not found. Generated new snapshot at:\n{path}"); + $"未找到快照文件,已在以下路径生成新快照:\n{path}"); } var expected = await File.ReadAllTextAsync(path); @@ -78,7 +78,7 @@ public static class GeneratorSnapshotTest Assert.That( Normalize(expected), Is.EqualTo(Normalize(content.ToString())), - $"Snapshot mismatch: {snapshotFileName}"); + $"快照不匹配:{snapshotFileName}"); } } diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 28e80f6d..50483a08 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -1168,6 +1168,32 @@ public class CqrsHandlerRegistryGeneratorTests ("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected)); } + /// + /// 验证当某轮生成仍然需要程序集级 reflection fallback 元数据时, + /// 若 runtime 合同未提供对应特性契约,生成器会放弃输出注册器以避免静默漏注册。 + /// + [Test] + public void + Rejects_Registry_Emission_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute() + { + var method = typeof(CqrsHandlerRegistryGenerator).GetMethod( + "CanEmitGeneratedRegistry", + BindingFlags.NonPublic | BindingFlags.Static); + + Assert.That(method, Is.Not.Null); + + var canEmitWithoutFallbackRequirement = (bool?)method!.Invoke(null, [false, 0]); + var canEmitWithSupportedFallbackAttribute = (bool?)method.Invoke(null, [true, 1]); + var canEmitWithoutSupportedFallbackAttribute = (bool?)method.Invoke(null, [false, 1]); + + Assert.Multiple(() => + { + Assert.That(canEmitWithoutFallbackRequirement, Is.True); + Assert.That(canEmitWithSupportedFallbackAttribute, Is.True); + Assert.That(canEmitWithoutSupportedFallbackAttribute, Is.False); + }); + } + /// /// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。 /// diff --git a/GFramework.SourceGenerators.Tests/Enums/EnumExtensionsGeneratorSnapshotTests.cs b/GFramework.SourceGenerators.Tests/Enums/EnumExtensionsGeneratorSnapshotTests.cs index f5fa2157..e5720826 100644 --- a/GFramework.SourceGenerators.Tests/Enums/EnumExtensionsGeneratorSnapshotTests.cs +++ b/GFramework.SourceGenerators.Tests/Enums/EnumExtensionsGeneratorSnapshotTests.cs @@ -15,6 +15,7 @@ public class EnumExtensionsGeneratorSnapshotTests /// /// 验证默认配置会为普通枚举生成逐项判断方法与集合判断方法。 /// + /// 异步任务。 [Test] public async Task Snapshot_BasicEnum_IsMethods() { @@ -37,6 +38,7 @@ public class EnumExtensionsGeneratorSnapshotTests /// /// 验证未提供快照文件名映射时,会直接按生成文件名进行快照比对。 /// + /// 异步任务。 [Test] public async Task Snapshot_BasicEnum_IsMethods_DefaultSnapshotFileNameSelector() { @@ -57,6 +59,7 @@ public class EnumExtensionsGeneratorSnapshotTests /// /// 验证默认配置在较小枚举上仍会生成集合判断方法。 /// + /// 异步任务。 [Test] public async Task Snapshot_BasicEnum_IsInMethod() { @@ -78,6 +81,7 @@ public class EnumExtensionsGeneratorSnapshotTests /// /// 验证带显式位标志值的枚举也会生成对应扩展方法。 /// + /// 异步任务。 [Test] public async Task Snapshot_EnumWithFlagValues() { @@ -102,6 +106,7 @@ public class EnumExtensionsGeneratorSnapshotTests /// /// 验证关闭逐项判断开关后仅保留集合判断方法。 /// + /// 异步任务。 [Test] public async Task Snapshot_DisableIsMethods() { @@ -124,6 +129,7 @@ public class EnumExtensionsGeneratorSnapshotTests /// /// 验证关闭集合判断开关后仅保留逐项判断方法。 /// + /// 异步任务。 [Test] public async Task Snapshot_DisableIsInMethod() { @@ -146,6 +152,7 @@ public class EnumExtensionsGeneratorSnapshotTests /// /// 验证同时关闭两个生成开关时不会输出任何扩展方法。 /// + /// 异步任务。 [Test] public async Task Snapshot_DisableAllGeneratedMethods() { diff --git a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsInMethod/Status.EnumExtensions.g.txt b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsInMethod/Status.EnumExtensions.g.txt index 2897d09c..89db6bdb 100644 --- a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsInMethod/Status.EnumExtensions.g.txt +++ b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsInMethod/Status.EnumExtensions.g.txt @@ -2,15 +2,31 @@ using System; namespace TestApp { + /// + /// 为 提供自动生成的扩展方法。 + /// public static partial class StatusExtensions { - /// 是否为 Active + /// + /// 判断给定值是否为 。 + /// + /// 要检查的枚举值。 + /// 等于 时返回 ;否则返回 public static bool IsActive(this TestApp.Status value) => value == TestApp.Status.Active; - /// 是否为 Inactive + /// + /// 判断给定值是否为 。 + /// + /// 要检查的枚举值。 + /// 等于 时返回 ;否则返回 public static bool IsInactive(this TestApp.Status value) => value == TestApp.Status.Inactive; - /// 判断是否属于指定集合 + /// + /// 判断给定值是否属于指定候选集合。 + /// + /// 要检查的枚举值。 + /// 用于匹配的候选枚举值集合。 + /// 命中任一候选值时返回 ;否则返回 public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values) { if (values == null) return false; diff --git a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods/Status.EnumExtensions.g.txt b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods/Status.EnumExtensions.g.txt index db9fa7ba..1709ecf7 100644 --- a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods/Status.EnumExtensions.g.txt +++ b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods/Status.EnumExtensions.g.txt @@ -2,18 +2,38 @@ using System; namespace TestApp { + /// + /// 为 提供自动生成的扩展方法。 + /// public static partial class StatusExtensions { - /// 是否为 Active + /// + /// 判断给定值是否为 。 + /// + /// 要检查的枚举值。 + /// 等于 时返回 ;否则返回 public static bool IsActive(this TestApp.Status value) => value == TestApp.Status.Active; - /// 是否为 Inactive + /// + /// 判断给定值是否为 。 + /// + /// 要检查的枚举值。 + /// 等于 时返回 ;否则返回 public static bool IsInactive(this TestApp.Status value) => value == TestApp.Status.Inactive; - /// 是否为 Pending + /// + /// 判断给定值是否为 。 + /// + /// 要检查的枚举值。 + /// 等于 时返回 ;否则返回 public static bool IsPending(this TestApp.Status value) => value == TestApp.Status.Pending; - /// 判断是否属于指定集合 + /// + /// 判断给定值是否属于指定候选集合。 + /// + /// 要检查的枚举值。 + /// 用于匹配的候选枚举值集合。 + /// 命中任一候选值时返回 ;否则返回 public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values) { if (values == null) return false; diff --git a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods_DefaultSnapshotFileNameSelector/Status.EnumExtensions.g.cs b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods_DefaultSnapshotFileNameSelector/Status.EnumExtensions.g.cs index 2897d09c..89db6bdb 100644 --- a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods_DefaultSnapshotFileNameSelector/Status.EnumExtensions.g.cs +++ b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods_DefaultSnapshotFileNameSelector/Status.EnumExtensions.g.cs @@ -2,15 +2,31 @@ using System; namespace TestApp { + /// + /// 为 提供自动生成的扩展方法。 + /// public static partial class StatusExtensions { - /// 是否为 Active + /// + /// 判断给定值是否为 。 + /// + /// 要检查的枚举值。 + /// 等于 时返回 ;否则返回 public static bool IsActive(this TestApp.Status value) => value == TestApp.Status.Active; - /// 是否为 Inactive + /// + /// 判断给定值是否为 。 + /// + /// 要检查的枚举值。 + /// 等于 时返回 ;否则返回 public static bool IsInactive(this TestApp.Status value) => value == TestApp.Status.Inactive; - /// 判断是否属于指定集合 + /// + /// 判断给定值是否属于指定候选集合。 + /// + /// 要检查的枚举值。 + /// 用于匹配的候选枚举值集合。 + /// 命中任一候选值时返回 ;否则返回 public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values) { if (values == null) return false; diff --git a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableAllGeneratedMethods/Status.EnumExtensions.g.txt b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableAllGeneratedMethods/Status.EnumExtensions.g.txt index 74b39b20..23ea0fa5 100644 --- a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableAllGeneratedMethods/Status.EnumExtensions.g.txt +++ b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableAllGeneratedMethods/Status.EnumExtensions.g.txt @@ -2,6 +2,9 @@ using System; namespace TestApp { + /// + /// 为 提供自动生成的扩展方法。 + /// public static partial class StatusExtensions { } diff --git a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableIsInMethod/Status.EnumExtensions.g.txt b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableIsInMethod/Status.EnumExtensions.g.txt index 3563db50..c79e62ef 100644 --- a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableIsInMethod/Status.EnumExtensions.g.txt +++ b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableIsInMethod/Status.EnumExtensions.g.txt @@ -2,12 +2,23 @@ using System; namespace TestApp { + /// + /// 为 提供自动生成的扩展方法。 + /// public static partial class StatusExtensions { - /// 是否为 Active + /// + /// 判断给定值是否为 。 + /// + /// 要检查的枚举值。 + /// 等于 时返回 ;否则返回 public static bool IsActive(this TestApp.Status value) => value == TestApp.Status.Active; - /// 是否为 Inactive + /// + /// 判断给定值是否为 。 + /// + /// 要检查的枚举值。 + /// 等于 时返回 ;否则返回 public static bool IsInactive(this TestApp.Status value) => value == TestApp.Status.Inactive; } } diff --git a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableIsMethods/Status.EnumExtensions.g.txt b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableIsMethods/Status.EnumExtensions.g.txt index 41154546..dc9086bb 100644 --- a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableIsMethods/Status.EnumExtensions.g.txt +++ b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableIsMethods/Status.EnumExtensions.g.txt @@ -2,9 +2,17 @@ using System; namespace TestApp { + /// + /// 为 提供自动生成的扩展方法。 + /// public static partial class StatusExtensions { - /// 判断是否属于指定集合 + /// + /// 判断给定值是否属于指定候选集合。 + /// + /// 要检查的枚举值。 + /// 用于匹配的候选枚举值集合。 + /// 命中任一候选值时返回 ;否则返回 public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values) { if (values == null) return false; diff --git a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/EnumWithFlagValues/Permissions.EnumExtensions.g.txt b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/EnumWithFlagValues/Permissions.EnumExtensions.g.txt index ebd87525..e99878fd 100644 --- a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/EnumWithFlagValues/Permissions.EnumExtensions.g.txt +++ b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/EnumWithFlagValues/Permissions.EnumExtensions.g.txt @@ -2,21 +2,45 @@ using System; namespace TestApp { + /// + /// 为 提供自动生成的扩展方法。 + /// public static partial class PermissionsExtensions { - /// 是否为 None + /// + /// 判断给定值是否为 。 + /// + /// 要检查的枚举值。 + /// 等于 时返回 ;否则返回 public static bool IsNone(this TestApp.Permissions value) => value == TestApp.Permissions.None; - /// 是否为 Read + /// + /// 判断给定值是否为 。 + /// + /// 要检查的枚举值。 + /// 等于 时返回 ;否则返回 public static bool IsRead(this TestApp.Permissions value) => value == TestApp.Permissions.Read; - /// 是否为 Write + /// + /// 判断给定值是否为 。 + /// + /// 要检查的枚举值。 + /// 等于 时返回 ;否则返回 public static bool IsWrite(this TestApp.Permissions value) => value == TestApp.Permissions.Write; - /// 是否为 Execute + /// + /// 判断给定值是否为 。 + /// + /// 要检查的枚举值。 + /// 等于 时返回 ;否则返回 public static bool IsExecute(this TestApp.Permissions value) => value == TestApp.Permissions.Execute; - /// 判断是否属于指定集合 + /// + /// 判断给定值是否属于指定候选集合。 + /// + /// 要检查的枚举值。 + /// 用于匹配的候选枚举值集合。 + /// 命中任一候选值时返回 ;否则返回 public static bool IsIn(this TestApp.Permissions value, params TestApp.Permissions[] values) { if (values == null) return false; diff --git a/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/CustomFieldName_Class/MyService.Logger.g.cs b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/CustomFieldName_Class/MyService.Logger.g.cs index bc4a50ae..00a34a72 100644 --- a/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/CustomFieldName_Class/MyService.Logger.g.cs +++ b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/CustomFieldName_Class/MyService.Logger.g.cs @@ -4,8 +4,13 @@ using GFramework.Core.Logging; namespace TestApp; +/// +/// 为当前分部类型提供自动生成的日志字段。 +/// partial class MyService { - /// Auto-generated logger + /// + /// 自动生成的日志字段。 + /// private static readonly ILogger MyLogger = LoggerFactoryResolver.Provider.CreateLogger("MyService"); } diff --git a/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/CustomName_Class/MyService.Logger.g.cs b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/CustomName_Class/MyService.Logger.g.cs index 02bcfad9..f355acc6 100644 --- a/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/CustomName_Class/MyService.Logger.g.cs +++ b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/CustomName_Class/MyService.Logger.g.cs @@ -4,8 +4,13 @@ using GFramework.Core.Logging; namespace TestApp; +/// +/// 为当前分部类型提供自动生成的日志字段。 +/// partial class MyService { - /// Auto-generated logger + /// + /// 自动生成的日志字段。 + /// private static readonly ILogger _log = LoggerFactoryResolver.Provider.CreateLogger("MyService"); } diff --git a/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/DefaultConfiguration_Class/MyService.Logger.g.cs b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/DefaultConfiguration_Class/MyService.Logger.g.cs index 02bcfad9..f355acc6 100644 --- a/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/DefaultConfiguration_Class/MyService.Logger.g.cs +++ b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/DefaultConfiguration_Class/MyService.Logger.g.cs @@ -4,8 +4,13 @@ using GFramework.Core.Logging; namespace TestApp; +/// +/// 为当前分部类型提供自动生成的日志字段。 +/// partial class MyService { - /// Auto-generated logger + /// + /// 自动生成的日志字段。 + /// private static readonly ILogger _log = LoggerFactoryResolver.Provider.CreateLogger("MyService"); } diff --git a/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/GenericClass/MyService.Logger.g.cs b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/GenericClass/MyService.Logger.g.cs index f06bd74d..231551aa 100644 --- a/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/GenericClass/MyService.Logger.g.cs +++ b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/GenericClass/MyService.Logger.g.cs @@ -4,8 +4,13 @@ using GFramework.Core.Logging; namespace TestApp; +/// +/// 为当前分部类型提供自动生成的日志字段。 +/// partial class MyService { - /// Auto-generated logger + /// + /// 自动生成的日志字段。 + /// private static readonly ILogger _log = LoggerFactoryResolver.Provider.CreateLogger("MyService"); } diff --git a/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/InstanceField_Class/MyService.Logger.g.cs b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/InstanceField_Class/MyService.Logger.g.cs index 05e566e4..f1e73e53 100644 --- a/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/InstanceField_Class/MyService.Logger.g.cs +++ b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/InstanceField_Class/MyService.Logger.g.cs @@ -4,8 +4,13 @@ using GFramework.Core.Logging; namespace TestApp; +/// +/// 为当前分部类型提供自动生成的日志字段。 +/// partial class MyService { - /// Auto-generated logger + /// + /// 自动生成的日志字段。 + /// private readonly ILogger _log = LoggerFactoryResolver.Provider.CreateLogger("MyService"); } diff --git a/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/PublicField_Class/MyService.Logger.g.cs b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/PublicField_Class/MyService.Logger.g.cs index 19326d76..fef90ca3 100644 --- a/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/PublicField_Class/MyService.Logger.g.cs +++ b/GFramework.SourceGenerators.Tests/Logging/snapshots/LoggerGenerator/PublicField_Class/MyService.Logger.g.cs @@ -4,8 +4,13 @@ using GFramework.Core.Logging; namespace TestApp; +/// +/// 为当前分部类型提供自动生成的日志字段。 +/// partial class MyService { - /// Auto-generated logger + /// + /// 自动生成的日志字段。 + /// public static readonly ILogger _log = LoggerFactoryResolver.Provider.CreateLogger("MyService"); } diff --git a/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs b/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs index 54df8881..ea081e93 100644 --- a/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs +++ b/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs @@ -3,10 +3,14 @@ namespace TestApp; +/// +/// 为当前规则类型补充自动生成的架构上下文访问实现。 +/// partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware { private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _context; private static global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider? _contextProvider; + private static readonly object _contextSync = new(); /// /// 自动获取的架构上下文(懒加载,默认使用 GameContextProvider) @@ -15,13 +19,19 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware { get { - if (_context == null) + var context = _context; + if (context is not null) { - _contextProvider ??= new global::GFramework.Core.Architectures.GameContextProvider(); - _context = _contextProvider.GetContext(); + return context; } - return _context; + // 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。 + lock (_contextSync) + { + _contextProvider ??= new global::GFramework.Core.Architectures.GameContextProvider(); + _context ??= _contextProvider.GetContext(); + return _context; + } } } @@ -31,7 +41,10 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware /// 上下文提供者实例 public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider) { - _contextProvider = provider; + lock (_contextSync) + { + _contextProvider = provider; + } } /// @@ -39,7 +52,10 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware /// public static void ResetContextProvider() { - _contextProvider = null; + lock (_contextSync) + { + _contextProvider = null; + } } void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context) @@ -52,4 +68,4 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware return Context; } -} \ No newline at end of file +} From 9ec83fa56ae90a279c42a31298afa75eb78c5cca Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:44:09 +0800 Subject: [PATCH 4/7] =?UTF-8?q?feat(source-generators):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0CQRS=E5=A4=84=E7=90=86=E5=99=A8=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E5=99=A8=E5=92=8C=E6=9E=9A=E4=B8=BE=E6=89=A9=E5=B1=95=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了CqrsHandlerRegistryGenerator用于自动生成CQRS处理器注册器 - 添加了EnumExtensionsGenerator用于自动生成枚举相关的扩展方法 - 创建了ContextAwareGenerator为标记类自动生成IContextAware接口实现 - 支持运行时类型引用的安全编码和反射回退机制 - 实现了精确的运行时类型引用描述和泛型类型处理 - 添加了完整的诊断报告和错误处理机制 --- .../Enums/EnumExtensionsGenerator.cs | 3 +- .../Rule/ContextAwareGenerator.cs | 44 ++++- .../AnalyzerReleases.Shipped.md | 2 + .../AnalyzerReleases.Unshipped.md | 8 + .../Cqrs/CqrsHandlerRegistryGenerator.cs | 30 ++++ .../Core/GeneratorSnapshotTest.cs | 26 ++- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 160 +++++++++++++++--- .../Status.EnumExtensions.g.txt | 2 +- .../Status.EnumExtensions.g.txt | 2 +- .../Status.EnumExtensions.g.cs | 2 +- .../Status.EnumExtensions.g.txt | 2 +- .../Permissions.EnumExtensions.g.txt | 2 +- .../MyRule.ContextAware.g.cs | 35 +++- 13 files changed, 271 insertions(+), 47 deletions(-) create mode 100644 GFramework.Cqrs.SourceGenerators/AnalyzerReleases.Shipped.md create mode 100644 GFramework.Cqrs.SourceGenerators/AnalyzerReleases.Unshipped.md diff --git a/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs b/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs index 32a4715a..c89f18ca 100644 --- a/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs +++ b/GFramework.Core.SourceGenerators/Enums/EnumExtensionsGenerator.cs @@ -205,7 +205,8 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase builder.AppendLine(" /// 判断给定值是否属于指定候选集合。"); builder.AppendLine(" /// "); builder.AppendLine(" /// 要检查的枚举值。"); - builder.AppendLine(" /// 用于匹配的候选枚举值集合。"); + builder.AppendLine( + " /// 用于匹配的候选枚举值集合;当为 时返回 。"); builder.AppendLine( " /// 命中任一候选值时返回 ;否则返回 "); builder.AppendLine( diff --git a/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs b/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs index 0fd15fb7..86a0c0de 100644 --- a/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs +++ b/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs @@ -99,6 +99,14 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine("/// "); sb.AppendLine("/// 为当前规则类型补充自动生成的架构上下文访问实现。"); sb.AppendLine("/// "); + sb.AppendLine("/// "); + sb.AppendLine( + "/// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 。"); + sb.AppendLine( + "/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例,"); + sb.AppendLine( + "/// 已缓存的实例上下文需要通过 显式覆盖。"); + sb.AppendLine("/// "); sb.AppendLine($"partial class {symbol.Name} : {interfaceName}"); sb.AppendLine("{"); @@ -134,8 +142,18 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine(" private static readonly object _contextSync = new();"); sb.AppendLine(); sb.AppendLine(" /// "); - sb.AppendLine(" /// 自动获取的架构上下文(懒加载,默认使用 GameContextProvider)"); + sb.AppendLine(" /// 获取当前实例绑定的架构上下文。"); sb.AppendLine(" /// "); + sb.AppendLine(" /// "); + sb.AppendLine( + " /// 该属性会先返回通过 IContextAware.SetContext(...) 显式注入的实例上下文;若尚未设置,则在同一个同步域内惰性初始化共享提供者。"); + sb.AppendLine( + " /// 当静态提供者尚未配置时,生成代码会回退到 。"); + sb.AppendLine( + " /// 一旦某个实例成功缓存上下文,后续 "); + sb.AppendLine( + " /// 或 不会自动清除此缓存;如需覆盖,请显式调用 IContextAware.SetContext(...)。"); + sb.AppendLine(" /// "); sb.AppendLine(" protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context"); sb.AppendLine(" {"); sb.AppendLine(" get"); @@ -158,9 +176,15 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); - sb.AppendLine(" /// 配置上下文提供者(用于测试或多架构场景)"); + sb.AppendLine(" /// 配置当前生成类型共享的上下文提供者。"); sb.AppendLine(" /// "); - sb.AppendLine(" /// 上下文提供者实例"); + sb.AppendLine(" /// 后续懒加载上下文时要使用的提供者实例。"); + sb.AppendLine(" /// "); + sb.AppendLine(" /// 该方法使用与 相同的同步锁,避免提供者切换与惰性初始化交错。"); + sb.AppendLine( + " /// 已经缓存上下文的实例不会因为提供者切换而自动失效;该变更仅影响尚未初始化上下文的新实例或未缓存实例。"); + sb.AppendLine(" /// 如需覆盖已有实例的上下文,请显式调用 IContextAware.SetContext(...)。"); + sb.AppendLine(" /// "); sb.AppendLine( " public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)"); sb.AppendLine(" {"); @@ -171,8 +195,14 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" /// "); - sb.AppendLine(" /// 重置上下文提供者为默认值(用于测试清理)"); + sb.AppendLine(" /// 重置共享上下文提供者,使后续懒加载回退到默认提供者。"); sb.AppendLine(" /// "); + sb.AppendLine(" /// "); + sb.AppendLine(" /// 该方法主要用于测试清理或跨用例恢复默认行为。"); + sb.AppendLine( + " /// 它不会清除已经缓存到实例字段中的上下文;只有后续尚未初始化上下文的实例会重新回退到 。"); + sb.AppendLine(" /// 如需覆盖已有实例的上下文,请显式调用 IContextAware.SetContext(...)。"); + sb.AppendLine(" /// "); sb.AppendLine(" public static void ResetContextProvider()"); sb.AppendLine(" {"); sb.AppendLine(" lock (_contextSync)"); @@ -248,7 +278,11 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase switch (method.Name) { case "SetContext": - sb.AppendLine(" _context = context;"); + sb.AppendLine(" // 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。"); + sb.AppendLine(" lock (_contextSync)"); + sb.AppendLine(" {"); + sb.AppendLine(" _context = context;"); + sb.AppendLine(" }"); break; case "GetContext": diff --git a/GFramework.Cqrs.SourceGenerators/AnalyzerReleases.Shipped.md b/GFramework.Cqrs.SourceGenerators/AnalyzerReleases.Shipped.md new file mode 100644 index 00000000..9c6fa74f --- /dev/null +++ b/GFramework.Cqrs.SourceGenerators/AnalyzerReleases.Shipped.md @@ -0,0 +1,2 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md diff --git a/GFramework.Cqrs.SourceGenerators/AnalyzerReleases.Unshipped.md b/GFramework.Cqrs.SourceGenerators/AnalyzerReleases.Unshipped.md new file mode 100644 index 00000000..72d71316 --- /dev/null +++ b/GFramework.Cqrs.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -0,0 +1,8 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + + Rule ID | Category | Severity | Notes +-------------|----------------------------------|----------|------------------------------ + GF_Cqrs_001 | GFramework.Cqrs.SourceGenerators | Error | CqrsHandlerRegistryGenerator diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index a1cb10a6..a337f9ac 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -28,6 +28,14 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator private const string GeneratedTypeName = "__GFrameworkGeneratedCqrsHandlerRegistry"; private const string HintName = "CqrsHandlerRegistry.g.cs"; + private static readonly DiagnosticDescriptor MissingReflectionFallbackContractDiagnostic = new( + "GF_Cqrs_001", + "Cannot emit CQRS registry without reflection fallback contract", + "Cannot generate CQRS handler registry because fallback metadata is required for handler(s): {0}, but runtime contract '{1}' is unavailable", + "GFramework.Cqrs.SourceGenerators", + DiagnosticSeverity.Error, + true); + /// public void Initialize(IncrementalGeneratorInitializationContext context) { @@ -169,6 +177,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator generationEnvironment.SupportsReflectionFallbackAttribute, fallbackHandlerTypeMetadataNames.Length)) { + ReportMissingReflectionFallbackContractDiagnostic( + context, + fallbackHandlerTypeMetadataNames); return; } @@ -197,6 +208,25 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return fallbackHandlerTypeCount == 0 || supportsReflectionFallbackAttribute; } + /// + /// 报告当前轮次因缺少 fallback 元数据承载契约而无法安全生成注册器的诊断。 + /// + /// 源生产上下文。 + /// 需要通过程序集级 reflection fallback 元数据恢复的 handler 元数据名称。 + private static void ReportMissingReflectionFallbackContractDiagnostic( + SourceProductionContext context, + IReadOnlyList fallbackHandlerTypeMetadataNames) + { + var handlerList = string.Join( + ", ", + fallbackHandlerTypeMetadataNames.OrderBy(static name => name, StringComparer.Ordinal)); + context.ReportDiagnostic(Diagnostic.Create( + MissingReflectionFallbackContractDiagnostic, + Location.None, + handlerList, + CqrsReflectionFallbackAttributeMetadataName)); + } + private static List CollectRegistrations( ImmutableArray candidates) { diff --git a/GFramework.SourceGenerators.Tests/Core/GeneratorSnapshotTest.cs b/GFramework.SourceGenerators.Tests/Core/GeneratorSnapshotTest.cs index d8726a2f..fe8b9bff 100644 --- a/GFramework.SourceGenerators.Tests/Core/GeneratorSnapshotTest.cs +++ b/GFramework.SourceGenerators.Tests/Core/GeneratorSnapshotTest.cs @@ -10,12 +10,19 @@ public static class GeneratorSnapshotTest where TGenerator : new() { /// - /// 运行源代码生成器的快照测试 + /// 运行指定源生成器的端到端快照测试。 /// - /// 输入的源代码字符串 - /// 快照文件存储的文件夹路径 + /// 输入的源代码字符串。 + /// 用于存放已提交快照文件的根目录。 /// 将生成文件名映射为快照文件名的规则;为空时使用原始生成文件名。 - /// 异步任务 + /// 当所有生成输出都通过快照校验后完成的异步任务。 + /// + /// 该辅助器会手动构建 Roslyn 编译并执行生成器,然后依次验证生成器自身诊断、更新后编译诊断、生成输出数量和快照内容。 + /// 若生成器报告错误、生成后的编译出现错误、生成器没有任何输出,或首次运行缺少快照文件,测试都会失败。 + /// 首次缺少快照时,本方法会先将当前输出写入 ,再通过断言中断测试,提示调用方提交快照资产。 + /// 的返回值还必须保持在 根目录之内,否则会抛出异常。 + /// + /// 当快照文件名映射结果为空、为绝对路径,或逃逸出快照根目录时抛出。 public static async Task RunAsync( string source, string snapshotFolder, @@ -33,7 +40,16 @@ public static class GeneratorSnapshotTest driver = driver.RunGeneratorsAndUpdateCompilation( compilation, out var updatedCompilation, - out _); + out var generatorDiagnostics); + + var generatorErrors = generatorDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + Assert.That( + generatorErrors, + Is.Empty, + () => + $"执行生成器时出现错误:{Environment.NewLine}{string.Join(Environment.NewLine, generatorErrors.Select(static diagnostic => diagnostic.ToString()))}"); var compilationErrors = updatedCompilation.GetDiagnostics() .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 50483a08..a49025a5 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -1169,28 +1169,95 @@ public class CqrsHandlerRegistryGeneratorTests } /// - /// 验证当某轮生成仍然需要程序集级 reflection fallback 元数据时, - /// 若 runtime 合同未提供对应特性契约,生成器会放弃输出注册器以避免静默漏注册。 + /// 验证当某轮生成仍然需要程序集级 reflection fallback 元数据,且 runtime 合同缺少承载该元数据的特性时, + /// 生成器会给出明确诊断并停止输出注册器。 /// [Test] public void - Rejects_Registry_Emission_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute() + Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute() { - var method = typeof(CqrsHandlerRegistryGenerator).GetMethod( - "CanEmitGeneratedRegistry", - BindingFlags.NonPublic | BindingFlags.Static); + const string source = """ + using System; - Assert.That(method, Is.Not.Null); + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } - var canEmitWithoutFallbackRequirement = (bool?)method!.Invoke(null, [false, 0]); - var canEmitWithSupportedFallbackAttribute = (bool?)method.Invoke(null, [true, 1]); - var canEmitWithoutSupportedFallbackAttribute = (bool?)method.Invoke(null, [false, 1]); + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed class Container + { + private unsafe struct HiddenResponse + { + } + + private unsafe sealed record HiddenRequest() : IRequest; + + public unsafe sealed class HiddenHandler : IRequestHandler + { + } + } + } + """; + + var execution = ExecuteGenerator(source); + var generatorErrors = execution.GeneratorDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + var missingContractDiagnostic = + generatorErrors.SingleOrDefault(static diagnostic => diagnostic.Id == "GF_Cqrs_001"); Assert.Multiple(() => { - Assert.That(canEmitWithoutFallbackRequirement, Is.True); - Assert.That(canEmitWithSupportedFallbackAttribute, Is.True); - Assert.That(canEmitWithoutSupportedFallbackAttribute, Is.False); + Assert.That(execution.GeneratedSources, Is.Empty); + Assert.That(missingContractDiagnostic, Is.Not.Null); + Assert.That( + missingContractDiagnostic!.GetMessage(), + Does.Contain("TestApp.Container+HiddenHandler")); + Assert.That( + missingContractDiagnostic.GetMessage(), + Does.Contain("GFramework.Cqrs.CqrsReflectionFallbackAttribute")); }); } @@ -1219,6 +1286,40 @@ public class CqrsHandlerRegistryGeneratorTests private static string RunGenerator( string source, params MetadataReference[] additionalReferences) + { + var execution = ExecuteGenerator( + source, + additionalReferences); + var generatorErrors = execution.GeneratorDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + Assert.That( + generatorErrors, + Is.Empty, + () => + $"执行生成器时出现错误:{Environment.NewLine}{string.Join(Environment.NewLine, generatorErrors.Select(static diagnostic => diagnostic.ToString()))}"); + var compilationErrors = execution.CompilationDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + Assert.That( + compilationErrors, + Is.Empty, + () => + $"编译生成的代码时出现错误:{Environment.NewLine}{string.Join(Environment.NewLine, compilationErrors.Select(static diagnostic => diagnostic.ToString()))}"); + Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1)); + + return execution.GeneratedSources[0].content; + } + + /// + /// 运行 CQRS handler registry generator,并返回生成输出及相关诊断。 + /// + /// 输入源码。 + /// 附加元数据引用,用于构造跨程序集场景。 + /// 包含生成源、生成器诊断和更新后编译诊断的执行结果。 + private static GeneratorExecutionResult ExecuteGenerator( + string source, + params MetadataReference[] additionalReferences) { var syntaxTree = CSharpSyntaxTree.ParseText(source); var compilation = CSharpCompilation.Create( @@ -1233,21 +1334,28 @@ public class CqrsHandlerRegistryGeneratorTests driver = driver.RunGeneratorsAndUpdateCompilation( compilation, out var updatedCompilation, - out _); - - var compilationErrors = updatedCompilation.GetDiagnostics() - .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) - .ToArray(); - Assert.That( - compilationErrors, - Is.Empty, - () => - $"编译生成的代码时出现错误:{Environment.NewLine}{string.Join(Environment.NewLine, compilationErrors.Select(static diagnostic => diagnostic.ToString()))}"); + out var generatorDiagnostics); var runResult = driver.GetRunResult(); Assert.That(runResult.Results, Has.Length.EqualTo(1)); - Assert.That(runResult.Results[0].GeneratedSources, Has.Length.EqualTo(1)); - - return runResult.Results[0].GeneratedSources[0].SourceText.ToString(); + var generatedSources = runResult.Results[0].GeneratedSources + .Select(static sourceResult => + (filename: sourceResult.HintName, content: sourceResult.SourceText.ToString())) + .ToArray(); + return new GeneratorExecutionResult( + generatedSources, + generatorDiagnostics.ToArray(), + updatedCompilation.GetDiagnostics().ToArray()); } + + /// + /// 封装 CQRS handler registry generator 的单次执行结果。 + /// + /// 本轮生成产生的源文件集合。 + /// 生成器自身报告的诊断集合。 + /// 将生成结果并回编译后的编译诊断集合。 + private sealed record GeneratorExecutionResult( + (string filename, string content)[] GeneratedSources, + Diagnostic[] GeneratorDiagnostics, + Diagnostic[] CompilationDiagnostics); } diff --git a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsInMethod/Status.EnumExtensions.g.txt b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsInMethod/Status.EnumExtensions.g.txt index 89db6bdb..105fdecb 100644 --- a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsInMethod/Status.EnumExtensions.g.txt +++ b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsInMethod/Status.EnumExtensions.g.txt @@ -25,7 +25,7 @@ namespace TestApp /// 判断给定值是否属于指定候选集合。 /// /// 要检查的枚举值。 - /// 用于匹配的候选枚举值集合。 + /// 用于匹配的候选枚举值集合;当为 时返回 。 /// 命中任一候选值时返回 ;否则返回 public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values) { diff --git a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods/Status.EnumExtensions.g.txt b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods/Status.EnumExtensions.g.txt index 1709ecf7..117bcba9 100644 --- a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods/Status.EnumExtensions.g.txt +++ b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods/Status.EnumExtensions.g.txt @@ -32,7 +32,7 @@ namespace TestApp /// 判断给定值是否属于指定候选集合。 /// /// 要检查的枚举值。 - /// 用于匹配的候选枚举值集合。 + /// 用于匹配的候选枚举值集合;当为 时返回 。 /// 命中任一候选值时返回 ;否则返回 public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values) { diff --git a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods_DefaultSnapshotFileNameSelector/Status.EnumExtensions.g.cs b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods_DefaultSnapshotFileNameSelector/Status.EnumExtensions.g.cs index 89db6bdb..105fdecb 100644 --- a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods_DefaultSnapshotFileNameSelector/Status.EnumExtensions.g.cs +++ b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/BasicEnum_IsMethods_DefaultSnapshotFileNameSelector/Status.EnumExtensions.g.cs @@ -25,7 +25,7 @@ namespace TestApp /// 判断给定值是否属于指定候选集合。 /// /// 要检查的枚举值。 - /// 用于匹配的候选枚举值集合。 + /// 用于匹配的候选枚举值集合;当为 时返回 。 /// 命中任一候选值时返回 ;否则返回 public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values) { diff --git a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableIsMethods/Status.EnumExtensions.g.txt b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableIsMethods/Status.EnumExtensions.g.txt index dc9086bb..19e621e6 100644 --- a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableIsMethods/Status.EnumExtensions.g.txt +++ b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/DisableIsMethods/Status.EnumExtensions.g.txt @@ -11,7 +11,7 @@ namespace TestApp /// 判断给定值是否属于指定候选集合。 /// /// 要检查的枚举值。 - /// 用于匹配的候选枚举值集合。 + /// 用于匹配的候选枚举值集合;当为 时返回 。 /// 命中任一候选值时返回 ;否则返回 public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values) { diff --git a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/EnumWithFlagValues/Permissions.EnumExtensions.g.txt b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/EnumWithFlagValues/Permissions.EnumExtensions.g.txt index e99878fd..0026b44c 100644 --- a/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/EnumWithFlagValues/Permissions.EnumExtensions.g.txt +++ b/GFramework.SourceGenerators.Tests/Enums/snapshots/EnumExtensionsGenerator/EnumWithFlagValues/Permissions.EnumExtensions.g.txt @@ -39,7 +39,7 @@ namespace TestApp /// 判断给定值是否属于指定候选集合。 /// /// 要检查的枚举值。 - /// 用于匹配的候选枚举值集合。 + /// 用于匹配的候选枚举值集合;当为 时返回 。 /// 命中任一候选值时返回 ;否则返回 public static bool IsIn(this TestApp.Permissions value, params TestApp.Permissions[] values) { diff --git a/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs b/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs index ea081e93..96e929e2 100644 --- a/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs +++ b/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs @@ -6,6 +6,11 @@ namespace TestApp; /// /// 为当前规则类型补充自动生成的架构上下文访问实现。 /// +/// +/// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 。 +/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例, +/// 已缓存的实例上下文需要通过 显式覆盖。 +/// partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware { private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _context; @@ -13,8 +18,14 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware private static readonly object _contextSync = new(); /// - /// 自动获取的架构上下文(懒加载,默认使用 GameContextProvider) + /// 获取当前实例绑定的架构上下文。 /// + /// + /// 该属性会先返回通过 IContextAware.SetContext(...) 显式注入的实例上下文;若尚未设置,则在同一个同步域内惰性初始化共享提供者。 + /// 当静态提供者尚未配置时,生成代码会回退到 。 + /// 一旦某个实例成功缓存上下文,后续 + /// 或 不会自动清除此缓存;如需覆盖,请显式调用 IContextAware.SetContext(...)。 + /// protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context { get @@ -36,9 +47,14 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware } /// - /// 配置上下文提供者(用于测试或多架构场景) + /// 配置当前生成类型共享的上下文提供者。 /// - /// 上下文提供者实例 + /// 后续懒加载上下文时要使用的提供者实例。 + /// + /// 该方法使用与 相同的同步锁,避免提供者切换与惰性初始化交错。 + /// 已经缓存上下文的实例不会因为提供者切换而自动失效;该变更仅影响尚未初始化上下文的新实例或未缓存实例。 + /// 如需覆盖已有实例的上下文,请显式调用 IContextAware.SetContext(...)。 + /// public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider) { lock (_contextSync) @@ -48,8 +64,13 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware } /// - /// 重置上下文提供者为默认值(用于测试清理) + /// 重置共享上下文提供者,使后续懒加载回退到默认提供者。 /// + /// + /// 该方法主要用于测试清理或跨用例恢复默认行为。 + /// 它不会清除已经缓存到实例字段中的上下文;只有后续尚未初始化上下文的实例会重新回退到 。 + /// 如需覆盖已有实例的上下文,请显式调用 IContextAware.SetContext(...)。 + /// public static void ResetContextProvider() { lock (_contextSync) @@ -60,7 +81,11 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context) { - _context = context; + // 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。 + lock (_contextSync) + { + _context = context; + } } global::GFramework.Core.Abstractions.Architectures.IArchitectureContext global::GFramework.Core.Abstractions.Rule.IContextAware.GetContext() From 57a006caeb6baaf61fd278b1d830f31b23668701 Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 17 Apr 2026 10:58:42 +0800 Subject: [PATCH 5/7] =?UTF-8?q?chore(build):=20=E6=B7=BB=E5=8A=A0=20C#=20?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E8=A7=84=E8=8C=83=E9=AA=8C=E8=AF=81=E8=84=9A?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现了 PascalCase 命名规则检查功能 - 集成了 Git 文件遍历和 grep 文本匹配 - 支持排除特定目录如 Godot 模板和测试快照 - 验证命名空间和目录路径的大小写规范 - 提供详细的违规信息报告和错误定位 - 包含字母数字字符和缩写命名规则校验 --- scripts/validate-csharp-naming.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/validate-csharp-naming.sh b/scripts/validate-csharp-naming.sh index 1a38f049..b6b95130 100644 --- a/scripts/validate-csharp-naming.sh +++ b/scripts/validate-csharp-naming.sh @@ -28,6 +28,11 @@ is_excluded() { Godot/script_templates|Godot/script_templates/*) return 0 ;; + GFramework.SourceGenerators.Tests/*/snapshots|GFramework.SourceGenerators.Tests/*/snapshots/*) + # Source-generator snapshots are committed test assets rather than hand-authored source layout. + # Keep naming enforcement for the real test code, but skip generated snapshot trees. + return 0 + ;; *) return 1 ;; From bcde9f644e286c99ae45b694b12df717159ecb0a Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 17 Apr 2026 11:47:56 +0800 Subject: [PATCH 6/7] =?UTF-8?q?feat(rule):=20=E6=B7=BB=E5=8A=A0=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87=E6=84=9F=E7=9F=A5=E5=9F=BA=E7=B1=BB=E5=92=8C?= =?UTF-8?q?=E7=94=9F=E6=88=90=E5=99=A8=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 ContextAwareBase 抽象类提供手动继承的上下文感知基础实现 - 实现 IContextAware 接口的简单实例字段缓存上下文功能 - 添加 ContextAwareGenerator 源代码生成器自动生成上下文感知实现 - 生成器支持 partial 类的 ContextAware 特性标记自动实现 - 提供 CqrsHandlerRegistryGenerator 生成 CQRS 处理器注册器减少运行时反射扫描 --- .../Rule/ContextAwareGenerator.cs | 10 ++ GFramework.Core/Rule/ContextAwareBase.cs | 34 +++-- .../Cqrs/CqrsHandlerRegistryGenerator.cs | 20 +++ .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 132 +++++++++++++++++- .../MyRule.ContextAware.g.cs | 5 + 5 files changed, 190 insertions(+), 11 deletions(-) diff --git a/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs b/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs index 86a0c0de..b2c4d90e 100644 --- a/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs +++ b/GFramework.Core.SourceGenerators/Rule/ContextAwareGenerator.cs @@ -106,6 +106,10 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase "/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例,"); sb.AppendLine( "/// 已缓存的实例上下文需要通过 显式覆盖。"); + sb.AppendLine( + "/// 与手动继承 的路径相比,生成实现会使用 _contextSync 协调惰性初始化、provider 切换和显式上下文注入;"); + sb.AppendLine( + "/// 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。"); sb.AppendLine("/// "); sb.AppendLine($"partial class {symbol.Name} : {interfaceName}"); sb.AppendLine("{"); @@ -153,6 +157,10 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase " /// 一旦某个实例成功缓存上下文,后续 "); sb.AppendLine( " /// 或 不会自动清除此缓存;如需覆盖,请显式调用 IContextAware.SetContext(...)。"); + sb.AppendLine( + " /// 当前实现还假设 可在持有 _contextSync 时安全执行;"); + sb.AppendLine( + " /// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API,且应避免引入与外部全局锁相互等待的锁顺序。"); sb.AppendLine(" /// "); sb.AppendLine(" protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context"); sb.AppendLine(" {"); @@ -165,6 +173,8 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase sb.AppendLine(" }"); sb.AppendLine(); sb.AppendLine(" // 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。"); + sb.AppendLine( + " // provider 的 GetContext() 会在持有 _contextSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。"); sb.AppendLine(" lock (_contextSync)"); sb.AppendLine(" {"); sb.AppendLine( diff --git a/GFramework.Core/Rule/ContextAwareBase.cs b/GFramework.Core/Rule/ContextAwareBase.cs index 173c4ee4..bf1a0303 100644 --- a/GFramework.Core/Rule/ContextAwareBase.cs +++ b/GFramework.Core/Rule/ContextAwareBase.cs @@ -5,19 +5,33 @@ using GFramework.Core.Architectures; namespace GFramework.Core.Rule; /// -/// 上下文感知基类,实现了IContextAware接口,为需要感知架构上下文的类提供基础实现 +/// 上下文感知基类,实现了 ,为需要感知架构上下文的类提供基础实现。 /// +/// +/// 该基类面向手动继承场景,使用简单的实例字段缓存上下文,不提供额外同步保护。 +/// 与 ContextAwareGenerator 生成的实现不同,它不会维护静态共享的 +/// ,也不会在 / +/// 上加锁。 +/// 若调用方需要跨实例共享 provider、在惰性初始化期间协调 provider 切换,或希望生成代码自动补齐这些约束,应优先使用 +/// [ContextAware] 生成路径;若场景本身由框架主线程驱动,且只需要最小化的实例级上下文缓存,则该基类更直接。 +/// public abstract class ContextAwareBase : IContextAware { /// - /// 获取当前实例的架构上下文 + /// 获取或设置当前实例缓存的架构上下文。 /// + /// + /// 该属性不执行同步;调用方应保证对同一实例的访问遵循其自身线程模型。 + /// protected IArchitectureContext? Context { get; set; } /// - /// 设置架构上下文的实现方法,由框架调用 + /// 设置架构上下文的实现方法,由框架调用。 /// - /// 要设置的架构上下文实例 + /// 要设置的架构上下文实例。 + /// + /// 该实现只做简单赋值,然后调用 ;不与 共享锁。 + /// void IContextAware.SetContext(IArchitectureContext context) { Context = context; @@ -25,9 +39,13 @@ public abstract class ContextAwareBase : IContextAware } /// - /// 获取架构上下文 + /// 获取架构上下文。 /// - /// 当前架构上下文对象 + /// 当前架构上下文对象。 + /// + /// 当 为空时,该实现会直接回退到 。 + /// 该回退过程不执行额外同步,也不支持替换 provider;如需这些能力,请改用生成的 ContextAware 实现。 + /// IArchitectureContext IContextAware.GetContext() { Context ??= GameContext.GetFirstArchitectureContext(); @@ -35,9 +53,9 @@ public abstract class ContextAwareBase : IContextAware } /// - /// 当上下文准备就绪时调用的虚方法,子类可以重写此方法来执行上下文相关的初始化逻辑 + /// 当上下文准备就绪时调用的虚方法,子类可以重写此方法来执行上下文相关的初始化逻辑。 /// protected virtual void OnContextReady() { } -} \ No newline at end of file +} diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index a337f9ac..5be2c5ab 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -558,6 +558,26 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator return GetTypeSortKey(type).Replace("global::", string.Empty); } + /// + /// 生成程序集级 CQRS handler 注册器源码。 + /// + /// + /// 当前轮次的生成环境,用于决定 runtime 是否提供 CqrsReflectionFallbackAttribute 契约,以及是否需要在输出中发射对应的程序集级元数据。 + /// + /// + /// 已整理并排序的 handler 注册描述。方法会据此生成 CqrsHandlerRegistry.g.cs,其中包含直接注册、实现类型反射注册、精确运行时类型查找等分支。 + /// + /// + /// 仍需依赖程序集级 reflection fallback 元数据恢复的 handler 元数据名称集合。 + /// 调用方必须先确保:若该集合非空,则 已声明支持对应的 fallback attribute 契约; + /// 否则应在进入本方法前报告诊断并放弃生成,而不是输出会静默漏注册的半成品注册器。 + /// + /// 完整的注册器源代码文本。 + /// + /// 当 为空时,输出只包含程序集级 CqrsHandlerRegistryAttribute 和注册器实现。 + /// 当其非空且 runtime 合同可用时,输出还会附带程序集级 CqrsReflectionFallbackAttribute,让运行时补齐生成阶段无法精确表达的剩余 handler。 + /// 该方法本身不报告诊断;“fallback 必需但 runtime 契约缺失”的错误由调用方在进入本方法前处理。 + /// private static string GenerateSource( GenerationEnvironment generationEnvironment, IReadOnlyList registrations, diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index a49025a5..4b23a86a 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -1241,7 +1241,9 @@ public class CqrsHandlerRegistryGeneratorTests } """; - var execution = ExecuteGenerator(source); + var execution = ExecuteGenerator( + source, + allowUnsafe: true); var generatorErrors = execution.GeneratorDiagnostics .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ToArray(); @@ -1261,6 +1263,121 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证当 fallback metadata 仍然必需且 runtime 提供了承载契约时, + /// 生成器会继续产出注册器并发射程序集级 CqrsReflectionFallbackAttribute。 + /// + [Test] + public void + Emits_Assembly_Level_Fallback_Metadata_When_Fallback_Is_Required_And_Runtime_Contract_Is_Available() + { + const string source = """ + using System; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + + [AttributeUsage(AttributeTargets.Assembly)] + public sealed class CqrsReflectionFallbackAttribute : Attribute + { + public CqrsReflectionFallbackAttribute(params string[] fallbackHandlerTypeNames) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed class Container + { + private unsafe struct AlphaResponse + { + } + + private unsafe struct BetaResponse + { + } + + private unsafe sealed record AlphaRequest() : IRequest; + + private unsafe sealed record BetaRequest() : IRequest; + + public unsafe sealed class BetaHandler : IRequestHandler + { + } + + public unsafe sealed class AlphaHandler : IRequestHandler + { + } + } + } + """; + + var execution = ExecuteGenerator( + source, + allowUnsafe: true); + var generatorErrors = execution.GeneratorDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); + + Assert.Multiple(() => + { + Assert.That(generatorErrors, Is.Empty); + Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1)); + Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs")); + Assert.That( + execution.GeneratedSources[0].content, + Does.Contain( + "[assembly: global::GFramework.Cqrs.CqrsReflectionFallbackAttribute(\"TestApp.Container+AlphaHandler\", \"TestApp.Container+BetaHandler\")]")); + Assert.That( + execution.GeneratedSources[0].content, + Does.Contain( + "[assembly: global::GFramework.Cqrs.CqrsHandlerRegistryAttribute(typeof(global::GFramework.Generated.Cqrs.__GFrameworkGeneratedCqrsHandlerRegistry))]")); + Assert.That( + execution.GeneratedSources[0].content, + Does.Contain("internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry")); + }); + } + /// /// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。 /// @@ -1289,7 +1406,8 @@ public class CqrsHandlerRegistryGeneratorTests { var execution = ExecuteGenerator( source, - additionalReferences); + allowUnsafe: false, + additionalReferences: additionalReferences); var generatorErrors = execution.GeneratorDiagnostics .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ToArray(); @@ -1315,10 +1433,16 @@ public class CqrsHandlerRegistryGeneratorTests /// 运行 CQRS handler registry generator,并返回生成输出及相关诊断。 /// /// 输入源码。 + /// + /// 是否允许测试编译包含 unsafe 代码。 + /// 某些回归用例会故意构造带指针类型的非法 handler 合同,以覆盖 fallback 防御分支,此时需要启用该选项避免把缺少 + /// unsafe 编译上下文的错误与目标生成器行为混淆。 + /// /// 附加元数据引用,用于构造跨程序集场景。 /// 包含生成源、生成器诊断和更新后编译诊断的执行结果。 private static GeneratorExecutionResult ExecuteGenerator( string source, + bool allowUnsafe = false, params MetadataReference[] additionalReferences) { var syntaxTree = CSharpSyntaxTree.ParseText(source); @@ -1326,7 +1450,9 @@ public class CqrsHandlerRegistryGeneratorTests "TestProject", [syntaxTree], MetadataReferenceTestBuilder.GetRuntimeMetadataReferences().AddRange(additionalReferences), - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + new CSharpCompilationOptions( + OutputKind.DynamicallyLinkedLibrary, + allowUnsafe: allowUnsafe)); GeneratorDriver driver = CSharpGeneratorDriver.Create( generators: [new CqrsHandlerRegistryGenerator().AsSourceGenerator()], diff --git a/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs b/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs index 96e929e2..63e5cb5c 100644 --- a/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs +++ b/GFramework.SourceGenerators.Tests/Rule/snapshots/ContextAwareGenerator/MyRule.ContextAware.g.cs @@ -10,6 +10,8 @@ namespace TestApp; /// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 。 /// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例, /// 已缓存的实例上下文需要通过 显式覆盖。 +/// 与手动继承 的路径相比,生成实现会使用 _contextSync 协调惰性初始化、provider 切换和显式上下文注入; +/// 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。 /// partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware { @@ -25,6 +27,8 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware /// 当静态提供者尚未配置时,生成代码会回退到 。 /// 一旦某个实例成功缓存上下文,后续 /// 或 不会自动清除此缓存;如需覆盖,请显式调用 IContextAware.SetContext(...)。 + /// 当前实现还假设 可在持有 _contextSync 时安全执行; + /// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API,且应避免引入与外部全局锁相互等待的锁顺序。 /// protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context { @@ -37,6 +41,7 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware } // 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。 + // provider 的 GetContext() 会在持有 _contextSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。 lock (_contextSync) { _contextProvider ??= new global::GFramework.Core.Architectures.GameContextProvider(); From b7a476456a377c9dd7b9c56dba3ae71691aeadec Mon Sep 17 00:00:00 2001 From: GeWuYou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 17 Apr 2026 12:58:34 +0800 Subject: [PATCH 7/7] =?UTF-8?q?feat(cqrs):=20=E4=B8=BACQRS=E5=A4=84?= =?UTF-8?q?=E7=90=86=E5=99=A8=E6=B3=A8=E5=86=8C=E7=94=9F=E6=88=90=E5=99=A8?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E8=AF=A6=E7=BB=86=E6=96=87=E6=A1=A3=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E5=B9=B6=E6=94=B9=E8=BF=9B=E6=B5=8B=E8=AF=95=E9=AA=8C?= =?UTF-8?q?=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加了完整的XML文档注释到Execute方法,详细说明了CQRS处理器注册生成器的执行流程、参数含义、实现逻辑和注意事项。同时改进了测试框架,在GeneratorExecutionResult中分离了生成代码的编译诊断,使测试能够更精确地验证生成代码的质量。 --- .../Cqrs/CqrsHandlerRegistryGenerator.cs | 36 ++++++++++++++++++- .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 26 ++++++++++++-- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs index 5be2c5ab..6421e0ca 100644 --- a/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs +++ b/GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs @@ -155,7 +155,41 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator reflectionFallbackHandlerTypeMetadataName); } - private static void Execute(SourceProductionContext context, GenerationEnvironment generationEnvironment, + /// + /// 执行 CQRS handler registry 生成管线的最终发射阶段,负责将候选 handler 分析结果汇总为单个 + /// CqrsHandlerRegistry.g.cs,并在需要时附带程序集级 reflection fallback 元数据。 + /// + /// 用于报告诊断并发射生成源码的源生产上下文。 + /// + /// 当前编译轮次可用的 runtime 合同快照。 + /// 只有当 CQRS 注册器生成所需的基础契约齐备时,才允许继续生成;当存在 + /// CqrsReflectionFallbackAttribute 时,才允许输出依赖 fallback 元数据恢复的注册结果。 + /// + /// + /// 来自语法和语义分析阶段的 handler 候选结果。 + /// 集合中可能包含 占位项,且同一实现类型可能因 partial 声明重复出现,后续会统一去重并聚合。 + /// + /// + /// + /// 该方法负责发射两类生成结果:注册器类型本体,以及在静态类型信息不足时用于运行时补全注册的程序集级 + /// CqrsReflectionFallbackAttribute 元数据。生成这些结果的目标是把可静态确定的 handler 注册尽量前移到编译期, + /// 从而减少运行时程序集扫描成本,同时保留对少数复杂类型形态的兼容回退路径。 + /// + /// + /// 该阶段依赖两个语义前提:一是 runtime 已提供 CQRS 注册器生成所需的基础合同;二是只要存在任何 handler + /// 需要通过 reflection fallback 恢复,就必须同时存在承载该元数据的 + /// CqrsReflectionFallbackAttribute。如果基础合同缺失,生成器会静默跳过本轮发射;如果候选集合去重后没有任何可注册 + /// handler,也会直接跳过 AddSource,避免输出空注册器。 + /// + /// + /// 当 fallback handler 元数据非空但 runtime 缺少 CqrsReflectionFallbackAttribute 时, + /// 该方法会报告 GF_Cqrs_001 并停止发射源码。这样可以避免生成一个表面可用、但会静默漏掉部分 handler 注册的半成品 + /// registry。只有在静态注册结果与 fallback 契约同时成立时,才允许调用 AddSource。 + /// + /// + private static void Execute( + SourceProductionContext context, + GenerationEnvironment generationEnvironment, ImmutableArray candidates) { if (!generationEnvironment.GenerationEnabled) diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 4b23a86a..6db73364 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -1244,6 +1244,9 @@ public class CqrsHandlerRegistryGeneratorTests var execution = ExecuteGenerator( source, allowUnsafe: true); + var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); var generatorErrors = execution.GeneratorDiagnostics .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ToArray(); @@ -1252,6 +1255,7 @@ public class CqrsHandlerRegistryGeneratorTests Assert.Multiple(() => { + Assert.That(generatedCompilationErrors, Is.Empty); Assert.That(execution.GeneratedSources, Is.Empty); Assert.That(missingContractDiagnostic, Is.Not.Null); Assert.That( @@ -1355,12 +1359,16 @@ public class CqrsHandlerRegistryGeneratorTests var execution = ExecuteGenerator( source, allowUnsafe: true); + var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics + .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) + .ToArray(); var generatorErrors = execution.GeneratorDiagnostics .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .ToArray(); Assert.Multiple(() => { + Assert.That(generatedCompilationErrors, Is.Empty); Assert.That(generatorErrors, Is.Empty); Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1)); Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs")); @@ -1464,14 +1472,24 @@ public class CqrsHandlerRegistryGeneratorTests var runResult = driver.GetRunResult(); Assert.That(runResult.Results, Has.Length.EqualTo(1)); + var generatedSyntaxTrees = runResult.Results[0].GeneratedSources + .Select(static sourceResult => sourceResult.SyntaxTree) + .ToHashSet(); var generatedSources = runResult.Results[0].GeneratedSources .Select(static sourceResult => (filename: sourceResult.HintName, content: sourceResult.SourceText.ToString())) .ToArray(); + var compilationDiagnostics = updatedCompilation.GetDiagnostics().ToArray(); + var generatedCompilationDiagnostics = compilationDiagnostics + .Where(diagnostic => + diagnostic.Location.SourceTree is not null && + generatedSyntaxTrees.Contains(diagnostic.Location.SourceTree)) + .ToArray(); return new GeneratorExecutionResult( generatedSources, generatorDiagnostics.ToArray(), - updatedCompilation.GetDiagnostics().ToArray()); + compilationDiagnostics, + generatedCompilationDiagnostics); } /// @@ -1479,9 +1497,11 @@ public class CqrsHandlerRegistryGeneratorTests /// /// 本轮生成产生的源文件集合。 /// 生成器自身报告的诊断集合。 - /// 将生成结果并回编译后的编译诊断集合。 + /// 将生成结果并回编译后的完整编译诊断集合。 + /// 仅来自生成源文件的编译诊断集合。 private sealed record GeneratorExecutionResult( (string filename, string content)[] GeneratedSources, Diagnostic[] GeneratorDiagnostics, - Diagnostic[] CompilationDiagnostics); + Diagnostic[] CompilationDiagnostics, + Diagnostic[] GeneratedCompilationDiagnostics); }