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