Merge pull request #244 from GeWuYou/refactor/cqrs-and-config-system

Refactor/重构CQRS处理程序注册生成器以支持反射回退属性
This commit is contained in:
gewuyou 2026-04-17 13:15:24 +08:00 committed by GitHub
commit 59dfb68add
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 1285 additions and 357 deletions

View File

@ -118,6 +118,9 @@ public sealed class PriorityGenerator : MetadataAttributeClassGeneratorBase
? $"<{string.Join(", ", symbol.TypeParameters.Select(tp => tp.Name))}>"
: string.Empty;
sb.AppendLine("/// <summary>");
sb.AppendLine("/// 为当前分部类型补充自动生成的优先级契约实现。");
sb.AppendLine("/// </summary>");
sb.AppendLine(
$"partial class {symbol.Name}{typeParameters} : global::GFramework.Core.Abstractions.Bases.IPrioritized");
sb.AppendLine("{");

View File

@ -95,6 +95,9 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase
sb.AppendLine("{");
sb.AppendLine(" /// <summary>");
sb.AppendLine($" /// 为 <see cref=\"{fullEnumName}\" /> 提供自动生成的扩展方法。");
sb.AppendLine(" /// </summary>");
sb.AppendLine($" public static partial class {enumName}Extensions");
sb.AppendLine(" {");
@ -176,7 +179,13 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase
builder.AppendLine();
}
builder.AppendLine($" /// <summary>是否为 {memberName}</summary>");
builder.AppendLine(" /// <summary>");
builder.AppendLine(
$" /// 判断给定值是否为 <see cref=\"{fullEnumName}.{memberName}\" />。");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" /// <param name=\"value\">要检查的枚举值。</param>");
builder.AppendLine(
$" /// <returns>当 <paramref name=\"value\" /> 等于 <see cref=\"{fullEnumName}.{memberName}\" /> 时返回 <see langword=\"true\" />;否则返回 <see langword=\"false\" />。</returns>");
builder.AppendLine(
$" public static bool Is{memberName}(this {fullEnumName} value) => value == {fullEnumName}.{memberName};");
hasGeneratedMembers = true;
@ -192,7 +201,14 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase
/// <param name="fullEnumName">枚举的完整类型名。</param>
private static void AppendIsInMethod(StringBuilder builder, string fullEnumName)
{
builder.AppendLine(" /// <summary>判断是否属于指定集合</summary>");
builder.AppendLine(" /// <summary>");
builder.AppendLine(" /// 判断给定值是否属于指定候选集合。");
builder.AppendLine(" /// </summary>");
builder.AppendLine(" /// <param name=\"value\">要检查的枚举值。</param>");
builder.AppendLine(
" /// <param name=\"values\">用于匹配的候选枚举值集合;当为 <see langword=\"null\" /> 时返回 <see langword=\"false\" />。</param>");
builder.AppendLine(
" /// <returns>当 <paramref name=\"value\" /> 命中任一候选值时返回 <see langword=\"true\" />;否则返回 <see langword=\"false\" />。</returns>");
builder.AppendLine(
$" public static bool IsIn(this {fullEnumName} value, params {fullEnumName}[] values)");
builder.AppendLine(" {");

View File

@ -71,13 +71,18 @@ public sealed class LoggerGenerator : TypeAttributeClassGeneratorBase
.AppendLine($"namespace {ns};");
sb.AppendLine()
.AppendLine("/// <summary>")
.AppendLine("/// 为当前分部类型提供自动生成的日志字段。")
.AppendLine("/// </summary>")
.AppendLine($"partial {typeKind} {className}{generics.Parameters}");
foreach (var c in generics.Constraints)
sb.AppendLine($" {c}");
sb.AppendLine("{")
.AppendLine(" /// <summary>Auto-generated logger</summary>")
.AppendLine(" /// <summary>")
.AppendLine(" /// 自动生成的日志字段。")
.AppendLine(" /// </summary>")
.AppendLine(
$" {access} {staticKeyword}readonly ILogger {fieldName} = " +
$"LoggerFactoryResolver.Provider.CreateLogger(\"{logName}\");")

View File

@ -96,6 +96,21 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
var interfaceName = iContextAware.ToDisplayString(
SymbolDisplayFormat.FullyQualifiedFormat);
sb.AppendLine("/// <summary>");
sb.AppendLine("/// 为当前规则类型补充自动生成的架构上下文访问实现。");
sb.AppendLine("/// </summary>");
sb.AppendLine("/// <remarks>");
sb.AppendLine(
"/// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 <see cref=\"GFramework.Core.Architectures.GameContextProvider\" />。");
sb.AppendLine(
"/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例,");
sb.AppendLine(
"/// 已缓存的实例上下文需要通过 <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($"partial class {symbol.Name} : {interfaceName}");
sb.AppendLine("{");
@ -128,41 +143,82 @@ 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(" /// <summary>");
sb.AppendLine(" /// 自动获取的架构上下文(懒加载,默认使用 GameContextProvider");
sb.AppendLine(" /// 获取当前实例绑定的架构上下文。");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" /// <remarks>");
sb.AppendLine(
" /// 该属性会先返回通过 <c>IContextAware.SetContext(...)</c> 显式注入的实例上下文;若尚未设置,则在同一个同步域内惰性初始化共享提供者。");
sb.AppendLine(
" /// 当静态提供者尚未配置时,生成代码会回退到 <see cref=\"GFramework.Core.Architectures.GameContextProvider\" />。");
sb.AppendLine(
" /// 一旦某个实例成功缓存上下文,后续 <see cref=\"SetContextProvider(GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider)\" />");
sb.AppendLine(
" /// 或 <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(" protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context");
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(
" // provider 的 GetContext() 会在持有 _contextSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。");
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();
sb.AppendLine(" /// <summary>");
sb.AppendLine(" /// 配置上下文提供者(用于测试或多架构场景)");
sb.AppendLine(" /// 配置当前生成类型共享的上下文提供者。");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" /// <param name=\"provider\">上下文提供者实例</param>");
sb.AppendLine(" /// <param name=\"provider\">后续懒加载上下文时要使用的提供者实例。</param>");
sb.AppendLine(" /// <remarks>");
sb.AppendLine(" /// 该方法使用与 <see cref=\"Context\" /> 相同的同步锁,避免提供者切换与惰性初始化交错。");
sb.AppendLine(
" /// 已经缓存上下文的实例不会因为提供者切换而自动失效;该变更仅影响尚未初始化上下文的新实例或未缓存实例。");
sb.AppendLine(" /// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。");
sb.AppendLine(" /// </remarks>");
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(" /// <summary>");
sb.AppendLine(" /// 重置上下文提供者为默认值(用于测试清理)");
sb.AppendLine(" /// 重置共享上下文提供者,使后续懒加载回退到默认提供者。");
sb.AppendLine(" /// </summary>");
sb.AppendLine(" /// <remarks>");
sb.AppendLine(" /// 该方法主要用于测试清理或跨用例恢复默认行为。");
sb.AppendLine(
" /// 它不会清除已经缓存到实例字段中的上下文;只有后续尚未初始化上下文的实例会重新回退到 <see cref=\"GFramework.Core.Architectures.GameContextProvider\" />。");
sb.AppendLine(" /// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。");
sb.AppendLine(" /// </remarks>");
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();
}
@ -232,7 +288,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":

