feat(rule): 添加上下文感知基类和生成器实现

- 新增 ContextAwareBase 抽象类提供手动继承的上下文感知基础实现
- 实现 IContextAware 接口的简单实例字段缓存上下文功能
- 添加 ContextAwareGenerator 源代码生成器自动生成上下文感知实现
- 生成器支持 partial 类的 ContextAware 特性标记自动实现
- 提供 CqrsHandlerRegistryGenerator 生成 CQRS 处理器注册器减少运行时反射扫描
This commit is contained in:
GeWuYou 2026-04-17 11:47:56 +08:00
parent 57a006caeb
commit bcde9f644e
5 changed files with 190 additions and 11 deletions

View File

@ -106,6 +106,10 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
"/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例,"); "/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例,");
sb.AppendLine( sb.AppendLine(
"/// 已缓存的实例上下文需要通过 <see cref=\"GFramework.Core.Abstractions.Rule.IContextAware.SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext)\" /> 显式覆盖。"); "/// 已缓存的实例上下文需要通过 <see cref=\"GFramework.Core.Abstractions.Rule.IContextAware.SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext)\" /> 显式覆盖。");
sb.AppendLine(
"/// 与手动继承 <see cref=\"global::GFramework.Core.Rule.ContextAwareBase\" /> 的路径相比,生成实现会使用 <c>_contextSync</c> 协调惰性初始化、provider 切换和显式上下文注入;");
sb.AppendLine(
"/// <see cref=\"global::GFramework.Core.Rule.ContextAwareBase\" /> 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。");
sb.AppendLine("/// </remarks>"); sb.AppendLine("/// </remarks>");
sb.AppendLine($"partial class {symbol.Name} : {interfaceName}"); sb.AppendLine($"partial class {symbol.Name} : {interfaceName}");
sb.AppendLine("{"); sb.AppendLine("{");
@ -153,6 +157,10 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
" /// 一旦某个实例成功缓存上下文,后续 <see cref=\"SetContextProvider(GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider)\" />"); " /// 一旦某个实例成功缓存上下文,后续 <see cref=\"SetContextProvider(GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider)\" />");
sb.AppendLine( sb.AppendLine(
" /// 或 <see cref=\"ResetContextProvider\" /> 不会自动清除此缓存;如需覆盖,请显式调用 <c>IContextAware.SetContext(...)</c>。"); " /// 或 <see cref=\"ResetContextProvider\" /> 不会自动清除此缓存;如需覆盖,请显式调用 <c>IContextAware.SetContext(...)</c>。");
sb.AppendLine(
" /// 当前实现还假设 <see cref=\"GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext\" /> 可在持有 <c>_contextSync</c> 时安全执行;");
sb.AppendLine(
" /// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API且应避免引入与外部全局锁相互等待的锁顺序。");
sb.AppendLine(" /// </remarks>"); sb.AppendLine(" /// </remarks>");
sb.AppendLine(" protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context"); sb.AppendLine(" protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context");
sb.AppendLine(" {"); sb.AppendLine(" {");
@ -165,6 +173,8 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
sb.AppendLine(" }"); sb.AppendLine(" }");
sb.AppendLine(); sb.AppendLine();
sb.AppendLine(" // 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。"); sb.AppendLine(" // 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。");
sb.AppendLine(
" // provider 的 GetContext() 会在持有 _contextSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。");
sb.AppendLine(" lock (_contextSync)"); sb.AppendLine(" lock (_contextSync)");
sb.AppendLine(" {"); sb.AppendLine(" {");
sb.AppendLine( sb.AppendLine(

View File

@ -5,19 +5,33 @@ using GFramework.Core.Architectures;
namespace GFramework.Core.Rule; namespace GFramework.Core.Rule;
/// <summary> /// <summary>
/// 上下文感知基类,实现了IContextAware接口,为需要感知架构上下文的类提供基础实现 /// 上下文感知基类,实现了 <see cref="IContextAware" />,为需要感知架构上下文的类提供基础实现
/// </summary> /// </summary>
/// <remarks>
/// 该基类面向手动继承场景,使用简单的实例字段缓存上下文,不提供额外同步保护。
/// 与 <c>ContextAwareGenerator</c> 生成的实现不同,它不会维护静态共享的
/// <see cref="IArchitectureContextProvider" />,也不会在 <see cref="IContextAware.SetContext" /> /
/// <see cref="IContextAware.GetContext" /> 上加锁。
/// 若调用方需要跨实例共享 provider、在惰性初始化期间协调 provider 切换,或希望生成代码自动补齐这些约束,应优先使用
/// <c>[ContextAware]</c> 生成路径;若场景本身由框架主线程驱动,且只需要最小化的实例级上下文缓存,则该基类更直接。
/// </remarks>
public abstract class ContextAwareBase : IContextAware public abstract class ContextAwareBase : IContextAware
{ {
/// <summary> /// <summary>
/// 获取当前实例的架构上下文 /// 获取或设置当前实例缓存的架构上下文
/// </summary> /// </summary>
/// <remarks>
/// 该属性不执行同步;调用方应保证对同一实例的访问遵循其自身线程模型。
/// </remarks>
protected IArchitectureContext? Context { get; set; } protected IArchitectureContext? Context { get; set; }
/// <summary> /// <summary>
/// 设置架构上下文的实现方法,由框架调用 /// 设置架构上下文的实现方法,由框架调用
/// </summary> /// </summary>
/// <param name="context">要设置的架构上下文实例</param> /// <param name="context">要设置的架构上下文实例。</param>
/// <remarks>
/// 该实现只做简单赋值,然后调用 <see cref="OnContextReady" />;不与 <see cref="IContextAware.GetContext" /> 共享锁。
/// </remarks>
void IContextAware.SetContext(IArchitectureContext context) void IContextAware.SetContext(IArchitectureContext context)
{ {
Context = context; Context = context;
@ -25,9 +39,13 @@ public abstract class ContextAwareBase : IContextAware
} }
/// <summary> /// <summary>
/// 获取架构上下文 /// 获取架构上下文
/// </summary> /// </summary>
/// <returns>当前架构上下文对象</returns> /// <returns>当前架构上下文对象。</returns>
/// <remarks>
/// 当 <see cref="Context" /> 为空时,该实现会直接回退到 <see cref="GameContext.GetFirstArchitectureContext" />。
/// 该回退过程不执行额外同步,也不支持替换 provider如需这些能力请改用生成的 ContextAware 实现。
/// </remarks>
IArchitectureContext IContextAware.GetContext() IArchitectureContext IContextAware.GetContext()
{ {
Context ??= GameContext.GetFirstArchitectureContext(); Context ??= GameContext.GetFirstArchitectureContext();
@ -35,9 +53,9 @@ public abstract class ContextAwareBase : IContextAware
} }
/// <summary> /// <summary>
/// 当上下文准备就绪时调用的虚方法,子类可以重写此方法来执行上下文相关的初始化逻辑 /// 当上下文准备就绪时调用的虚方法,子类可以重写此方法来执行上下文相关的初始化逻辑
/// </summary> /// </summary>
protected virtual void OnContextReady() protected virtual void OnContextReady()
{ {
} }
} }

View File

@ -558,6 +558,26 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
return GetTypeSortKey(type).Replace("global::", string.Empty); return GetTypeSortKey(type).Replace("global::", string.Empty);
} }
/// <summary>
/// 生成程序集级 CQRS handler 注册器源码。
/// </summary>
/// <param name="generationEnvironment">
/// 当前轮次的生成环境,用于决定 runtime 是否提供 <c>CqrsReflectionFallbackAttribute</c> 契约,以及是否需要在输出中发射对应的程序集级元数据。
/// </param>
/// <param name="registrations">
/// 已整理并排序的 handler 注册描述。方法会据此生成 <c>CqrsHandlerRegistry.g.cs</c>,其中包含直接注册、实现类型反射注册、精确运行时类型查找等分支。
/// </param>
/// <param name="fallbackHandlerTypeMetadataNames">
/// 仍需依赖程序集级 reflection fallback 元数据恢复的 handler 元数据名称集合。
/// 调用方必须先确保:若该集合非空,则 <paramref name="generationEnvironment" /> 已声明支持对应的 fallback attribute 契约;
/// 否则应在进入本方法前报告诊断并放弃生成,而不是输出会静默漏注册的半成品注册器。
/// </param>
/// <returns>完整的注册器源代码文本。</returns>
/// <remarks>
/// 当 <paramref name="fallbackHandlerTypeMetadataNames" /> 为空时,输出只包含程序集级 <c>CqrsHandlerRegistryAttribute</c> 和注册器实现。
/// 当其非空且 runtime 合同可用时,输出还会附带程序集级 <c>CqrsReflectionFallbackAttribute</c>,让运行时补齐生成阶段无法精确表达的剩余 handler。
/// 该方法本身不报告诊断“fallback 必需但 runtime 契约缺失”的错误由调用方在进入本方法前处理。
/// </remarks>
private static string GenerateSource( private static string GenerateSource(
GenerationEnvironment generationEnvironment, GenerationEnvironment generationEnvironment,
IReadOnlyList<ImplementationRegistrationSpec> registrations, IReadOnlyList<ImplementationRegistrationSpec> registrations,

View File

@ -1241,7 +1241,9 @@ public class CqrsHandlerRegistryGeneratorTests
} }
"""; """;
var execution = ExecuteGenerator(source); var execution = ExecuteGenerator(
source,
allowUnsafe: true);
var generatorErrors = execution.GeneratorDiagnostics var generatorErrors = execution.GeneratorDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray(); .ToArray();
@ -1261,6 +1263,121 @@ public class CqrsHandlerRegistryGeneratorTests
}); });
} }
/// <summary>
/// 验证当 fallback metadata 仍然必需且 runtime 提供了承载契约时,
/// 生成器会继续产出注册器并发射程序集级 <c>CqrsReflectionFallbackAttribute</c>。
/// </summary>
[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<TResponse> { }
public interface INotification { }
public interface IStreamRequest<TResponse> { }
public interface IRequestHandler<in TRequest, TResponse> where TRequest : IRequest<TResponse> { }
public interface INotificationHandler<in TNotification> where TNotification : INotification { }
public interface IStreamRequestHandler<in TRequest, out TResponse> where TRequest : IStreamRequest<TResponse> { }
}
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<AlphaResponse*>;
private unsafe sealed record BetaRequest() : IRequest<BetaResponse*>;
public unsafe sealed class BetaHandler : IRequestHandler<BetaRequest, BetaResponse*>
{
}
public unsafe sealed class AlphaHandler : IRequestHandler<AlphaRequest, AlphaResponse*>
{
}
}
}
""";
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"));
});
}
/// <summary> /// <summary>
/// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。 /// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。
/// </summary> /// </summary>
@ -1289,7 +1406,8 @@ public class CqrsHandlerRegistryGeneratorTests
{ {
var execution = ExecuteGenerator( var execution = ExecuteGenerator(
source, source,
additionalReferences); allowUnsafe: false,
additionalReferences: additionalReferences);
var generatorErrors = execution.GeneratorDiagnostics var generatorErrors = execution.GeneratorDiagnostics
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error) .Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
.ToArray(); .ToArray();
@ -1315,10 +1433,16 @@ public class CqrsHandlerRegistryGeneratorTests
/// 运行 CQRS handler registry generator并返回生成输出及相关诊断。 /// 运行 CQRS handler registry generator并返回生成输出及相关诊断。
/// </summary> /// </summary>
/// <param name="source">输入源码。</param> /// <param name="source">输入源码。</param>
/// <param name="allowUnsafe">
/// 是否允许测试编译包含 <c>unsafe</c> 代码。
/// 某些回归用例会故意构造带指针类型的非法 handler 合同,以覆盖 fallback 防御分支,此时需要启用该选项避免把缺少
/// <c>unsafe</c> 编译上下文的错误与目标生成器行为混淆。
/// </param>
/// <param name="additionalReferences">附加元数据引用,用于构造跨程序集场景。</param> /// <param name="additionalReferences">附加元数据引用,用于构造跨程序集场景。</param>
/// <returns>包含生成源、生成器诊断和更新后编译诊断的执行结果。</returns> /// <returns>包含生成源、生成器诊断和更新后编译诊断的执行结果。</returns>
private static GeneratorExecutionResult ExecuteGenerator( private static GeneratorExecutionResult ExecuteGenerator(
string source, string source,
bool allowUnsafe = false,
params MetadataReference[] additionalReferences) params MetadataReference[] additionalReferences)
{ {
var syntaxTree = CSharpSyntaxTree.ParseText(source); var syntaxTree = CSharpSyntaxTree.ParseText(source);
@ -1326,7 +1450,9 @@ public class CqrsHandlerRegistryGeneratorTests
"TestProject", "TestProject",
[syntaxTree], [syntaxTree],
MetadataReferenceTestBuilder.GetRuntimeMetadataReferences().AddRange(additionalReferences), MetadataReferenceTestBuilder.GetRuntimeMetadataReferences().AddRange(additionalReferences),
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); new CSharpCompilationOptions(
OutputKind.DynamicallyLinkedLibrary,
allowUnsafe: allowUnsafe));
GeneratorDriver driver = CSharpGeneratorDriver.Create( GeneratorDriver driver = CSharpGeneratorDriver.Create(
generators: [new CqrsHandlerRegistryGenerator().AsSourceGenerator()], generators: [new CqrsHandlerRegistryGenerator().AsSourceGenerator()],

View File

@ -10,6 +10,8 @@ namespace TestApp;
/// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。 /// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例, /// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例,
/// 已缓存的实例上下文需要通过 <see cref="GFramework.Core.Abstractions.Rule.IContextAware.SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext)" /> 显式覆盖。 /// 已缓存的实例上下文需要通过 <see cref="GFramework.Core.Abstractions.Rule.IContextAware.SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext)" /> 显式覆盖。
/// 与手动继承 <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 的路径相比,生成实现会使用 <c>_contextSync</c> 协调惰性初始化、provider 切换和显式上下文注入;
/// <see cref="global::GFramework.Core.Rule.ContextAwareBase" /> 则保持无锁的实例级缓存语义,更适合已经由调用方线程模型保证串行访问的简单场景。
/// </remarks> /// </remarks>
partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
{ {
@ -25,6 +27,8 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
/// 当静态提供者尚未配置时,生成代码会回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。 /// 当静态提供者尚未配置时,生成代码会回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
/// 一旦某个实例成功缓存上下文,后续 <see cref="SetContextProvider(GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider)" /> /// 一旦某个实例成功缓存上下文,后续 <see cref="SetContextProvider(GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider)" />
/// 或 <see cref="ResetContextProvider" /> 不会自动清除此缓存;如需覆盖,请显式调用 <c>IContextAware.SetContext(...)</c>。 /// 或 <see cref="ResetContextProvider" /> 不会自动清除此缓存;如需覆盖,请显式调用 <c>IContextAware.SetContext(...)</c>。
/// 当前实现还假设 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext" /> 可在持有 <c>_contextSync</c> 时安全执行;
/// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API且应避免引入与外部全局锁相互等待的锁顺序。
/// </remarks> /// </remarks>
protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context
{ {
@ -37,6 +41,7 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
} }
// 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。 // 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。
// provider 的 GetContext() 会在持有 _contextSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。
lock (_contextSync) lock (_contextSync)
{ {
_contextProvider ??= new global::GFramework.Core.Architectures.GameContextProvider(); _contextProvider ??= new global::GFramework.Core.Architectures.GameContextProvider();