mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
feat(source-generators): 添加CQRS处理器注册器和枚举扩展生成器
- 实现了CqrsHandlerRegistryGenerator用于自动生成CQRS处理器注册器 - 添加了EnumExtensionsGenerator用于自动生成枚举相关的扩展方法 - 创建了ContextAwareGenerator为标记类自动生成IContextAware接口实现 - 支持运行时类型引用的安全编码和反射回退机制 - 实现了精确的运行时类型引用描述和泛型类型处理 - 添加了完整的诊断报告和错误处理机制
This commit is contained in:
parent
b19877f970
commit
9ec83fa56a
@ -205,7 +205,8 @@ public sealed class EnumExtensionsGenerator : AttributeEnumGeneratorBase
|
|||||||
builder.AppendLine(" /// 判断给定值是否属于指定候选集合。");
|
builder.AppendLine(" /// 判断给定值是否属于指定候选集合。");
|
||||||
builder.AppendLine(" /// </summary>");
|
builder.AppendLine(" /// </summary>");
|
||||||
builder.AppendLine(" /// <param name=\"value\">要检查的枚举值。</param>");
|
builder.AppendLine(" /// <param name=\"value\">要检查的枚举值。</param>");
|
||||||
builder.AppendLine(" /// <param name=\"values\">用于匹配的候选枚举值集合。</param>");
|
builder.AppendLine(
|
||||||
|
" /// <param name=\"values\">用于匹配的候选枚举值集合;当为 <see langword=\"null\" /> 时返回 <see langword=\"false\" />。</param>");
|
||||||
builder.AppendLine(
|
builder.AppendLine(
|
||||||
" /// <returns>当 <paramref name=\"value\" /> 命中任一候选值时返回 <see langword=\"true\" />;否则返回 <see langword=\"false\" />。</returns>");
|
" /// <returns>当 <paramref name=\"value\" /> 命中任一候选值时返回 <see langword=\"true\" />;否则返回 <see langword=\"false\" />。</returns>");
|
||||||
builder.AppendLine(
|
builder.AppendLine(
|
||||||
|
|||||||
@ -99,6 +99,14 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
|||||||
sb.AppendLine("/// <summary>");
|
sb.AppendLine("/// <summary>");
|
||||||
sb.AppendLine("/// 为当前规则类型补充自动生成的架构上下文访问实现。");
|
sb.AppendLine("/// 为当前规则类型补充自动生成的架构上下文访问实现。");
|
||||||
sb.AppendLine("/// </summary>");
|
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("/// </remarks>");
|
||||||
sb.AppendLine($"partial class {symbol.Name} : {interfaceName}");
|
sb.AppendLine($"partial class {symbol.Name} : {interfaceName}");
|
||||||
sb.AppendLine("{");
|
sb.AppendLine("{");
|
||||||
|
|
||||||
@ -134,8 +142,18 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
|||||||
sb.AppendLine(" private static readonly object _contextSync = new();");
|
sb.AppendLine(" private static readonly object _contextSync = new();");
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
sb.AppendLine(" /// <summary>");
|
sb.AppendLine(" /// <summary>");
|
||||||
sb.AppendLine(" /// 自动获取的架构上下文(懒加载,默认使用 GameContextProvider)");
|
sb.AppendLine(" /// 获取当前实例绑定的架构上下文。");
|
||||||
sb.AppendLine(" /// </summary>");
|
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(" /// </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(" {");
|
||||||
sb.AppendLine(" get");
|
sb.AppendLine(" get");
|
||||||
@ -158,9 +176,15 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
|||||||
sb.AppendLine(" }");
|
sb.AppendLine(" }");
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
sb.AppendLine(" /// <summary>");
|
sb.AppendLine(" /// <summary>");
|
||||||
sb.AppendLine(" /// 配置上下文提供者(用于测试或多架构场景)");
|
sb.AppendLine(" /// 配置当前生成类型共享的上下文提供者。");
|
||||||
sb.AppendLine(" /// </summary>");
|
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(
|
sb.AppendLine(
|
||||||
" public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)");
|
" public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)");
|
||||||
sb.AppendLine(" {");
|
sb.AppendLine(" {");
|
||||||
@ -171,8 +195,14 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
|||||||
sb.AppendLine(" }");
|
sb.AppendLine(" }");
|
||||||
sb.AppendLine();
|
sb.AppendLine();
|
||||||
sb.AppendLine(" /// <summary>");
|
sb.AppendLine(" /// <summary>");
|
||||||
sb.AppendLine(" /// 重置上下文提供者为默认值(用于测试清理)");
|
sb.AppendLine(" /// 重置共享上下文提供者,使后续懒加载回退到默认提供者。");
|
||||||
sb.AppendLine(" /// </summary>");
|
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(" public static void ResetContextProvider()");
|
||||||
sb.AppendLine(" {");
|
sb.AppendLine(" {");
|
||||||
sb.AppendLine(" lock (_contextSync)");
|
sb.AppendLine(" lock (_contextSync)");
|
||||||
@ -248,7 +278,11 @@ public sealed class ContextAwareGenerator : MetadataAttributeClassGeneratorBase
|
|||||||
switch (method.Name)
|
switch (method.Name)
|
||||||
{
|
{
|
||||||
case "SetContext":
|
case "SetContext":
|
||||||
sb.AppendLine(" _context = context;");
|
sb.AppendLine(" // 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。");
|
||||||
|
sb.AppendLine(" lock (_contextSync)");
|
||||||
|
sb.AppendLine(" {");
|
||||||
|
sb.AppendLine(" _context = context;");
|
||||||
|
sb.AppendLine(" }");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "GetContext":
|
case "GetContext":
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
; Shipped analyzer releases
|
||||||
|
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
; Unshipped analyzer release
|
||||||
|
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
|
||||||
|
|
||||||
|
### New Rules
|
||||||
|
|
||||||
|
Rule ID | Category | Severity | Notes
|
||||||
|
-------------|----------------------------------|----------|------------------------------
|
||||||
|
GF_Cqrs_001 | GFramework.Cqrs.SourceGenerators | Error | CqrsHandlerRegistryGenerator
|
||||||
@ -28,6 +28,14 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
private const string GeneratedTypeName = "__GFrameworkGeneratedCqrsHandlerRegistry";
|
private const string GeneratedTypeName = "__GFrameworkGeneratedCqrsHandlerRegistry";
|
||||||
private const string HintName = "CqrsHandlerRegistry.g.cs";
|
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 />
|
/// <inheritdoc />
|
||||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||||
{
|
{
|
||||||
@ -169,6 +177,9 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
generationEnvironment.SupportsReflectionFallbackAttribute,
|
generationEnvironment.SupportsReflectionFallbackAttribute,
|
||||||
fallbackHandlerTypeMetadataNames.Length))
|
fallbackHandlerTypeMetadataNames.Length))
|
||||||
{
|
{
|
||||||
|
ReportMissingReflectionFallbackContractDiagnostic(
|
||||||
|
context,
|
||||||
|
fallbackHandlerTypeMetadataNames);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,6 +208,25 @@ public sealed class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
|||||||
return fallbackHandlerTypeCount == 0 || supportsReflectionFallbackAttribute;
|
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(
|
private static List<ImplementationRegistrationSpec> CollectRegistrations(
|
||||||
ImmutableArray<HandlerCandidateAnalysis?> candidates)
|
ImmutableArray<HandlerCandidateAnalysis?> candidates)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -10,12 +10,19 @@ public static class GeneratorSnapshotTest<TGenerator>
|
|||||||
where TGenerator : new()
|
where TGenerator : new()
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 运行源代码生成器的快照测试
|
/// 运行指定源生成器的端到端快照测试。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="source">输入的源代码字符串</param>
|
/// <param name="source">输入的源代码字符串。</param>
|
||||||
/// <param name="snapshotFolder">快照文件存储的文件夹路径</param>
|
/// <param name="snapshotFolder">用于存放已提交快照文件的根目录。</param>
|
||||||
/// <param name="snapshotFileNameSelector">将生成文件名映射为快照文件名的规则;为空时使用原始生成文件名。</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(
|
public static async Task RunAsync(
|
||||||
string source,
|
string source,
|
||||||
string snapshotFolder,
|
string snapshotFolder,
|
||||||
@ -33,7 +40,16 @@ public static class GeneratorSnapshotTest<TGenerator>
|
|||||||
driver = driver.RunGeneratorsAndUpdateCompilation(
|
driver = driver.RunGeneratorsAndUpdateCompilation(
|
||||||
compilation,
|
compilation,
|
||||||
out var updatedCompilation,
|
out var updatedCompilation,
|
||||||
out _);
|
out var generatorDiagnostics);
|
||||||
|
|
||||||
|
var generatorErrors = generatorDiagnostics
|
||||||
|
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||||
|
.ToArray();
|
||||||
|
Assert.That(
|
||||||
|
generatorErrors,
|
||||||
|
Is.Empty,
|
||||||
|
() =>
|
||||||
|
$"执行生成器时出现错误:{Environment.NewLine}{string.Join(Environment.NewLine, generatorErrors.Select(static diagnostic => diagnostic.ToString()))}");
|
||||||
|
|
||||||
var compilationErrors = updatedCompilation.GetDiagnostics()
|
var compilationErrors = updatedCompilation.GetDiagnostics()
|
||||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||||
|
|||||||
@ -1169,28 +1169,95 @@ public class CqrsHandlerRegistryGeneratorTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 验证当某轮生成仍然需要程序集级 reflection fallback 元数据时,
|
/// 验证当某轮生成仍然需要程序集级 reflection fallback 元数据,且 runtime 合同缺少承载该元数据的特性时,
|
||||||
/// 若 runtime 合同未提供对应特性契约,生成器会放弃输出注册器以避免静默漏注册。
|
/// 生成器会给出明确诊断并停止输出注册器。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Test]
|
[Test]
|
||||||
public void
|
public void
|
||||||
Rejects_Registry_Emission_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute()
|
Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute()
|
||||||
{
|
{
|
||||||
var method = typeof(CqrsHandlerRegistryGenerator).GetMethod(
|
const string source = """
|
||||||
"CanEmitGeneratedRegistry",
|
using System;
|
||||||
BindingFlags.NonPublic | BindingFlags.Static);
|
|
||||||
|
|
||||||
Assert.That(method, Is.Not.Null);
|
namespace Microsoft.Extensions.DependencyInjection
|
||||||
|
{
|
||||||
|
public interface IServiceCollection { }
|
||||||
|
|
||||||
var canEmitWithoutFallbackRequirement = (bool?)method!.Invoke(null, [false, 0]);
|
public static class ServiceCollectionServiceExtensions
|
||||||
var canEmitWithSupportedFallbackAttribute = (bool?)method.Invoke(null, [true, 1]);
|
{
|
||||||
var canEmitWithoutSupportedFallbackAttribute = (bool?)method.Invoke(null, [false, 1]);
|
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);
|
||||||
|
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.Multiple(() =>
|
||||||
{
|
{
|
||||||
Assert.That(canEmitWithoutFallbackRequirement, Is.True);
|
Assert.That(execution.GeneratedSources, Is.Empty);
|
||||||
Assert.That(canEmitWithSupportedFallbackAttribute, Is.True);
|
Assert.That(missingContractDiagnostic, Is.Not.Null);
|
||||||
Assert.That(canEmitWithoutSupportedFallbackAttribute, Is.False);
|
Assert.That(
|
||||||
|
missingContractDiagnostic!.GetMessage(),
|
||||||
|
Does.Contain("TestApp.Container+HiddenHandler"));
|
||||||
|
Assert.That(
|
||||||
|
missingContractDiagnostic.GetMessage(),
|
||||||
|
Does.Contain("GFramework.Cqrs.CqrsReflectionFallbackAttribute"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1219,6 +1286,40 @@ public class CqrsHandlerRegistryGeneratorTests
|
|||||||
private static string RunGenerator(
|
private static string RunGenerator(
|
||||||
string source,
|
string source,
|
||||||
params MetadataReference[] additionalReferences)
|
params MetadataReference[] additionalReferences)
|
||||||
|
{
|
||||||
|
var execution = ExecuteGenerator(
|
||||||
|
source,
|
||||||
|
additionalReferences);
|
||||||
|
var generatorErrors = execution.GeneratorDiagnostics
|
||||||
|
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||||
|
.ToArray();
|
||||||
|
Assert.That(
|
||||||
|
generatorErrors,
|
||||||
|
Is.Empty,
|
||||||
|
() =>
|
||||||
|
$"执行生成器时出现错误:{Environment.NewLine}{string.Join(Environment.NewLine, generatorErrors.Select(static diagnostic => diagnostic.ToString()))}");
|
||||||
|
var compilationErrors = execution.CompilationDiagnostics
|
||||||
|
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||||
|
.ToArray();
|
||||||
|
Assert.That(
|
||||||
|
compilationErrors,
|
||||||
|
Is.Empty,
|
||||||
|
() =>
|
||||||
|
$"编译生成的代码时出现错误:{Environment.NewLine}{string.Join(Environment.NewLine, compilationErrors.Select(static diagnostic => diagnostic.ToString()))}");
|
||||||
|
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1));
|
||||||
|
|
||||||
|
return execution.GeneratedSources[0].content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 运行 CQRS handler registry generator,并返回生成输出及相关诊断。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">输入源码。</param>
|
||||||
|
/// <param name="additionalReferences">附加元数据引用,用于构造跨程序集场景。</param>
|
||||||
|
/// <returns>包含生成源、生成器诊断和更新后编译诊断的执行结果。</returns>
|
||||||
|
private static GeneratorExecutionResult ExecuteGenerator(
|
||||||
|
string source,
|
||||||
|
params MetadataReference[] additionalReferences)
|
||||||
{
|
{
|
||||||
var syntaxTree = CSharpSyntaxTree.ParseText(source);
|
var syntaxTree = CSharpSyntaxTree.ParseText(source);
|
||||||
var compilation = CSharpCompilation.Create(
|
var compilation = CSharpCompilation.Create(
|
||||||
@ -1233,21 +1334,28 @@ public class CqrsHandlerRegistryGeneratorTests
|
|||||||
driver = driver.RunGeneratorsAndUpdateCompilation(
|
driver = driver.RunGeneratorsAndUpdateCompilation(
|
||||||
compilation,
|
compilation,
|
||||||
out var updatedCompilation,
|
out var updatedCompilation,
|
||||||
out _);
|
out var generatorDiagnostics);
|
||||||
|
|
||||||
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 runResult = driver.GetRunResult();
|
||||||
Assert.That(runResult.Results, Has.Length.EqualTo(1));
|
Assert.That(runResult.Results, Has.Length.EqualTo(1));
|
||||||
Assert.That(runResult.Results[0].GeneratedSources, Has.Length.EqualTo(1));
|
var generatedSources = runResult.Results[0].GeneratedSources
|
||||||
|
.Select(static sourceResult =>
|
||||||
return runResult.Results[0].GeneratedSources[0].SourceText.ToString();
|
(filename: sourceResult.HintName, content: sourceResult.SourceText.ToString()))
|
||||||
|
.ToArray();
|
||||||
|
return new GeneratorExecutionResult(
|
||||||
|
generatedSources,
|
||||||
|
generatorDiagnostics.ToArray(),
|
||||||
|
updatedCompilation.GetDiagnostics().ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 封装 CQRS handler registry generator 的单次执行结果。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="GeneratedSources">本轮生成产生的源文件集合。</param>
|
||||||
|
/// <param name="GeneratorDiagnostics">生成器自身报告的诊断集合。</param>
|
||||||
|
/// <param name="CompilationDiagnostics">将生成结果并回编译后的编译诊断集合。</param>
|
||||||
|
private sealed record GeneratorExecutionResult(
|
||||||
|
(string filename, string content)[] GeneratedSources,
|
||||||
|
Diagnostic[] GeneratorDiagnostics,
|
||||||
|
Diagnostic[] CompilationDiagnostics);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,7 @@ namespace TestApp
|
|||||||
/// 判断给定值是否属于指定候选集合。
|
/// 判断给定值是否属于指定候选集合。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value">要检查的枚举值。</param>
|
/// <param name="value">要检查的枚举值。</param>
|
||||||
/// <param name="values">用于匹配的候选枚举值集合。</param>
|
/// <param name="values">用于匹配的候选枚举值集合;当为 <see langword="null" /> 时返回 <see langword="false" />。</param>
|
||||||
/// <returns>当 <paramref name="value" /> 命中任一候选值时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
/// <returns>当 <paramref name="value" /> 命中任一候选值时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||||
public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values)
|
public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -32,7 +32,7 @@ namespace TestApp
|
|||||||
/// 判断给定值是否属于指定候选集合。
|
/// 判断给定值是否属于指定候选集合。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value">要检查的枚举值。</param>
|
/// <param name="value">要检查的枚举值。</param>
|
||||||
/// <param name="values">用于匹配的候选枚举值集合。</param>
|
/// <param name="values">用于匹配的候选枚举值集合;当为 <see langword="null" /> 时返回 <see langword="false" />。</param>
|
||||||
/// <returns>当 <paramref name="value" /> 命中任一候选值时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
/// <returns>当 <paramref name="value" /> 命中任一候选值时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||||
public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values)
|
public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -25,7 +25,7 @@ namespace TestApp
|
|||||||
/// 判断给定值是否属于指定候选集合。
|
/// 判断给定值是否属于指定候选集合。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value">要检查的枚举值。</param>
|
/// <param name="value">要检查的枚举值。</param>
|
||||||
/// <param name="values">用于匹配的候选枚举值集合。</param>
|
/// <param name="values">用于匹配的候选枚举值集合;当为 <see langword="null" /> 时返回 <see langword="false" />。</param>
|
||||||
/// <returns>当 <paramref name="value" /> 命中任一候选值时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
/// <returns>当 <paramref name="value" /> 命中任一候选值时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||||
public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values)
|
public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -11,7 +11,7 @@ namespace TestApp
|
|||||||
/// 判断给定值是否属于指定候选集合。
|
/// 判断给定值是否属于指定候选集合。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value">要检查的枚举值。</param>
|
/// <param name="value">要检查的枚举值。</param>
|
||||||
/// <param name="values">用于匹配的候选枚举值集合。</param>
|
/// <param name="values">用于匹配的候选枚举值集合;当为 <see langword="null" /> 时返回 <see langword="false" />。</param>
|
||||||
/// <returns>当 <paramref name="value" /> 命中任一候选值时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
/// <returns>当 <paramref name="value" /> 命中任一候选值时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||||
public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values)
|
public static bool IsIn(this TestApp.Status value, params TestApp.Status[] values)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -39,7 +39,7 @@ namespace TestApp
|
|||||||
/// 判断给定值是否属于指定候选集合。
|
/// 判断给定值是否属于指定候选集合。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="value">要检查的枚举值。</param>
|
/// <param name="value">要检查的枚举值。</param>
|
||||||
/// <param name="values">用于匹配的候选枚举值集合。</param>
|
/// <param name="values">用于匹配的候选枚举值集合;当为 <see langword="null" /> 时返回 <see langword="false" />。</param>
|
||||||
/// <returns>当 <paramref name="value" /> 命中任一候选值时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
/// <returns>当 <paramref name="value" /> 命中任一候选值时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||||
public static bool IsIn(this TestApp.Permissions value, params TestApp.Permissions[] values)
|
public static bool IsIn(this TestApp.Permissions value, params TestApp.Permissions[] values)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -6,6 +6,11 @@ namespace TestApp;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 为当前规则类型补充自动生成的架构上下文访问实现。
|
/// 为当前规则类型补充自动生成的架构上下文访问实现。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 生成代码会在实例级缓存首次解析到的上下文,并在未显式配置提供者时回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
|
||||||
|
/// 同一生成类型的所有实例共享一个静态上下文提供者;切换或重置提供者只会影响尚未缓存上下文的新实例或未初始化实例,
|
||||||
|
/// 已缓存的实例上下文需要通过 <see cref="GFramework.Core.Abstractions.Rule.IContextAware.SetContext(GFramework.Core.Abstractions.Architectures.IArchitectureContext)" /> 显式覆盖。
|
||||||
|
/// </remarks>
|
||||||
partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
||||||
{
|
{
|
||||||
private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _context;
|
private global::GFramework.Core.Abstractions.Architectures.IArchitectureContext? _context;
|
||||||
@ -13,8 +18,14 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
|||||||
private static readonly object _contextSync = new();
|
private static readonly object _contextSync = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 自动获取的架构上下文(懒加载,默认使用 GameContextProvider)
|
/// 获取当前实例绑定的架构上下文。
|
||||||
/// </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>。
|
||||||
|
/// </remarks>
|
||||||
protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context
|
protected global::GFramework.Core.Abstractions.Architectures.IArchitectureContext Context
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@ -36,9 +47,14 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 配置上下文提供者(用于测试或多架构场景)
|
/// 配置当前生成类型共享的上下文提供者。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="provider">上下文提供者实例</param>
|
/// <param name="provider">后续懒加载上下文时要使用的提供者实例。</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// 该方法使用与 <see cref="Context" /> 相同的同步锁,避免提供者切换与惰性初始化交错。
|
||||||
|
/// 已经缓存上下文的实例不会因为提供者切换而自动失效;该变更仅影响尚未初始化上下文的新实例或未缓存实例。
|
||||||
|
/// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。
|
||||||
|
/// </remarks>
|
||||||
public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)
|
public static void SetContextProvider(global::GFramework.Core.Abstractions.Architectures.IArchitectureContextProvider provider)
|
||||||
{
|
{
|
||||||
lock (_contextSync)
|
lock (_contextSync)
|
||||||
@ -48,8 +64,13 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 重置上下文提供者为默认值(用于测试清理)
|
/// 重置共享上下文提供者,使后续懒加载回退到默认提供者。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 该方法主要用于测试清理或跨用例恢复默认行为。
|
||||||
|
/// 它不会清除已经缓存到实例字段中的上下文;只有后续尚未初始化上下文的实例会重新回退到 <see cref="GFramework.Core.Architectures.GameContextProvider" />。
|
||||||
|
/// 如需覆盖已有实例的上下文,请显式调用 <c>IContextAware.SetContext(...)</c>。
|
||||||
|
/// </remarks>
|
||||||
public static void ResetContextProvider()
|
public static void ResetContextProvider()
|
||||||
{
|
{
|
||||||
lock (_contextSync)
|
lock (_contextSync)
|
||||||
@ -60,7 +81,11 @@ partial class MyRule : global::GFramework.Core.Abstractions.Rule.IContextAware
|
|||||||
|
|
||||||
void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
|
void global::GFramework.Core.Abstractions.Rule.IContextAware.SetContext(global::GFramework.Core.Abstractions.Architectures.IArchitectureContext context)
|
||||||
{
|
{
|
||||||
_context = context;
|
// 与 Context getter 共享同一同步协议,避免显式注入被并发懒加载覆盖。
|
||||||
|
lock (_contextSync)
|
||||||
|
{
|
||||||
|
_context = context;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
global::GFramework.Core.Abstractions.Architectures.IArchitectureContext global::GFramework.Core.Abstractions.Rule.IContextAware.GetContext()
|
global::GFramework.Core.Abstractions.Architectures.IArchitectureContext global::GFramework.Core.Abstractions.Rule.IContextAware.GetContext()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user