View File

@ -5,19 +5,33 @@ using GFramework.Core.Architectures;
namespace GFramework.Core.Rule;
/// <summary>
/// 上下文感知基类,实现了IContextAware接口,为需要感知架构上下文的类提供基础实现
/// 上下文感知基类,实现了 <see cref="IContextAware" />,为需要感知架构上下文的类提供基础实现
/// </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
{
/// <summary>
/// 获取当前实例的架构上下文
/// 获取或设置当前实例缓存的架构上下文
/// </summary>
/// <remarks>
/// 该属性不执行同步;调用方应保证对同一实例的访问遵循其自身线程模型。
/// </remarks>
protected IArchitectureContext? Context { get; set; }
/// <summary>
/// 设置架构上下文的实现方法,由框架调用
/// 设置架构上下文的实现方法,由框架调用
/// </summary>
/// <param name="context">要设置的架构上下文实例</param>
/// <param name="context">要设置的架构上下文实例。</param>
/// <remarks>
/// 该实现只做简单赋值,然后调用 <see cref="OnContextReady" />;不与 <see cref="IContextAware.GetContext" /> 共享锁。
/// </remarks>
void IContextAware.SetContext(IArchitectureContext context)
{
Context = context;
@ -25,9 +39,13 @@ public abstract class ContextAwareBase : IContextAware
}
/// <summary>
/// 获取架构上下文
/// 获取架构上下文
/// </summary>
/// <returns>当前架构上下文对象</returns>
/// <returns>当前架构上下文对象。</returns>
/// <remarks>
/// 当 <see cref="Context" /> 为空时,该实现会直接回退到 <see cref="GameContext.GetFirstArchitectureContext" />。
/// 该回退过程不执行额外同步,也不支持替换 provider如需这些能力请改用生成的 ContextAware 实现。
/// </remarks>
IArchitectureContext IContextAware.GetContext()
{
Context ??= GameContext.GetFirstArchitectureContext();
@ -35,9 +53,9 @@ public abstract class ContextAwareBase : IContextAware
}
/// <summary>
/// 当上下文准备就绪时调用的虚方法,子类可以重写此方法来执行上下文相关的初始化逻辑
/// 当上下文准备就绪时调用的虚方法,子类可以重写此方法来执行上下文相关的初始化逻辑
/// </summary>
protected virtual void OnContextReady()
{
}
}
}

View File

@ -0,0 +1,2 @@
; Shipped analyzer releases
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md

View File

@ -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

View File

@ -19,12 +19,23 @@ 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";
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);
/// <inheritdoc />
public void Initialize(IncrementalGeneratorInitializationContext context)
{
@ -53,8 +64,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 +105,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
ImmutableArray.CreateBuilder<ReflectedImplementationRegistrationSpec>(handlerInterfaces.Length);
var preciseReflectedRegistrations =
ImmutableArray.CreateBuilder<PreciseReflectedRegistrationSpec>(handlerInterfaces.Length);
var runtimeDiscoveredHandlerInterfaceLogNames =
ImmutableArray.CreateBuilder<string>(handlerInterfaces.Length);
var requiresRuntimeInterfaceDiscovery = false;
string? reflectionFallbackHandlerTypeMetadataName = null;
foreach (var handlerInterface in handlerInterfaces)
{
var canReferenceHandlerInterface =
@ -126,11 +137,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,11 +152,44 @@ 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,
/// <summary>
/// 执行 CQRS handler registry 生成管线的最终发射阶段,负责将候选 handler 分析结果汇总为单个
/// <c>CqrsHandlerRegistry.g.cs</c>,并在需要时附带程序集级 reflection fallback 元数据。
/// </summary>
/// <param name="context">用于报告诊断并发射生成源码的源生产上下文。</param>
/// <param name="generationEnvironment">
/// 当前编译轮次可用的 runtime 合同快照。
/// 只有当 CQRS 注册器生成所需的基础契约齐备时,才允许继续生成;当存在
/// <c>CqrsReflectionFallbackAttribute</c> 时,才允许输出依赖 fallback 元数据恢复的注册结果。
/// </param>
/// <param name="candidates">
/// 来自语法和语义分析阶段的 handler 候选结果。
/// 集合中可能包含 <see langword="null" /> 占位项,且同一实现类型可能因 partial 声明重复出现,后续会统一去重并聚合。
/// </param>
/// <remarks>
/// <para>
/// 该方法负责发射两类生成结果:注册器类型本体,以及在静态类型信息不足时用于运行时补全注册的程序集级
/// <c>CqrsReflectionFallbackAttribute</c> 元数据。生成这些结果的目标是把可静态确定的 handler 注册尽量前移到编译期,
/// 从而减少运行时程序集扫描成本,同时保留对少数复杂类型形态的兼容回退路径。
/// </para>
/// <para>
/// 该阶段依赖两个语义前提:一是 runtime 已提供 CQRS 注册器生成所需的基础合同;二是只要存在任何 handler
/// 需要通过 reflection fallback 恢复,就必须同时存在承载该元数据的
/// <c>CqrsReflectionFallbackAttribute</c>。如果基础合同缺失,生成器会静默跳过本轮发射;如果候选集合去重后没有任何可注册
/// handler也会直接跳过 <c>AddSource</c>,避免输出空注册器。
/// </para>
/// <para>
/// 当 fallback handler 元数据非空但 runtime 缺少 <c>CqrsReflectionFallbackAttribute</c> 时,
/// 该方法会报告 <c>GF_Cqrs_001</c> 并停止发射源码。这样可以避免生成一个表面可用、但会静默漏掉部分 handler 注册的半成品
/// registry。只有在静态注册结果与 fallback 契约同时成立时,才允许调用 <c>AddSource</c>。
/// </para>
/// </remarks>
private static void Execute(
SourceProductionContext context,
GenerationEnvironment generationEnvironment,
ImmutableArray<HandlerCandidateAnalysis?> candidates)
{
if (!generationEnvironment.GenerationEnabled)
@ -155,9 +200,65 @@ 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<string>()
.ToArray();
if (!CanEmitGeneratedRegistry(
generationEnvironment.SupportsReflectionFallbackAttribute,
fallbackHandlerTypeMetadataNames.Length))
{
ReportMissingReflectionFallbackContractDiagnostic(
context,
fallbackHandlerTypeMetadataNames);
return;
}
context.AddSource(
HintName,
GenerateSource(registrations));
GenerateSource(generationEnvironment, registrations, fallbackHandlerTypeMetadataNames));
}
/// <summary>
/// 判断当前轮次是否允许输出生成注册器。
/// </summary>
/// <param name="supportsReflectionFallbackAttribute">
/// runtime 合同中是否存在 <c>CqrsReflectionFallbackAttribute</c>,以承载生成器无法静态精确表达的 handler 回退元数据。
/// </param>
/// <param name="fallbackHandlerTypeCount">
/// 当前轮次需要依赖程序集级 reflection fallback 元数据恢复的 handler 数量。
/// </param>
/// <returns>
/// 当没有 handler 依赖 fallback或 runtime 已提供承载该元数据的特性契约时返回 <see langword="true" />
/// 否则返回 <see langword="false" />,调用方必须放弃生成以避免输出会静默漏注册的半成品注册器。
/// </returns>
private static bool CanEmitGeneratedRegistry(
bool supportsReflectionFallbackAttribute,
int fallbackHandlerTypeCount)
{
return fallbackHandlerTypeCount == 0 || supportsReflectionFallbackAttribute;
}
/// <summary>
/// 报告当前轮次因缺少 fallback 元数据承载契约而无法安全生成注册器的诊断。
/// </summary>
/// <param name="context">源生产上下文。</param>
/// <param name="fallbackHandlerTypeMetadataNames">需要通过程序集级 reflection fallback 元数据恢复的 handler 元数据名称。</param>
private static void ReportMissingReflectionFallbackContractDiagnostic(
SourceProductionContext context,
IReadOnlyList<string> fallbackHandlerTypeMetadataNames)
{
var handlerList = string.Join(
", ",
fallbackHandlerTypeMetadataNames.OrderBy(static name => name, StringComparer.Ordinal));
context.ReportDiagnostic(Diagnostic.Create(
MissingReflectionFallbackContractDiagnostic,
Location.None,
handlerList,
CqrsReflectionFallbackAttributeMetadataName));
}
private static List<ImplementationRegistrationSpec> CollectRegistrations(
@ -186,8 +287,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
candidate.ReflectedImplementationRegistrations,
candidate.PreciseReflectedRegistrations,
candidate.ReflectionTypeMetadataName,
candidate.RequiresRuntimeInterfaceDiscovery,
candidate.RuntimeDiscoveredHandlerInterfaceLogNames));
candidate.ReflectionFallbackHandlerTypeMetadataName));
}
registrations.Sort(static (left, right) =>
@ -492,15 +592,35 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
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(
IReadOnlyList<ImplementationRegistrationSpec> registrations)
GenerationEnvironment generationEnvironment,
IReadOnlyList<ImplementationRegistrationSpec> registrations,
IReadOnlyList<string> 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 +628,25 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
builder.AppendLine("// <auto-generated />");
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 +694,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 +706,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 +782,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 +804,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<global::System.Type>();");
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 +828,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 +854,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
preciseRegistration.OpenHandlerTypeDisplayName,
registration.ImplementationLogName,
preciseRegistration.HandlerInterfaceLogName,
knownServiceTypesVariableName,
registration.RequiresRuntimeInterfaceDiscovery,
3);
break;
default:
@ -762,15 +862,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 +873,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 +937,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 +1025,6 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
private static void AppendReflectionHelpers(
StringBuilder builder,
bool includeRuntimeInterfaceDiscoveryHelpers,
bool includeExternalAssemblyTypeLookupHelpers)
{
if (includeExternalAssemblyTypeLookupHelpers)
@ -991,123 +1070,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<global::System.Type> 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 +1180,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
ImmutableArray<ReflectedImplementationRegistrationSpec> ReflectedImplementationRegistrations,
ImmutableArray<PreciseReflectedRegistrationSpec> PreciseReflectedRegistrations,
string? ReflectionTypeMetadataName,
bool RequiresRuntimeInterfaceDiscovery,
ImmutableArray<string> RuntimeDiscoveredHandlerInterfaceLogNames);
string? ReflectionFallbackHandlerTypeMetadataName);
private readonly struct HandlerCandidateAnalysis : IEquatable<HandlerCandidateAnalysis>
{
@ -1230,8 +1191,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
ImmutableArray<ReflectedImplementationRegistrationSpec> reflectedImplementationRegistrations,
ImmutableArray<PreciseReflectedRegistrationSpec> preciseReflectedRegistrations,
string? reflectionTypeMetadataName,
bool requiresRuntimeInterfaceDiscovery,
ImmutableArray<string> runtimeDiscoveredHandlerInterfaceLogNames)
string? reflectionFallbackHandlerTypeMetadataName)
{
ImplementationTypeDisplayName = implementationTypeDisplayName;
ImplementationLogName = implementationLogName;
@ -1239,8 +1199,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
ReflectedImplementationRegistrations = reflectedImplementationRegistrations;
PreciseReflectedRegistrations = preciseReflectedRegistrations;
ReflectionTypeMetadataName = reflectionTypeMetadataName;
RequiresRuntimeInterfaceDiscovery = requiresRuntimeInterfaceDiscovery;
RuntimeDiscoveredHandlerInterfaceLogNames = runtimeDiscoveredHandlerInterfaceLogNames;
ReflectionFallbackHandlerTypeMetadataName = reflectionFallbackHandlerTypeMetadataName;
}
public string ImplementationTypeDisplayName { get; }
@ -1255,9 +1214,7 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
public string? ReflectionTypeMetadataName { get; }
public bool RequiresRuntimeInterfaceDiscovery { get; }
public ImmutableArray<string> RuntimeDiscoveredHandlerInterfaceLogNames { get; }
public string? ReflectionFallbackHandlerTypeMetadataName { get; }
public bool Equals(HandlerCandidateAnalysis other)
{
@ -1266,12 +1223,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 +1253,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 +1271,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 +1290,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);
}

View File

@ -50,12 +50,7 @@ public class PriorityGeneratorSnapshotTests
await GeneratorSnapshotTest<PriorityGenerator>.RunAsync(
source,
Path.Combine(
TestContext.CurrentContext.TestDirectory,
"bases",
"snapshots",
"PriorityGenerator",
"BasicPriority"));
GetSnapshotFolder("BasicPriority"));
}
/// <summary>
@ -98,12 +93,7 @@ public class PriorityGeneratorSnapshotTests
await GeneratorSnapshotTest<PriorityGenerator>.RunAsync(
source,
Path.Combine(
TestContext.CurrentContext.TestDirectory,
"bases",
"snapshots",
"PriorityGenerator",
"NegativePriority"));
GetSnapshotFolder("NegativePriority"));
}
/// <summary>
@ -156,12 +146,7 @@ public class PriorityGeneratorSnapshotTests
await GeneratorSnapshotTest<PriorityGenerator>.RunAsync(
source,
Path.Combine(
TestContext.CurrentContext.TestDirectory,
"bases",
"snapshots",
"PriorityGenerator",
"PriorityGroup"));
GetSnapshotFolder("PriorityGroup"));
}
/// <summary>
@ -204,11 +189,25 @@ public class PriorityGeneratorSnapshotTests
await GeneratorSnapshotTest<PriorityGenerator>.RunAsync(
source,
GetSnapshotFolder("GenericClass"));
}
/// <summary>
/// 将运行时测试目录映射回仓库内已提交的 Priority 生成器快照目录。
/// </summary>
/// <param name="scenarioName">快照场景名称。</param>
/// <returns>场景对应的绝对快照目录。</returns>
private static string GetSnapshotFolder(string scenarioName)
{
return Path.GetFullPath(
Path.Combine(
TestContext.CurrentContext.TestDirectory,
"bases",
"..",
"..",
"..",
"Bases",
"snapshots",
"PriorityGenerator",
"GenericClass"));
scenarioName));
}
}

View File

@ -0,0 +1,15 @@
// <auto-generated/>
#nullable enable
namespace TestApp;
/// <summary>
/// 为当前分部类型补充自动生成的优先级契约实现。
/// </summary>
partial class MySystem : global::GFramework.Core.Abstractions.Bases.IPrioritized
{
/// <summary>
/// 获取优先级值: 10
/// </summary>
public int Priority => 10;
}

View File

@ -0,0 +1,15 @@
// <auto-generated/>
#nullable enable
namespace TestApp;
/// <summary>
/// 为当前分部类型补充自动生成的优先级契约实现。
/// </summary>
partial class GenericSystem<T> : global::GFramework.Core.Abstractions.Bases.IPrioritized
{
/// <summary>
/// 获取优先级值: 20
/// </summary>
public int Priority => 20;
}

View File

@ -0,0 +1,15 @@
// <auto-generated/>
#nullable enable
namespace TestApp;
/// <summary>
/// 为当前分部类型补充自动生成的优先级契约实现。
/// </summary>
partial class CriticalSystem : global::GFramework.Core.Abstractions.Bases.IPrioritized
{
/// <summary>
/// 获取优先级值: -100
/// </summary>
public int Priority => -100;
}

View File

@ -0,0 +1,15 @@
// <auto-generated/>
#nullable enable
namespace TestApp;
/// <summary>
/// 为当前分部类型补充自动生成的优先级契约实现。
/// </summary>
partial class HighPrioritySystem : global::GFramework.Core.Abstractions.Bases.IPrioritized
{
/// <summary>
/// 获取优先级值: -50
/// </summary>
public int Priority => -50;
}

View File

@ -10,36 +10,72 @@ public static class GeneratorSnapshotTest<TGenerator>
where TGenerator : new()
{
/// <summary>
/// 运行代码生成器的快照测试
/// 运行指定源生成器的端到端快照测试
/// </summary>
/// <param name="source">输入的源代码字符串</param>
/// <param name="snapshotFolder">快照文件存储的文件夹路径</param>
/// <param name="source">输入的源代码字符串</param>
/// <param name="snapshotFolder">用于存放已提交快照文件的根目录。</param>
/// <param name="snapshotFileNameSelector">将生成文件名映射为快照文件名的规则;为空时使用原始生成文件名。</param>
/// <returns>异步任务</returns>
/// <returns>当所有生成输出都通过快照校验后完成的异步任务。</returns>
/// <remarks>
/// 该辅助器会手动构建 Roslyn 编译并执行生成器,然后依次验证生成器自身诊断、更新后编译诊断、生成输出数量和快照内容。
/// 若生成器报告错误、生成后的编译出现错误、生成器没有任何输出,或首次运行缺少快照文件,测试都会失败。
/// 首次缺少快照时,本方法会先将当前输出写入 <paramref name="snapshotFolder" />,再通过断言中断测试,提示调用方提交快照资产。
/// <paramref name="snapshotFileNameSelector" /> 的返回值还必须保持在 <paramref name="snapshotFolder" /> 根目录之内,否则会抛出异常。
/// </remarks>
/// <exception cref="InvalidOperationException">当快照文件名映射结果为空、为绝对路径,或逃逸出快照根目录时抛出。</exception>
public static async Task RunAsync(
string source,
string snapshotFolder,
Func<string, string>? snapshotFileNameSelector = null)
{
var test = new CSharpSourceGeneratorTest<TGenerator, DefaultVerifier>
{
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 var generatorDiagnostics);
await test.RunAsync();
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 generated = test.TestState.GeneratedSources;
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 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,
$"生成器 '{typeof(TGenerator).FullName}' 未产生任何输出。");
foreach (var (filename, content) in generated)
{
// 不同测试套件可能需要将生成文件映射到非 .cs 快照,以避免测试资产被当作可编译源码参与构建。
var snapshotFileName = snapshotFileNameSelector?.Invoke(filename) ?? filename;
var path = Path.Combine(
var path = ResolveSnapshotPath(
snapshotFolder,
snapshotFileName);
@ -50,7 +86,7 @@ public static class GeneratorSnapshotTest<TGenerator>
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);
@ -58,7 +94,7 @@ public static class GeneratorSnapshotTest<TGenerator>
Assert.That(
Normalize(expected),
Is.EqualTo(Normalize(content.ToString())),
$"Snapshot mismatch: {snapshotFileName}");
$"快照不匹配:{snapshotFileName}");
}
}
@ -71,4 +107,52 @@ public static class GeneratorSnapshotTest<TGenerator>
{
return text.Replace("\r\n", "\n").Trim();
}
/// <summary>
/// 创建可由 Roslyn 驱动直接执行的源生成器实例,并统一兼容经典与增量生成器。
/// </summary>
/// <returns>适配后的源生成器实例。</returns>
/// <exception cref="InvalidOperationException">当测试类型既不是源生成器也不是增量生成器时抛出。</exception>
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)}.")
};
}
/// <summary>
/// 解析并验证快照路径,确保文件名映射不会逃逸出当前快照根目录。
/// </summary>
/// <param name="snapshotFolder">快照根目录。</param>
/// <param name="snapshotFileName">映射后的快照文件名。</param>
/// <returns>可安全访问的快照绝对路径。</returns>
/// <exception cref="InvalidOperationException">
/// 当映射结果为空白、为绝对路径,或通过相对路径越界到快照目录之外时抛出。
/// </exception>
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;
}
}

View File

@ -0,0 +1,91 @@
using System.IO;
using GFramework.Core.SourceGenerators.Enums;
namespace GFramework.SourceGenerators.Tests.Core;
/// <summary>
/// 验证快照测试辅助器对快照文件路径映射的安全约束。
/// </summary>
[TestFixture]
public class GeneratorSnapshotTestSecurityTests
{
private const string EnumAttributeNamespace = "GFramework.Core.SourceGenerators.Abstractions.Enums";
/// <summary>
/// 验证快照文件名映射返回绝对路径时,会在访问文件系统前被拒绝。
/// </summary>
[Test]
public void RunAsync_SnapshotFileNameSelectorReturnsAbsolutePath_ThrowsInvalidOperationException()
{
var snapshotRoot = CreateSnapshotRoot();
var source = BuildSource();
Assert.ThrowsAsync<InvalidOperationException>(async () =>
await GeneratorSnapshotTest<EnumExtensionsGenerator>.RunAsync(
source,
snapshotRoot,
_ => Path.Combine(snapshotRoot, "Status.EnumExtensions.g.cs")));
}
/// <summary>
/// 验证快照文件名映射尝试通过父级目录片段逃逸根目录时,会在访问文件系统前被拒绝。
/// </summary>
[Test]
public void RunAsync_SnapshotFileNameSelectorEscapesSnapshotRoot_ThrowsInvalidOperationException()
{
var snapshotRoot = CreateSnapshotRoot();
var source = BuildSource();
Assert.ThrowsAsync<InvalidOperationException>(async () =>
await GeneratorSnapshotTest<EnumExtensionsGenerator>.RunAsync(
source,
snapshotRoot,
_ => Path.Combine("..", "escaped", "Status.EnumExtensions.g.cs")));
}
/// <summary>
/// 为安全测试创建隔离的快照根目录路径,避免不同用例共享状态。
/// </summary>
/// <returns>当前用例专属的快照根目录绝对路径。</returns>
private static string CreateSnapshotRoot()
{
return Path.Combine(
TestContext.CurrentContext.WorkDirectory,
"temp-snapshots",
TestContext.CurrentContext.Test.ID,
Guid.NewGuid().ToString("N"));
}
/// <summary>
/// 构造可稳定触发枚举扩展生成器输出的最小测试源码。
/// </summary>
/// <returns>包含测试属性与目标枚举的完整源码。</returns>
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
}
}
""";
}
}

View File

@ -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));
@ -1162,6 +1168,224 @@ public class CqrsHandlerRegistryGeneratorTests
("CqrsHandlerRegistry.g.cs", HiddenNestedHandlerSelfRegistrationExpected));
}
/// <summary>
/// 验证当某轮生成仍然需要程序集级 reflection fallback 元数据,且 runtime 合同缺少承载该元数据的特性时,
/// 生成器会给出明确诊断并停止输出注册器。
/// </summary>
[Test]
public void
Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute()
{
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) { }
}
}
namespace TestApp
{
using GFramework.Cqrs.Abstractions.Cqrs;
public sealed class Container
{
private unsafe struct HiddenResponse
{
}
private unsafe sealed record HiddenRequest() : IRequest<HiddenResponse*>;
public unsafe sealed class HiddenHandler : IRequestHandler<HiddenRequest, HiddenResponse*>
{
}
}
}
""";
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();
var missingContractDiagnostic =
generatorErrors.SingleOrDefault(static diagnostic => diagnostic.Id == "GF_Cqrs_001");
Assert.Multiple(() =>
{
Assert.That(generatedCompilationErrors, Is.Empty);
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"));
});
}
/// <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 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"));
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>
@ -1188,22 +1412,19 @@ public class CqrsHandlerRegistryGeneratorTests
string source,
params MetadataReference[] additionalReferences)
{
var syntaxTree = CSharpSyntaxTree.ParseText(source);
var compilation = CSharpCompilation.Create(
"TestProject",
[syntaxTree],
MetadataReferenceTestBuilder.GetRuntimeMetadataReferences().AddRange(additionalReferences),
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
GeneratorDriver driver = CSharpGeneratorDriver.Create(
generators: [new CqrsHandlerRegistryGenerator().AsSourceGenerator()],
parseOptions: (CSharpParseOptions)syntaxTree.Options);
driver = driver.RunGeneratorsAndUpdateCompilation(
compilation,
out var updatedCompilation,
out _);
var compilationErrors = updatedCompilation.GetDiagnostics()
var execution = ExecuteGenerator(
source,
allowUnsafe: false,
additionalReferences: 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(
@ -1211,11 +1432,76 @@ public class CqrsHandlerRegistryGeneratorTests
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;
}
/// <summary>
/// 运行 CQRS handler registry generator并返回生成输出及相关诊断。
/// </summary>
/// <param name="source">输入源码。</param>
/// <param name="allowUnsafe">
/// 是否允许测试编译包含 <c>unsafe</c> 代码。
/// 某些回归用例会故意构造带指针类型的非法 handler 合同,以覆盖 fallback 防御分支,此时需要启用该选项避免把缺少
/// <c>unsafe</c> 编译上下文的错误与目标生成器行为混淆。
/// </param>
/// <param name="additionalReferences">附加元数据引用,用于构造跨程序集场景。</param>
/// <returns>包含生成源、生成器诊断和更新后编译诊断的执行结果。</returns>
private static GeneratorExecutionResult ExecuteGenerator(
string source,
bool allowUnsafe = false,
params MetadataReference[] additionalReferences)
{
var syntaxTree = CSharpSyntaxTree.ParseText(source);
var compilation = CSharpCompilation.Create(
"TestProject",
[syntaxTree],
MetadataReferenceTestBuilder.GetRuntimeMetadataReferences().AddRange(additionalReferences),
new CSharpCompilationOptions(
OutputKind.DynamicallyLinkedLibrary,
allowUnsafe: allowUnsafe));
GeneratorDriver driver = CSharpGeneratorDriver.Create(
generators: [new CqrsHandlerRegistryGenerator().AsSourceGenerator()],
parseOptions: (CSharpParseOptions)syntaxTree.Options);
driver = driver.RunGeneratorsAndUpdateCompilation(
compilation,
out var updatedCompilation,
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 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(),
compilationDiagnostics,
generatedCompilationDiagnostics);
}
/// <summary>
/// 封装 CQRS handler registry generator 的单次执行结果。
/// </summary>
/// <param name="GeneratedSources">本轮生成产生的源文件集合。</param>
/// <param name="GeneratorDiagnostics">生成器自身报告的诊断集合。</param>
/// <param name="CompilationDiagnostics">将生成结果并回编译后的完整编译诊断集合。</param>
/// <param name="GeneratedCompilationDiagnostics">仅来自生成源文件的编译诊断集合。</param>
private sealed record GeneratorExecutionResult(
(string filename, string content)[] GeneratedSources,
Diagnostic[] GeneratorDiagnostics,
Diagnostic[] CompilationDiagnostics,
Diagnostic[] GeneratedCompilationDiagnostics);
}

View File

@ -15,6 +15,7 @@ public class EnumExtensionsGeneratorSnapshotTests
/// <summary>
/// 验证默认配置会为普通枚举生成逐项判断方法与集合判断方法。
/// </summary>
/// <returns>异步任务。</returns>
[Test]
public async Task Snapshot_BasicEnum_IsMethods()
{
@ -34,9 +35,31 @@ public class EnumExtensionsGeneratorSnapshotTests
GetSnapshotFileName);
}
/// <summary>
/// 验证未提供快照文件名映射时,会直接按生成文件名进行快照比对。
/// </summary>
/// <returns>异步任务。</returns>
[Test]
public async Task Snapshot_BasicEnum_IsMethods_DefaultSnapshotFileNameSelector()
{
var source = BuildSource(
"""
public enum Status
{
Active,
Inactive
}
""");
await GeneratorSnapshotTest<EnumExtensionsGenerator>.RunAsync(
source,
GetSnapshotFolder("BasicEnum_IsMethods_DefaultSnapshotFileNameSelector"));
}
/// <summary>
/// 验证默认配置在较小枚举上仍会生成集合判断方法。
/// </summary>
/// <returns>异步任务。</returns>
[Test]
public async Task Snapshot_BasicEnum_IsInMethod()
{
@ -58,6 +81,7 @@ public class EnumExtensionsGeneratorSnapshotTests
/// <summary>
/// 验证带显式位标志值的枚举也会生成对应扩展方法。
/// </summary>
/// <returns>异步任务。</returns>
[Test]
public async Task Snapshot_EnumWithFlagValues()
{
@ -82,6 +106,7 @@ public class EnumExtensionsGeneratorSnapshotTests
/// <summary>
/// 验证关闭逐项判断开关后仅保留集合判断方法。
/// </summary>
/// <returns>异步任务。</returns>
[Test]
public async Task Snapshot_DisableIsMethods()
{
@ -104,6 +129,7 @@ public class EnumExtensionsGeneratorSnapshotTests
/// <summary>
/// 验证关闭集合判断开关后仅保留逐项判断方法。
/// </summary>
/// <returns>异步任务。</returns>
[Test]
public async Task Snapshot_DisableIsInMethod()
{
@ -126,6 +152,7 @@ public class EnumExtensionsGeneratorSnapshotTests
/// <summary>
/// 验证同时关闭两个生成开关时不会输出任何扩展方法。
/// </summary>
/// <returns>异步任务。</returns>
[Test]
public async Task Snapshot_DisableAllGeneratedMethods()
{

View File

@ -2,15 +2,31 @@
using System;
namespace TestApp
{
/// <summary>
/// 为 <see cref="TestApp.Status" /> 提供自动生成的扩展方法。
/// </summary>
public static partial class StatusExtensions
{
/// <summary>是否为 Active</summary>
/// <summary>
/// 判断给定值是否为 <see cref="TestApp.Status.Active" />。
/// </summary>
/// <param name="value">要检查的枚举值。</param>
/// <returns>当 <paramref name="value" /> 等于 <see cref="TestApp.Status.Active" /> 时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
public static bool IsActive(this TestApp.Status value) => value == TestApp.Status.Active;
/// <summary>是否为 Inactive</summary>
/// <summary>
/// 判断给定值是否为 <see cref="TestApp.Status.Inactive" />。
/// </summary>
/// <param name="value">要检查的枚举值。</param>
/// <returns>当 <paramref name="value" /> 等于 <see cref="TestApp.Status.Inactive" /> 时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
public static bool IsInactive(this TestApp.Status value) => value == TestApp.Status.Inactive;
/// <summary>判断是否属于指定集合</summary>
/// <summary>
/// 判断给定值是否属于指定候选集合。
/// </summary>
/// <param name="value">要检查的枚举值。</param>
/// <param name="values">用于匹配的候选枚举值集合;当为 <see langword="null" /> 时返回 <see langword="false" />。</param>
/// <returns>当 <paramref name="value" /> 命中任一候选值时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values)
{
if (values == null) return false;

View File

@ -2,18 +2,38 @@
using System;
namespace TestApp
{
/// <summary>
/// 为 <see cref="TestApp.Status" /> 提供自动生成的扩展方法。
/// </summary>
public static partial class StatusExtensions
{
/// <summary>是否为 Active</summary>
/// <summary>
/// 判断给定值是否为 <see cref="TestApp.Status.Active" />。
/// </summary>
/// <param name="value">要检查的枚举值。</param>
/// <returns>当 <paramref name="value" /> 等于 <see cref="TestApp.Status.Active" /> 时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
public static bool IsActive(this TestApp.Status value) => value == TestApp.Status.Active;
/// <summary>是否为 Inactive</summary>
/// <summary>
/// 判断给定值是否为 <see cref="TestApp.Status.Inactive" />。
/// </summary>
/// <param name="value">要检查的枚举值。</param>
/// <returns>当 <paramref name="value" /> 等于 <see cref="TestApp.Status.Inactive" /> 时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
public static bool IsInactive(this TestApp.Status value) => value == TestApp.Status.Inactive;
/// <summary>是否为 Pending</summary>
/// <summary>
/// 判断给定值是否为 <see cref="TestApp.Status.Pending" />。
/// </summary>
/// <param name="value">要检查的枚举值。</param>
/// <returns>当 <paramref name="value" /> 等于 <see cref="TestApp.Status.Pending" /> 时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
public static bool IsPending(this TestApp.Status value) => value == TestApp.Status.Pending;
/// <summary>判断是否属于指定集合</summary>
/// <summary>
/// 判断给定值是否属于指定候选集合。
/// </summary>
/// <param name="value">要检查的枚举值。</param>
/// <param name="values">用于匹配的候选枚举值集合;当为 <see langword="null" /> 时返回 <see langword="false" />。</param>
/// <returns>当 <paramref name="value" /> 命中任一候选值时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values)
{
if (values == null) return false;

View File

@ -0,0 +1,37 @@
// <auto-generated />
using System;
namespace TestApp
{
/// <summary>
/// 为 <see cref="TestApp.Status" /> 提供自动生成的扩展方法。
/// </summary>
public static partial class StatusExtensions
{
/// <summary>
/// 判断给定值是否为 <see cref="TestApp.Status.Active" />。
/// </summary>
/// <param name="value">要检查的枚举值。</param>
/// <returns>当 <paramref name="value" /> 等于 <see cref="TestApp.Status.Active" /> 时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
public static bool IsActive(this TestApp.Status value) => value == TestApp.Status.Active;
/// <summary>
/// 判断给定值是否为 <see cref="TestApp.Status.Inactive" />。
/// </summary>
/// <param name="value">要检查的枚举值。</param>
/// <returns>当 <paramref name="value" /> 等于 <see cref="TestApp.Status.Inactive" /> 时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
public static bool IsInactive(this TestApp.Status value) => value == TestApp.Status.Inactive;
/// <summary>
/// 判断给定值是否属于指定候选集合。
/// </summary>
/// <param name="value">要检查的枚举值。</param>
/// <param name="values">用于匹配的候选枚举值集合;当为 <see langword="null" /> 时返回 <see langword="false" />。</param>
/// <returns>当 <paramref name="value" /> 命中任一候选值时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
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;
}
}
}

View File

@ -2,6 +2,9 @@
using System;
namespace TestApp
{
/// <summary>
/// 为 <see cref="TestApp.Status" /> 提供自动生成的扩展方法。
/// </summary>
public static partial class StatusExtensions
{
}

View File

@ -2,12 +2,23 @@
using System;
namespace TestApp
{
/// <summary>
/// 为 <see cref="TestApp.Status" /> 提供自动生成的扩展方法。
/// </summary>
public static partial class StatusExtensions
{
/// <summary>是否为 Active</summary>
/// <summary>
/// 判断给定值是否为 <see cref="TestApp.Status.Active" />。
/// </summary>
/// <param name="value">要检查的枚举值。</param>
/// <returns>当 <paramref name="value" /> 等于 <see cref="TestApp.Status.Active" /> 时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
public static bool IsActive(this TestApp.Status value) => value == TestApp.Status.Active;
/// <summary>是否为 Inactive</summary>
/// <summary>
/// 判断给定值是否为 <see cref="TestApp.Status.Inactive" />。
/// </summary>
/// <param name="value">要检查的枚举值。</param>
/// <returns>当 <paramref name="value" /> 等于 <see cref="TestApp.Status.Inactive" /> 时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
public static bool IsInactive(this TestApp.Status value) => value == TestApp.Status.Inactive;
}
}

View File

@ -2,9 +2,17 @@
using System;
namespace TestApp
{
/// <summary>
/// 为 <see cref="TestApp.Status" /> 提供自动生成的扩展方法。
/// </summary>
public static partial class StatusExtensions
{
/// <summary>判断是否属于指定集合</summary>
/// <summary>
/// 判断给定值是否属于指定候选集合。
/// </summary>
/// <param name="value">要检查的枚举值。</param>
/// <param name="values">用于匹配的候选枚举值集合;当为 <see langword="null" /> 时返回 <see langword="false" />。</param>
/// <returns>当 <paramref name="value" /> 命中任一候选值时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values)
{
if (values == null) return false;

View File

@ -2,21 +2,45 @@
using System;
namespace TestApp
{
/// <summary>
/// 为 <see cref="TestApp.Permissions" /> 提供自动生成的扩展方法。
/// </summary>
public static partial class PermissionsExtensions
{
/// <summary>是否为 None</summary>
/// <summary>
/// 判断给定值是否为 <see cref="TestApp.Permissions.None" />。
/// </summary>
/// <param name="value">要检查的枚举值。</param>
/// <returns>当 <paramref name="value" /> 等于 <see cref="TestApp.Permissions.None" /> 时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
public static bool IsNone(this TestApp.Permissions value) => value == TestApp.Permissions.None;
/// <summary>是否为 Read</summary>
/// <summary>
/// 判断给定值是否为 <see cref="TestApp.Permissions.Read" />。
/// </summary>
/// <param name="value">要检查的枚举值。</param>
/// <returns>当 <paramref name="value" /> 等于 <see cref="TestApp.Permissions.Read" /> 时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
public static bool IsRead(this TestApp.Permissions value) => value == TestApp.Permissions.Read;
/// <summary>是否为 Write</summary>
/// <summary>
/// 判断给定值是否为 <see cref="TestApp.Permissions.Write" />。
/// </summary>
/// <param name="value">要检查的枚举值。</param>
/// <returns>当 <paramref name="value" /> 等于 <see cref="TestApp.Permissions.Write" /> 时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
public static bool IsWrite(this TestApp.Permissions value) => value == TestApp.Permissions.Write;
/// <summary>是否为 Execute</summary>
/// <summary>
/// 判断给定值是否为 <see cref="TestApp.Permissions.Execute" />。
/// </summary>
/// <param name="value">要检查的枚举值。</param>
/// <returns>当 <paramref name="value" /> 等于 <see cref="TestApp.Permissions.Execute" /> 时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
public static bool IsExecute(this TestApp.Permissions value) => value == TestApp.Permissions.Execute;
/// <summary>判断是否属于指定集合</summary>
/// <summary>
/// 判断给定值是否属于指定候选集合。
/// </summary>
/// <param name="value">要检查的枚举值。</param>
/// <param name="values">用于匹配的候选枚举值集合;当为 <see langword="null" /> 时返回 <see langword="false" />。</param>
/// <returns>当 <paramref name="value" /> 命中任一候选值时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
public static bool IsIn(this TestApp.Permissions value, params TestApp.Permissions[] values)
{
if (values == null) return false;

View File

@ -96,12 +96,7 @@ public class LoggerGeneratorSnapshotTests
await GeneratorSnapshotTest<LoggerGenerator>.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<LoggerGenerator>.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<LoggerGenerator>.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<LoggerGenerator>.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<LoggerGenerator>.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<LoggerGenerator>.RunAsync(
source,
GetSnapshotFolder("GenericClass"));
}
/// <summary>
/// 将运行时测试目录映射回仓库内已提交的日志生成器快照目录。
/// </summary>
/// <param name="scenarioName">快照场景名称。</param>
/// <returns>场景对应的绝对快照目录。</returns>
private static string GetSnapshotFolder(string scenarioName)
{
return Path.GetFullPath(
Path.Combine(
TestContext.CurrentContext.TestDirectory,
"logging",
"..",
"..",
"..",
"Logging",
"snapshots",
"LoggerGenerator",
"GenericClass"));
scenarioName));
}
}

View File

@ -0,0 +1,16 @@
// <auto-generated />
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Logging;
namespace TestApp;
/// <summary>
/// 为当前分部类型提供自动生成的日志字段。
/// </summary>
partial class MyService
{
/// <summary>
/// 自动生成的日志字段。
/// </summary>
private static readonly ILogger MyLogger = LoggerFactoryResolver.Provider.CreateLogger("MyService");
}

View File

@ -0,0 +1,16 @@
// <auto-generated />
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Logging;
namespace TestApp;
/// <summary>
/// 为当前分部类型提供自动生成的日志字段。
/// </summary>
partial class MyService
{
/// <summary>
/// 自动生成的日志字段。
/// </summary>
private static readonly ILogger _log = LoggerFactoryResolver.Provider.CreateLogger("MyService");
}

View File

@ -0,0 +1,16 @@
// <auto-generated />
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Logging;
namespace TestApp;
/// <summary>
/// 为当前分部类型提供自动生成的日志字段。
/// </summary>
partial class MyService
{
/// <summary>
/// 自动生成的日志字段。
/// </summary>
private static readonly ILogger _log = LoggerFactoryResolver.Provider.CreateLogger("MyService");
}

View File

@ -0,0 +1,16 @@
// <auto-generated />
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Logging;
namespace TestApp;
/// <summary>
/// 为当前分部类型提供自动生成的日志字段。
/// </summary>
partial class MyService<T>
{
/// <summary>
/// 自动生成的日志字段。
/// </summary>
private static readonly ILogger _log = LoggerFactoryResolver.Provider.CreateLogger("MyService");
}

View File

@ -0,0 +1,16 @@
// <auto-generated />
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Logging;
namespace TestApp;
/// <summary>
/// 为当前分部类型提供自动生成的日志字段。
/// </summary>
partial class MyService
{
/// <summary>
/// 自动生成的日志字段。
/// </summary>
private readonly ILogger _log = LoggerFactoryResolver.Provider.CreateLogger("MyService");
}

View File

@ -0,0 +1,16 @@
// <auto-generated />
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Logging;
namespace TestApp;
/// <summary>
/// 为当前分部类型提供自动生成的日志字段。
/// </summary>
partial class MyService
{
/// <summary>
/// 自动生成的日志字段。
/// </summary>
public static readonly ILogger _log = LoggerFactoryResolver.Provider.CreateLogger("MyService");
}

View File

@ -86,9 +86,22 @@ public class ContextAwareGeneratorSnapshotTests
// 执行生成器快照测试,将生成的代码与预期快照进行比较
await GeneratorSnapshotTest<ContextAwareGenerator>.RunAsync(
source,
GetSnapshotFolder());
}
/// <summary>
/// 将运行时测试目录映射回仓库内已提交的上下文感知生成器快照目录。
/// </summary>
/// <returns>快照目录的绝对路径。</returns>
private static string GetSnapshotFolder()
{
return Path.GetFullPath(
Path.Combine(
TestContext.CurrentContext.TestDirectory,
"rule",
"..",
"..",
"..",
"Rule",
"snapshots",
"ContextAwareGenerator"));
}

View File

@ -0,0 +1,101 @@
// <auto-generated/>
#nullable enable
namespace TestApp;
/// <summary>
/// 为当前规则类型补充自动生成的架构上下文访问实现。
/// </summary>
/// <remarks>
/// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例,
/// 已缓存的实例上下文需要通过 <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>
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();
/// <summary>
/// 获取当前实例绑定的架构上下文。
/// </summary>
/// <remarks>
/// 该属性会先返回通过 <c>IContextAware.SetContext(...)</c> 显式注入的实例上下文;若尚未设置,则在同一个同步域内惰性初始化共享提供者。
/// 当静态提供者尚未配置时,生成代码会回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
/// 一旦某个实例成功缓存上下文,后续 <see cref="SetContextProvider(GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider)" />
/// 或 <see cref="ResetContextProvider" /> 不会自动清除此缓存;如需覆盖,请显式调用 <c>IContextAware.SetContext(...)</c>。
/// 当前实现还假设 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider.GetContext" /> 可在持有 <c>_contextSync</c> 时安全执行;
/// 自定义 provider 不应在该调用链内重新进入当前类型的 provider 配置 API且应避免引入与外部全局锁相互等待的锁顺序。
/// </remarks>
protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context
{
get
{
var context = _context;
if (context is not null)
{
return context;
}
// 在同一个同步域内协调懒加载与 provider 切换,避免读取到被并发重置的空提供者。
// provider 的 GetContext() 会在持有 _contextSync 时执行;自定义 provider 必须避免在该调用链内回调 SetContextProvider/ResetContextProvider 或形成反向锁顺序。
lock (_contextSync)
{
_contextProvider ??= new global::GFramework.Core.Architectures.GameContextProvider();
_context ??= _contextProvider.GetContext();
return _context;
}
}
}
/// <summary>
/// 配置当前生成类型共享的上下文提供者。
/// </summary>
/// <param name="provider">后续懒加载上下文时要使用的提供者实例。</param>
/// <remarks>
/// 该方法使用与 <see cref="Context" /> 相同的同步锁,避免提供者切换与惰性初始化交错。
/// 已经缓存上下文的实例不会因为提供者切换而自动失效;该变更仅影响尚未初始化上下文的新实例或未缓存实例。
/// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。
/// </remarks>
public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)
{
lock (_contextSync)
{
_contextProvider = provider;
}
}
/// <summary>
/// 重置共享上下文提供者,使后续懒加载回退到默认提供者。
/// </summary>
/// <remarks>
/// 该方法主要用于测试清理或跨用例恢复默认行为。
/// 它不会清除已经缓存到实例字段中的上下文;只有后续尚未初始化上下文的实例会重新回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
/// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。
/// </remarks>
public static void ResetContextProvider()
{
lock (_contextSync)
{
_contextProvider = null;
}
}
void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
{
// 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。
lock (_contextSync)
{
_context = context;
}
}
global::GFramework.Core.Abstractions.Architectures.IArchitectureContext global::GFramework.Core.Abstractions.Rule.IContextAware.GetContext()
{
return Context;
}
}

View File

@ -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
;;