Merge pull request #304 from GeWuYou/feat/cqrs-optimization

Feat/cqrs optimization
This commit is contained in:
gewuyou 2026-04-30 09:43:53 +08:00 committed by GitHub
commit 5eea12b5ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 2143 additions and 279 deletions

View File

@ -60,7 +60,6 @@ public sealed partial class CqrsHandlerRegistryGenerator
string? ReflectionAssemblyName,
RuntimeTypeReferenceSpec? ArrayElementTypeReference,
int ArrayRank,
RuntimeTypeReferenceSpec? PointerElementTypeReference,
RuntimeTypeReferenceSpec? GenericTypeDefinitionReference,
ImmutableArray<RuntimeTypeReferenceSpec> GenericTypeArguments)
{
@ -76,7 +75,6 @@ public sealed partial class CqrsHandlerRegistryGenerator
null,
0,
null,
null,
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
}
@ -92,7 +90,6 @@ public sealed partial class CqrsHandlerRegistryGenerator
null,
0,
null,
null,
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
}
@ -110,7 +107,6 @@ public sealed partial class CqrsHandlerRegistryGenerator
null,
0,
null,
null,
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
}
@ -126,7 +122,6 @@ public sealed partial class CqrsHandlerRegistryGenerator
elementTypeReference,
arrayRank,
null,
null,
ImmutableArray<RuntimeTypeReferenceSpec>.Empty);
}
@ -143,7 +138,6 @@ public sealed partial class CqrsHandlerRegistryGenerator
null,
null,
0,
null,
genericTypeDefinitionReference,
genericTypeArguments);
}

View File

@ -304,12 +304,6 @@ public sealed partial class CqrsHandlerRegistryGenerator
return true;
}
if (runtimeTypeReference.PointerElementTypeReference is not null &&
ContainsExternalAssemblyTypeLookup(runtimeTypeReference.PointerElementTypeReference))
{
return true;
}
if (runtimeTypeReference.GenericTypeDefinitionReference is not null &&
ContainsExternalAssemblyTypeLookup(runtimeTypeReference.GenericTypeDefinitionReference))
{

View File

@ -662,14 +662,6 @@ public sealed partial class CqrsHandlerRegistryGenerator
reflectedArgumentNames,
indent);
if (runtimeTypeReference.PointerElementTypeReference is not null)
return AppendPointerRuntimeTypeReferenceResolution(
builder,
runtimeTypeReference,
variableBaseName,
reflectedArgumentNames,
indent);
if (runtimeTypeReference.GenericTypeDefinitionReference is not null)
return AppendConstructedGenericRuntimeTypeReferenceResolution(
builder,
@ -714,32 +706,6 @@ public sealed partial class CqrsHandlerRegistryGenerator
: $"{elementExpression}.MakeArrayType({runtimeTypeReference.ArrayRank})";
}
/// <summary>
/// 发射指针类型引用的运行时重建表达式。
/// </summary>
/// <param name="builder">生成源码构造器。</param>
/// <param name="runtimeTypeReference">指针类型引用描述。</param>
/// <param name="variableBaseName">用于递归生成变量名的稳定前缀。</param>
/// <param name="reflectedArgumentNames">需要空值检查的反射解析变量集合。</param>
/// <param name="indent">当前生成语句的缩进。</param>
/// <returns>指针类型表达式。</returns>
private static string AppendPointerRuntimeTypeReferenceResolution(
StringBuilder builder,
RuntimeTypeReferenceSpec runtimeTypeReference,
string variableBaseName,
ICollection<string> reflectedArgumentNames,
string indent)
{
var pointedAtExpression = AppendRuntimeTypeReferenceResolution(
builder,
runtimeTypeReference.PointerElementTypeReference!,
$"{variableBaseName}PointedAt",
reflectedArgumentNames,
indent);
return $"{pointedAtExpression}.MakePointerType()";
}
/// <summary>
/// 发射已构造泛型类型引用的运行时重建表达式。
/// </summary>

View File

@ -10,6 +10,7 @@ namespace GFramework.Cqrs.Tests.Cqrs;
/// 验证 CQRS dispatcher 会缓存热路径中的 dispatch binding。
/// </summary>
[TestFixture]
[NonParallelizable]
internal sealed class CqrsDispatcherCacheTests
{
private MicrosoftDiContainer? _container;
@ -24,6 +25,9 @@ internal sealed class CqrsDispatcherCacheTests
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
_container = new MicrosoftDiContainer();
_container.RegisterCqrsPipelineBehavior<DispatcherPipelineCacheBehavior>();
_container.RegisterCqrsPipelineBehavior<DispatcherPipelineContextRefreshBehavior>();
_container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderOuterBehavior>();
_container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderInnerBehavior>();
CqrsTestRuntime.RegisterHandlers(
_container,
@ -32,6 +36,9 @@ internal sealed class CqrsDispatcherCacheTests
_container.Freeze();
_context = new ArchitectureContext(_container);
DispatcherNotificationContextRefreshState.Reset();
DispatcherPipelineContextRefreshState.Reset();
DispatcherStreamContextRefreshState.Reset();
ClearDispatcherCaches();
}
@ -145,6 +152,243 @@ internal sealed class CqrsDispatcherCacheTests
});
}
/// <summary>
/// 验证 request pipeline executor 会按行为数量在 binding 内首次创建并在后续分发中复用。
/// </summary>
[Test]
public async Task Dispatcher_Should_Cache_Request_Pipeline_Executors_Per_Behavior_Count()
{
var requestBindings = GetCacheField("RequestDispatchBindings");
Assert.Multiple(() =>
{
Assert.That(
GetRequestPipelineExecutorValue(
requestBindings,
typeof(DispatcherPipelineCacheRequest),
typeof(int),
1),
Is.Null);
Assert.That(
GetRequestPipelineExecutorValue(
requestBindings,
typeof(DispatcherPipelineOrderCacheRequest),
typeof(int),
2),
Is.Null);
});
await _context!.SendRequestAsync(new DispatcherPipelineCacheRequest());
await _context.SendRequestAsync(new DispatcherPipelineOrderCacheRequest());
var singleBehaviorExecutor = GetRequestPipelineExecutorValue(
requestBindings,
typeof(DispatcherPipelineCacheRequest),
typeof(int),
1);
var twoBehaviorExecutor = GetRequestPipelineExecutorValue(
requestBindings,
typeof(DispatcherPipelineOrderCacheRequest),
typeof(int),
2);
await _context.SendRequestAsync(new DispatcherPipelineCacheRequest());
await _context.SendRequestAsync(new DispatcherPipelineOrderCacheRequest());
Assert.Multiple(() =>
{
Assert.That(singleBehaviorExecutor, Is.Not.Null);
Assert.That(twoBehaviorExecutor, Is.Not.Null);
Assert.That(singleBehaviorExecutor, Is.Not.SameAs(twoBehaviorExecutor));
Assert.That(
GetRequestPipelineExecutorValue(
requestBindings,
typeof(DispatcherPipelineCacheRequest),
typeof(int),
1),
Is.SameAs(singleBehaviorExecutor));
Assert.That(
GetRequestPipelineExecutorValue(
requestBindings,
typeof(DispatcherPipelineOrderCacheRequest),
typeof(int),
2),
Is.SameAs(twoBehaviorExecutor));
});
}
/// <summary>
/// 验证复用缓存的 request pipeline executor 后,行为顺序和最终处理器顺序保持不变。
/// </summary>
[Test]
public async Task Dispatcher_Should_Preserve_Request_Pipeline_Order_When_Reusing_Cached_Executor()
{
DispatcherPipelineOrderState.Reset();
await _context!.SendRequestAsync(new DispatcherPipelineOrderCacheRequest());
var firstInvocation = DispatcherPipelineOrderState.Steps.ToArray();
DispatcherPipelineOrderState.Reset();
await _context.SendRequestAsync(new DispatcherPipelineOrderCacheRequest());
var secondInvocation = DispatcherPipelineOrderState.Steps.ToArray();
var expectedOrder = new[]
{
"Outer:Before",
"Inner:Before",
"Handler",
"Inner:After",
"Outer:After"
};
Assert.Multiple(() =>
{
Assert.That(firstInvocation, Is.EqualTo(expectedOrder));
Assert.That(secondInvocation, Is.EqualTo(expectedOrder));
});
}
/// <summary>
/// 验证缓存的 request pipeline executor 在重复分发时仍会重新解析 handler/behavior
/// 并为当次实例重新注入当前架构上下文。
/// </summary>
[Test]
public async Task Dispatcher_Should_Reinject_Current_Context_When_Reusing_Cached_Request_Pipeline_Executor()
{
DispatcherPipelineContextRefreshState.Reset();
var requestBindings = GetCacheField("RequestDispatchBindings");
var firstContext = new ArchitectureContext(_container!);
var secondContext = new ArchitectureContext(_container!);
await firstContext.SendRequestAsync(new DispatcherPipelineContextRefreshRequest("first"));
var executorAfterFirstDispatch = GetRequestPipelineExecutorValue(
requestBindings,
typeof(DispatcherPipelineContextRefreshRequest),
typeof(int),
1);
await secondContext.SendRequestAsync(new DispatcherPipelineContextRefreshRequest("second"));
var executorAfterSecondDispatch = GetRequestPipelineExecutorValue(
requestBindings,
typeof(DispatcherPipelineContextRefreshRequest),
typeof(int),
1);
var behaviorSnapshots = DispatcherPipelineContextRefreshState.BehaviorSnapshots.ToArray();
var handlerSnapshots = DispatcherPipelineContextRefreshState.HandlerSnapshots.ToArray();
Assert.Multiple(() =>
{
Assert.That(executorAfterFirstDispatch, Is.Not.Null);
Assert.That(executorAfterSecondDispatch, Is.SameAs(executorAfterFirstDispatch));
Assert.That(behaviorSnapshots, Has.Length.EqualTo(2));
Assert.That(handlerSnapshots, Has.Length.EqualTo(2));
Assert.That(behaviorSnapshots[0].DispatchId, Is.EqualTo("first"));
Assert.That(behaviorSnapshots[0].Context, Is.SameAs(firstContext));
Assert.That(behaviorSnapshots[1].DispatchId, Is.EqualTo("second"));
Assert.That(behaviorSnapshots[1].Context, Is.SameAs(secondContext));
Assert.That(behaviorSnapshots[1].Context, Is.Not.SameAs(behaviorSnapshots[0].Context));
Assert.That(handlerSnapshots[0].DispatchId, Is.EqualTo("first"));
Assert.That(handlerSnapshots[0].Context, Is.SameAs(firstContext));
Assert.That(handlerSnapshots[1].DispatchId, Is.EqualTo("second"));
Assert.That(handlerSnapshots[1].Context, Is.SameAs(secondContext));
Assert.That(handlerSnapshots[1].Context, Is.Not.SameAs(handlerSnapshots[0].Context));
Assert.That(handlerSnapshots[1].InstanceId, Is.Not.EqualTo(handlerSnapshots[0].InstanceId));
});
}
/// <summary>
/// 验证缓存的 notification dispatch binding 在重复分发时仍会重新解析 handler
/// 并为当次实例重新注入当前架构上下文。
/// </summary>
[Test]
public async Task Dispatcher_Should_Reinject_Current_Context_When_Reusing_Cached_Notification_Dispatch_Binding()
{
DispatcherNotificationContextRefreshState.Reset();
var notificationBindings = GetCacheField("NotificationDispatchBindings");
var firstContext = new ArchitectureContext(_container!);
var secondContext = new ArchitectureContext(_container!);
await firstContext.PublishAsync(new DispatcherNotificationContextRefreshNotification("first"));
var bindingAfterFirstDispatch = GetSingleKeyCacheValue(
notificationBindings,
typeof(DispatcherNotificationContextRefreshNotification));
await secondContext.PublishAsync(new DispatcherNotificationContextRefreshNotification("second"));
var bindingAfterSecondDispatch = GetSingleKeyCacheValue(
notificationBindings,
typeof(DispatcherNotificationContextRefreshNotification));
var handlerSnapshots = DispatcherNotificationContextRefreshState.HandlerSnapshots.ToArray();
Assert.Multiple(() =>
{
Assert.That(bindingAfterFirstDispatch, Is.Not.Null);
Assert.That(bindingAfterSecondDispatch, Is.SameAs(bindingAfterFirstDispatch));
Assert.That(handlerSnapshots, Has.Length.EqualTo(2));
Assert.That(handlerSnapshots[0].DispatchId, Is.EqualTo("first"));
Assert.That(handlerSnapshots[0].Context, Is.SameAs(firstContext));
Assert.That(handlerSnapshots[1].DispatchId, Is.EqualTo("second"));
Assert.That(handlerSnapshots[1].Context, Is.SameAs(secondContext));
Assert.That(handlerSnapshots[1].Context, Is.Not.SameAs(handlerSnapshots[0].Context));
Assert.That(handlerSnapshots[1].InstanceId, Is.Not.EqualTo(handlerSnapshots[0].InstanceId));
});
}
/// <summary>
/// 验证缓存的 stream dispatch binding 在重复建流时仍会重新解析 handler
/// 并为当次实例重新注入当前架构上下文。
/// </summary>
[Test]
public async Task Dispatcher_Should_Reinject_Current_Context_When_Reusing_Cached_Stream_Dispatch_Binding()
{
DispatcherStreamContextRefreshState.Reset();
var streamBindings = GetCacheField("StreamDispatchBindings");
var firstContext = new ArchitectureContext(_container!);
var secondContext = new ArchitectureContext(_container!);
var firstStream = firstContext.CreateStream(new DispatcherStreamContextRefreshRequest("first"));
await DrainAsync(firstStream);
var bindingAfterFirstDispatch = GetPairCacheValue(
streamBindings,
typeof(DispatcherStreamContextRefreshRequest),
typeof(int));
var secondStream = secondContext.CreateStream(new DispatcherStreamContextRefreshRequest("second"));
await DrainAsync(secondStream);
var bindingAfterSecondDispatch = GetPairCacheValue(
streamBindings,
typeof(DispatcherStreamContextRefreshRequest),
typeof(int));
var handlerSnapshots = DispatcherStreamContextRefreshState.HandlerSnapshots.ToArray();
Assert.Multiple(() =>
{
Assert.That(bindingAfterFirstDispatch, Is.Not.Null);
Assert.That(bindingAfterSecondDispatch, Is.SameAs(bindingAfterFirstDispatch));
Assert.That(handlerSnapshots, Has.Length.EqualTo(2));
Assert.That(handlerSnapshots[0].DispatchId, Is.EqualTo("first"));
Assert.That(handlerSnapshots[0].Context, Is.SameAs(firstContext));
Assert.That(handlerSnapshots[1].DispatchId, Is.EqualTo("second"));
Assert.That(handlerSnapshots[1].Context, Is.SameAs(secondContext));
Assert.That(handlerSnapshots[1].Context, Is.Not.SameAs(handlerSnapshots[0].Context));
Assert.That(handlerSnapshots[1].InstanceId, Is.Not.EqualTo(handlerSnapshots[0].InstanceId));
});
}
/// <summary>
/// 通过反射读取 dispatcher 的静态缓存对象。
/// </summary>
@ -188,6 +432,26 @@ internal sealed class CqrsDispatcherCacheTests
return InvokeInstanceMethod(cache, "GetValueOrDefaultForTesting", primaryType, secondaryType);
}
/// <summary>
/// 读取 request dispatch binding 中指定行为数量的 pipeline executor 缓存项。
/// </summary>
/// <param name="requestBindings">dispatcher 内部的 request binding 缓存对象。</param>
/// <param name="requestType">要读取的请求运行时类型。</param>
/// <param name="responseType">要读取的响应运行时类型。</param>
/// <param name="behaviorCount">目标 executor 对应的行为数量。</param>
/// <returns>已缓存的 executor若 binding 或 executor 尚未建立则返回 <see langword="null" />。</returns>
private static object? GetRequestPipelineExecutorValue(
object requestBindings,
Type requestType,
Type responseType,
int behaviorCount)
{
var binding = GetRequestDispatchBindingValue(requestBindings, requestType, responseType);
return binding is null
? null
: InvokeInstanceMethod(binding, "GetPipelineExecutorForTesting", behaviorCount);
}
/// <summary>
/// 调用缓存实例上的无参清理方法。
/// </summary>
@ -210,6 +474,32 @@ internal sealed class CqrsDispatcherCacheTests
return method!.Invoke(target, arguments);
}
/// <summary>
/// 读取指定请求/响应类型对对应的强类型 request dispatch binding。
/// </summary>
/// <param name="requestBindings">dispatcher 内部的 request binding 缓存对象。</param>
/// <param name="requestType">要读取的请求运行时类型。</param>
/// <param name="responseType">要读取的响应运行时类型。</param>
/// <returns>强类型 binding若缓存尚未建立则返回 <see langword="null" />。</returns>
private static object? GetRequestDispatchBindingValue(object requestBindings, Type requestType, Type responseType)
{
var bindingBox = GetPairCacheValue(requestBindings, requestType, responseType);
if (bindingBox is null)
{
return null;
}
var method = bindingBox.GetType().GetMethod(
"Get",
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
Assert.That(method, Is.Not.Null, $"Missing request binding accessor on {bindingBox.GetType().FullName}.");
return method!
.MakeGenericMethod(responseType)
.Invoke(bindingBox, Array.Empty<object>());
}
/// <summary>
/// 获取 CQRS dispatcher 运行时类型。
/// </summary>

View File

@ -0,0 +1,174 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Cqrs;
using GFramework.Cqrs.Tests.Logging;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 验证默认 dispatcher 在上下文注入前置条件不满足时的失败语义。
/// </summary>
[TestFixture]
internal sealed class CqrsDispatcherContextValidationTests
{
/// <summary>
/// 验证当 request handler 需要上下文注入、但当前 CQRS 上下文不实现 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContext" /> 时,
/// dispatcher 会在调用前显式失败。
/// </summary>
[Test]
public void SendAsync_Should_Throw_When_Context_Does_Not_Implement_IArchitectureContext()
{
var runtime = CreateRuntime(
container =>
{
container
.Setup(currentContainer => currentContainer.Get(typeof(IRequestHandler<ContextAwareRequest, int>)))
.Returns(new ContextAwareRequestHandler());
container
.Setup(currentContainer => currentContainer.GetAll(typeof(IPipelineBehavior<ContextAwareRequest, int>)))
.Returns(Array.Empty<object>());
});
Assert.That(
async () => await runtime.SendAsync(new FakeCqrsContext(), new ContextAwareRequest()).ConfigureAwait(false),
Throws.InvalidOperationException.With.Message.Contains("does not implement IArchitectureContext"));
}
/// <summary>
/// 验证当 notification handler 需要上下文注入、但当前 CQRS 上下文不实现 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContext" /> 时,
/// dispatcher 会在发布前显式失败。
/// </summary>
[Test]
public void PublishAsync_Should_Throw_When_Context_Does_Not_Implement_IArchitectureContext()
{
var runtime = CreateRuntime(
container =>
{
container
.Setup(currentContainer => currentContainer.GetAll(typeof(INotificationHandler<ContextAwareNotification>)))
.Returns([new ContextAwareNotificationHandler()]);
});
Assert.That(
async () => await runtime.PublishAsync(new FakeCqrsContext(), new ContextAwareNotification()).ConfigureAwait(false),
Throws.InvalidOperationException.With.Message.Contains("does not implement IArchitectureContext"));
}
/// <summary>
/// 验证当 stream handler 需要上下文注入、但当前 CQRS 上下文不实现 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContext" /> 时,
/// dispatcher 会在建流前显式失败。
/// </summary>
[Test]
public void CreateStream_Should_Throw_When_Context_Does_Not_Implement_IArchitectureContext()
{
var runtime = CreateRuntime(
container =>
{
container
.Setup(currentContainer => currentContainer.Get(typeof(IStreamRequestHandler<ContextAwareStreamRequest, int>)))
.Returns(new ContextAwareStreamHandler());
});
Assert.That(
() => runtime.CreateStream(new FakeCqrsContext(), new ContextAwareStreamRequest()),
Throws.InvalidOperationException.With.Message.Contains("does not implement IArchitectureContext"));
}
/// <summary>
/// 创建一个只满足当前测试最小依赖面的 dispatcher runtime。
/// </summary>
/// <param name="configureContainer">对容器 mock 的额外配置。</param>
/// <returns>默认 CQRS runtime。</returns>
private static GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime CreateRuntime(
Action<Mock<IIocContainer>> configureContainer)
{
var container = new Mock<IIocContainer>(MockBehavior.Strict);
var logger = new TestLogger("CqrsDispatcherContextValidationTests", LogLevel.Debug);
configureContainer(container);
return CqrsRuntimeFactory.CreateRuntime(container.Object, logger);
}
/// <summary>
/// 为失败语义测试提供最小 CQRS 上下文标记,但故意不实现架构上下文能力。
/// </summary>
private sealed class FakeCqrsContext : ICqrsContext
{
}
/// <summary>
/// 为 request 上下文校验提供最小测试请求。
/// </summary>
private sealed record ContextAwareRequest : IRequest<int>;
/// <summary>
/// 为 notification 上下文校验提供最小测试通知。
/// </summary>
private sealed record ContextAwareNotification : INotification;
/// <summary>
/// 为 stream 上下文校验提供最小测试请求。
/// </summary>
private sealed record ContextAwareStreamRequest : IStreamRequest<int>;
/// <summary>
/// 为 request 上下文校验提供需要注入架构上下文的最小 handler。
/// </summary>
private sealed class ContextAwareRequestHandler : CqrsContextAwareHandlerBase, IRequestHandler<ContextAwareRequest, int>
{
/// <summary>
/// 返回固定结果;当前测试只关心调用前的上下文校验。
/// </summary>
/// <param name="request">当前请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>固定整型结果。</returns>
public ValueTask<int> Handle(ContextAwareRequest request, CancellationToken cancellationToken)
{
return ValueTask.FromResult(1);
}
}
/// <summary>
/// 为 notification 上下文校验提供需要注入架构上下文的最小 handler。
/// </summary>
private sealed class ContextAwareNotificationHandler
: CqrsContextAwareHandlerBase,
INotificationHandler<ContextAwareNotification>
{
/// <summary>
/// 返回已完成任务;当前测试只关心调用前的上下文校验。
/// </summary>
/// <param name="notification">当前通知。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>已完成任务。</returns>
public ValueTask Handle(ContextAwareNotification notification, CancellationToken cancellationToken)
{
return ValueTask.CompletedTask;
}
}
/// <summary>
/// 为 stream 上下文校验提供需要注入架构上下文的最小 handler。
/// </summary>
private sealed class ContextAwareStreamHandler
: CqrsContextAwareHandlerBase,
IStreamRequestHandler<ContextAwareStreamRequest, int>
{
/// <summary>
/// 返回一个最小流;当前测试只关心建流前的上下文校验。
/// </summary>
/// <param name="request">当前流请求。</param>
/// <param name="cancellationToken">取消枚举时使用的取消令牌。</param>
/// <returns>包含单个固定元素的异步流。</returns>
public async IAsyncEnumerable<int> Handle(
ContextAwareStreamRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
yield return 1;
await ValueTask.CompletedTask.ConfigureAwait(false);
}
}
}

View File

@ -0,0 +1,259 @@
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Tests.Logging;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 验证 CQRS handler registrar 在 reflection fallback 元数据失效时的可观察告警行为。
/// </summary>
[TestFixture]
[NonParallelizable]
internal sealed class CqrsHandlerRegistrarFallbackFailureTests
{
private ILoggerFactoryProvider? _originalLoggerFactoryProvider;
private CapturingLoggerFactoryProvider? _capturingLoggerFactoryProvider;
/// <summary>
/// 切换为捕获型日志工厂,并清空 registrar 进程级缓存,避免跨用例共享状态污染断言。
/// </summary>
[SetUp]
public void SetUp()
{
_originalLoggerFactoryProvider = LoggerFactoryResolver.Provider;
_capturingLoggerFactoryProvider = new CapturingLoggerFactoryProvider(LogLevel.Warning);
LoggerFactoryResolver.Provider = _capturingLoggerFactoryProvider;
ClearRegistrarCaches();
}
/// <summary>
/// 恢复测试前的日志工厂,并清理 registrar 缓存。
/// </summary>
[TearDown]
public void TearDown()
{
LoggerFactoryResolver.Provider = _originalLoggerFactoryProvider!;
_capturingLoggerFactoryProvider = null;
_originalLoggerFactoryProvider = null;
ClearRegistrarCaches();
}
/// <summary>
/// 验证当 fallback 类型名无法解析时registrar 会跳过该条目并记录告警。
/// </summary>
[Test]
public void RegisterHandlers_Should_Skip_Unresolvable_Named_Fallback_And_Log_Warning()
{
const string missingTypeName =
"GFramework.Cqrs.Tests.Cqrs.MissingGeneratedRegistryNotificationHandler";
var generatedAssembly = CreateGeneratedFallbackAssembly(
"GFramework.Cqrs.Tests.Cqrs.NamedFallbackMissingAssembly, Version=1.0.0.0",
new CqrsReflectionFallbackAttribute(missingTypeName));
generatedAssembly
.Setup(static assembly => assembly.GetType(missingTypeName, false, false))
.Returns((Type?)null);
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
Assert.Multiple(() =>
{
Assert.That(
GetGeneratedRegistryNotificationHandlerTypes(container),
Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)]));
Assert.That(
GetWarningLogs().Any(log =>
log.Message.Contains(
$"Generated CQRS reflection fallback type {missingTypeName} could not be resolved",
StringComparison.Ordinal)),
Is.True);
});
}
/// <summary>
/// 验证当 fallback 类型名解析抛出异常时registrar 会记录该加载失败告警并继续跳过条目。
/// </summary>
[Test]
public void RegisterHandlers_Should_Log_Warning_When_Named_Fallback_Resolution_Throws()
{
const string failingTypeName =
"GFramework.Cqrs.Tests.Cqrs.ThrowingGeneratedRegistryNotificationHandler";
const string exceptionMessage = "Fallback resolution exploded.";
var generatedAssembly = CreateGeneratedFallbackAssembly(
"GFramework.Cqrs.Tests.Cqrs.NamedFallbackThrowingAssembly, Version=1.0.0.0",
new CqrsReflectionFallbackAttribute(failingTypeName));
generatedAssembly
.Setup(static assembly => assembly.GetType(failingTypeName, false, false))
.Throws(new TypeLoadException(exceptionMessage));
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
Assert.Multiple(() =>
{
Assert.That(
GetGeneratedRegistryNotificationHandlerTypes(container),
Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)]));
Assert.That(
GetWarningLogs().Any(log =>
log.Message.Contains(
$"Generated CQRS reflection fallback type {failingTypeName} failed to load",
StringComparison.Ordinal) &&
log.Message.Contains(exceptionMessage, StringComparison.Ordinal)),
Is.True);
});
}
/// <summary>
/// 验证当 direct fallback 类型属于其他程序集时registrar 会跳过该条目并记录跨程序集告警。
/// </summary>
[Test]
public void RegisterHandlers_Should_Skip_Cross_Assembly_Direct_Fallback_Type_And_Log_Warning()
{
var crossAssemblyFallbackType = ReflectionFallbackNotificationContainer.ReflectionOnlyHandlerType;
var generatedAssembly = CreateGeneratedFallbackAssembly(
"GFramework.Cqrs.Tests.Cqrs.DirectFallbackMismatchAssembly, Version=1.0.0.0",
new CqrsReflectionFallbackAttribute(crossAssemblyFallbackType));
var container = new MicrosoftDiContainer();
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
Assert.Multiple(() =>
{
Assert.That(
GetGeneratedRegistryNotificationHandlerTypes(container),
Is.EqualTo([typeof(GeneratedRegistryNotificationHandler)]));
Assert.That(
GetWarningLogs().Any(log =>
log.Message.Contains(
$"Generated CQRS reflection fallback type {crossAssemblyFallbackType.FullName} was declared on assembly",
StringComparison.Ordinal) &&
log.Message.Contains("Skipping mismatched fallback entry.", StringComparison.Ordinal)),
Is.True);
});
}
/// <summary>
/// 创建一个仅通过 generated registry 注册主 handler、并附带指定 fallback 元数据的程序集替身。
/// </summary>
/// <param name="assemblyName">用于日志与缓存键的程序集名。</param>
/// <param name="fallbackAttribute">要暴露给 registrar 的 fallback attribute。</param>
/// <returns>已完成基础接线的程序集 mock。</returns>
private static Mock<Assembly> CreateGeneratedFallbackAssembly(
string assemblyName,
CqrsReflectionFallbackAttribute fallbackAttribute)
{
var generatedAssembly = new Mock<Assembly>();
generatedAssembly
.SetupGet(static assembly => assembly.FullName)
.Returns(assemblyName);
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
.Returns([new CqrsHandlerRegistryAttribute(typeof(PartialGeneratedNotificationHandlerRegistry))]);
generatedAssembly
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsReflectionFallbackAttribute), false))
.Returns([fallbackAttribute]);
return generatedAssembly;
}
/// <summary>
/// 提取容器中针对 generated notification 注册的处理器实现类型。
/// </summary>
/// <param name="container">已执行注册的测试容器。</param>
/// <returns>按注册顺序返回的处理器类型数组。</returns>
private static Type[] GetGeneratedRegistryNotificationHandlerTypes(MicrosoftDiContainer container)
{
return container.GetServicesUnsafe
.Where(static descriptor =>
descriptor.ServiceType == typeof(INotificationHandler<GeneratedRegistryNotification>) &&
descriptor.ImplementationType is not null)
.Select(static descriptor => descriptor.ImplementationType!)
.ToArray();
}
/// <summary>
/// 清空本测试依赖的 registrar 静态缓存,确保每个用例都会重新执行 fallback 元数据解析。
/// 这些字段名直接耦合 <c>CqrsHandlerRegistrar</c> 当前内部实现;若后续重构缓存布局,需要同步更新这里。
/// </summary>
private static void ClearRegistrarCaches()
{
ClearCache(GetRegistrarCacheField("AssemblyMetadataCache"));
ClearCache(GetRegistrarCacheField("RegistryActivationMetadataCache"));
ClearCache(GetRegistrarCacheField("LoadableTypesCache"));
ClearCache(GetRegistrarCacheField("SupportedHandlerInterfacesCache"));
}
/// <summary>
/// 通过反射读取 registrar 的静态缓存字段。
/// </summary>
/// <param name="fieldName">缓存字段名。</param>
/// <returns>缓存实例。</returns>
private static object GetRegistrarCacheField(string fieldName)
{
var field = GetRegistrarType().GetField(
fieldName,
BindingFlags.NonPublic | BindingFlags.Static);
Assert.That(
field,
Is.Not.Null,
$"Expected field '{fieldName}' on CqrsHandlerRegistrar not found; rename/refactor may require test update.");
return field!.GetValue(null)
?? throw new InvalidOperationException(
$"Registrar cache field '{fieldName}' on CqrsHandlerRegistrar returned null.");
}
/// <summary>
/// 清空缓存对象中的已保存条目。
/// </summary>
/// <param name="cache">目标缓存实例。</param>
private static void ClearCache(object cache)
{
_ = InvokeInstanceMethod(cache, "Clear");
}
/// <summary>
/// 调用缓存对象上的实例方法。
/// </summary>
/// <param name="target">目标对象。</param>
/// <param name="methodName">方法名。</param>
/// <param name="arguments">方法参数。</param>
/// <returns>方法返回值。</returns>
private static object? InvokeInstanceMethod(object target, string methodName, params object[] arguments)
{
var method = target.GetType().GetMethod(
methodName,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
Assert.That(method, Is.Not.Null, $"Missing cache method {target.GetType().FullName}.{methodName}.");
return method!.Invoke(target, arguments);
}
/// <summary>
/// 获取 CQRS handler registrar 的运行时类型。
/// </summary>
/// <returns>registrar 实现类型。</returns>
private static Type GetRegistrarType()
{
return typeof(CqrsReflectionFallbackAttribute).Assembly
.GetType("GFramework.Cqrs.Internal.CqrsHandlerRegistrar", throwOnError: true)!;
}
/// <summary>
/// 汇总当前测试期间捕获到的 warning 日志。
/// </summary>
/// <returns>所有 warning 级别日志条目。</returns>
private IReadOnlyList<TestLogger.LogEntry> GetWarningLogs()
{
Assert.That(_capturingLoggerFactoryProvider, Is.Not.Null);
return _capturingLoggerFactoryProvider!.Loggers
.SelectMany(static logger => logger.Logs)
.Where(static log => log.Level == LogLevel.Warning)
.ToArray();
}
}

View File

@ -0,0 +1,97 @@
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 验证 <see cref="CqrsReflectionFallbackAttribute" /> 公开构造器的归一化合同,
/// 以固定 runtime 读取程序集级 fallback 元数据时可依赖的可观察语义。
/// </summary>
[TestFixture]
internal sealed class CqrsReflectionFallbackAttributeTests
{
/// <summary>
/// 验证无参构造器会保留旧版 marker 语义,并暴露空的 fallback 集合。
/// </summary>
[Test]
public void Constructor_Without_Arguments_Should_Expose_Empty_Fallback_Collections()
{
var attribute = new CqrsReflectionFallbackAttribute();
Assert.Multiple(() =>
{
Assert.That(attribute.FallbackHandlerTypeNames, Is.Empty);
Assert.That(attribute.FallbackHandlerTypes, Is.Empty);
});
}
/// <summary>
/// 验证字符串名称重载会过滤空白项,并按序号稳定去重排序,
/// 确保 runtime 后续读取到的名称清单不依赖调用端输入顺序。
/// </summary>
[Test]
public void Constructor_With_Type_Names_Should_Normalize_By_Filtering_Deduplicating_And_Sorting()
{
var attribute = new CqrsReflectionFallbackAttribute(
"Zeta.Handler",
" ",
"Alpha.Handler",
"Zeta.Handler",
string.Empty,
"Beta.Handler",
"Alpha.Handler");
Assert.Multiple(() =>
{
Assert.That(
attribute.FallbackHandlerTypeNames,
Is.EqualTo(["Alpha.Handler", "Beta.Handler", "Zeta.Handler"]));
Assert.That(attribute.FallbackHandlerTypes, Is.Empty);
});
}
/// <summary>
/// 验证字符串名称重载收到 <see langword="null" /> 参数数组时会立即拒绝,
/// 避免 runtime 在读取程序集元数据时延迟暴露无效状态。
/// </summary>
[Test]
public void Constructor_With_Null_Type_Name_Array_Should_Throw_ArgumentNullException()
{
Assert.That(
() => _ = new CqrsReflectionFallbackAttribute((string[])null!),
Throws.ArgumentNullException);
}
/// <summary>
/// 验证 <see cref="Type" /> 重载会过滤空引用,并按稳定名称顺序去重,
/// 确保后续 fallback 补扫不会因为重复输入或反射枚举顺序产生非确定性。
/// </summary>
[Test]
public void Constructor_With_Types_Should_Normalize_By_Filtering_Deduplicating_And_Sorting()
{
var attribute = new CqrsReflectionFallbackAttribute(
typeof(string),
null!,
typeof(Uri),
typeof(string),
typeof(Version));
// 这里按 FullName 的 Ordinal 顺序断言,固定该 attribute 对 runtime 暴露的元数据排序合同。
Assert.Multiple(() =>
{
Assert.That(
attribute.FallbackHandlerTypes,
Is.EqualTo([typeof(string), typeof(Uri), typeof(Version)]));
Assert.That(attribute.FallbackHandlerTypeNames, Is.Empty);
});
}
/// <summary>
/// 验证 <see cref="Type" /> 重载收到 <see langword="null" /> 参数数组时会立即拒绝,
/// 从而维持 attribute 元数据的最小有效性边界。
/// </summary>
[Test]
public void Constructor_With_Null_Type_Array_Should_Throw_ArgumentNullException()
{
Assert.That(
() => _ = new CqrsReflectionFallbackAttribute((Type[])null!),
Throws.ArgumentNullException);
}
}

View File

@ -0,0 +1,101 @@
using GFramework.Core.Abstractions.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Tests.Logging;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 验证 CQRS 程序集注册协调器在程序集键去重层面的可观察行为。
/// </summary>
[TestFixture]
internal sealed class CqrsRegistrationServiceTests
{
/// <summary>
/// 验证同一次调用内出现重复程序集键时,底层注册器只会接收到一次注册请求。
/// </summary>
[Test]
public void RegisterHandlers_Should_Register_Duplicate_Assembly_Key_Only_Once_Per_Call()
{
var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug);
var registrar = new Mock<ICqrsHandlerRegistrar>(MockBehavior.Strict);
var duplicateAssemblyA = CreateAssembly("GFramework.Cqrs.Tests.DuplicateAssembly, Version=1.0.0.0");
var duplicateAssemblyB = CreateAssembly("GFramework.Cqrs.Tests.DuplicateAssembly, Version=1.0.0.0");
var expectedAssembly = duplicateAssemblyA.Object;
IEnumerable<Assembly>? registeredAssemblies = null;
registrar
.Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()))
.Callback<IEnumerable<Assembly>>(assemblies => registeredAssemblies = assemblies.ToArray());
var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger);
service.RegisterHandlers([duplicateAssemblyA.Object, duplicateAssemblyB.Object]);
registrar.Verify(
static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()),
Times.Once);
Assert.Multiple(() =>
{
Assert.That(registeredAssemblies, Is.Not.Null);
Assert.That(registeredAssemblies, Is.EqualTo([expectedAssembly]));
Assert.That(logger.Logs, Has.Count.EqualTo(0));
});
}
/// <summary>
/// 验证跨两次调用重复程序集键时,协调器会跳过重复注册并写入 debug 日志。
/// </summary>
[Test]
public void RegisterHandlers_Should_Skip_Already_Registered_Assembly_Key_Across_Calls_And_Log_Debug_Message()
{
var logger = new TestLogger("DefaultCqrsRegistrationService", LogLevel.Debug);
var registrar = new Mock<ICqrsHandlerRegistrar>(MockBehavior.Strict);
var firstAssembly = CreateAssembly("GFramework.Cqrs.Tests.RegisteredAssembly, Version=1.0.0.0");
var secondAssembly = CreateAssembly("GFramework.Cqrs.Tests.RegisteredAssembly, Version=1.0.0.0");
IEnumerable<Assembly>? registeredAssemblies = null;
registrar
.Setup(static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()))
.Callback<IEnumerable<Assembly>>(assemblies => registeredAssemblies = assemblies.ToArray());
var service = CqrsRuntimeFactory.CreateRegistrationService(registrar.Object, logger);
service.RegisterHandlers([firstAssembly.Object]);
service.RegisterHandlers([secondAssembly.Object]);
registrar.Verify(
static currentRegistrar => currentRegistrar.RegisterHandlers(It.IsAny<IEnumerable<Assembly>>()),
Times.Once);
Assert.Multiple(() =>
{
Assert.That(registeredAssemblies, Is.EqualTo([firstAssembly.Object]));
var debugMessages = logger.Logs
.Where(static log => log.Level == LogLevel.Debug)
.Select(static log => log.Message)
.ToArray();
Assert.That(debugMessages, Has.Length.EqualTo(1));
Assert.That(
debugMessages[0],
Does.Contain("Skipping CQRS handler registration for assembly"));
Assert.That(
debugMessages[0],
Does.Contain("GFramework.Cqrs.Tests.RegisteredAssembly, Version=1.0.0.0"));
Assert.That(debugMessages[0], Does.Contain("already registered"));
});
}
/// <summary>
/// 创建一个带稳定程序集键的程序集 mock用于模拟不同 <see cref="Assembly" /> 实例表示同一程序集的场景。
/// </summary>
/// <param name="assemblyFullName">要返回的程序集完整名称。</param>
/// <returns>配置好完整名称的程序集 mock。</returns>
private static Mock<Assembly> CreateAssembly(string assemblyFullName)
{
var assembly = new Mock<Assembly>();
assembly
.SetupGet(static currentAssembly => currentAssembly.FullName)
.Returns(assemblyFullName);
return assembly;
}
}

View File

@ -0,0 +1,29 @@
using System.Threading;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 记录缓存 notification binding 复用场景下每次分发注入到 handler 的上下文与实例身份。
/// </summary>
internal sealed class DispatcherNotificationContextRefreshHandler
: CqrsContextAwareHandlerBase,
INotificationHandler<DispatcherNotificationContextRefreshNotification>
{
private readonly int _instanceId = DispatcherNotificationContextRefreshState.AllocateHandlerInstanceId();
/// <summary>
/// 记录当前 handler 实例收到的上下文。
/// </summary>
/// <param name="notification">当前通知。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>已完成任务。</returns>
public ValueTask Handle(
DispatcherNotificationContextRefreshNotification notification,
CancellationToken cancellationToken)
{
DispatcherNotificationContextRefreshState.Record(notification.DispatchId, _instanceId, Context);
return ValueTask.CompletedTask;
}
}

View File

@ -0,0 +1,9 @@
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 为 notification dispatch binding 上下文刷新回归提供带分发标识的最小通知。
/// </summary>
/// <param name="DispatchId">当前分发的稳定标识,便于断言缓存 binding 复用时观察到的是同一次通知。</param>
internal sealed record DispatcherNotificationContextRefreshNotification(string DispatchId) : INotification;

View File

@ -0,0 +1,60 @@
using System.Threading;
using GFramework.Core.Abstractions.Architectures;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 记录 notification dispatch binding 缓存回归中每次分发实际使用的上下文与实例身份。
/// </summary>
internal static class DispatcherNotificationContextRefreshState
{
private static readonly Lock SyncRoot = new();
private static int _nextHandlerInstanceId;
private static readonly List<DispatcherPipelineContextSnapshot> _handlerSnapshots = [];
/// <summary>
/// 获取每次 notification 分发时记录的快照副本。
/// 共享状态通过 <c>SyncRoot</c> 串行化,避免并行测试写入抖动。
/// </summary>
public static IReadOnlyList<DispatcherPipelineContextSnapshot> HandlerSnapshots
{
get
{
lock (SyncRoot)
{
return _handlerSnapshots.ToArray();
}
}
}
/// <summary>
/// 为新的 handler 测试实例分配稳定编号。
/// </summary>
public static int AllocateHandlerInstanceId()
{
return Interlocked.Increment(ref _nextHandlerInstanceId);
}
/// <summary>
/// 记录 handler 在当前分发中观察到的上下文。
/// </summary>
public static void Record(string dispatchId, int instanceId, IArchitectureContext context)
{
lock (SyncRoot)
{
_handlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context));
}
}
/// <summary>
/// 清空历史记录与实例编号,避免跨测试污染断言。
/// </summary>
public static void Reset()
{
lock (SyncRoot)
{
_nextHandlerInstanceId = 0;
_handlerSnapshots.Clear();
}
}
}

View File

@ -0,0 +1,31 @@
using System.Threading;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 记录缓存 executor 复用场景下每次分发注入到 behavior 的上下文与实例身份。
/// </summary>
internal sealed class DispatcherPipelineContextRefreshBehavior
: CqrsContextAwareHandlerBase,
IPipelineBehavior<DispatcherPipelineContextRefreshRequest, int>
{
private readonly int _instanceId = DispatcherPipelineContextRefreshState.AllocateBehaviorInstanceId();
/// <summary>
/// 记录当前 behavior 实例实际收到的上下文,然后继续执行下游处理器。
/// </summary>
/// <param name="request">当前请求。</param>
/// <param name="next">下一个处理阶段。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>下游处理结果。</returns>
public async ValueTask<int> Handle(
DispatcherPipelineContextRefreshRequest request,
MessageHandlerDelegate<DispatcherPipelineContextRefreshRequest, int> next,
CancellationToken cancellationToken)
{
DispatcherPipelineContextRefreshState.RecordBehavior(request.DispatchId, _instanceId, Context);
return await next(request, cancellationToken).ConfigureAwait(false);
}
}

View File

@ -0,0 +1,9 @@
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 为 pipeline executor 上下文刷新回归提供带分发标识的最小请求。
/// </summary>
/// <param name="DispatchId">当前分发的稳定标识,便于断言 handler 与 behavior 看到的是同一次请求。</param>
internal sealed record DispatcherPipelineContextRefreshRequest(string DispatchId) : IRequest<int>;

View File

@ -0,0 +1,29 @@
using System.Threading;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 记录缓存 executor 复用场景下每次分发注入到 request handler 的上下文与实例身份。
/// </summary>
internal sealed class DispatcherPipelineContextRefreshRequestHandler
: CqrsContextAwareHandlerBase,
IRequestHandler<DispatcherPipelineContextRefreshRequest, int>
{
private readonly int _instanceId = DispatcherPipelineContextRefreshState.AllocateHandlerInstanceId();
/// <summary>
/// 记录当前 handler 实例收到的上下文,并返回稳定结果。
/// </summary>
/// <param name="request">当前请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>固定整数结果。</returns>
public ValueTask<int> Handle(
DispatcherPipelineContextRefreshRequest request,
CancellationToken cancellationToken)
{
DispatcherPipelineContextRefreshState.RecordHandler(request.DispatchId, _instanceId, Context);
return ValueTask.FromResult(7);
}
}

View File

@ -0,0 +1,98 @@
using System.Threading;
using GFramework.Core.Abstractions.Architectures;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 记录 pipeline executor 缓存回归中每次分发实际使用的上下文与实例身份。
/// </summary>
internal static class DispatcherPipelineContextRefreshState
{
private static readonly Lock SyncRoot = new();
private static int _nextBehaviorInstanceId;
private static int _nextHandlerInstanceId;
private static readonly List<DispatcherPipelineContextSnapshot> _behaviorSnapshots = [];
private static readonly List<DispatcherPipelineContextSnapshot> _handlerSnapshots = [];
/// <summary>
/// 获取每次 behavior 执行时记录的快照副本。
/// 共享状态通过 <c>SyncRoot</c> 串行化,读取端始终拿到当前稳定快照。
/// </summary>
public static IReadOnlyList<DispatcherPipelineContextSnapshot> BehaviorSnapshots
{
get
{
lock (SyncRoot)
{
return _behaviorSnapshots.ToArray();
}
}
}
/// <summary>
/// 获取每次 handler 执行时记录的快照副本。
/// 共享状态通过 <c>SyncRoot</c> 串行化,读取端始终拿到当前稳定快照。
/// </summary>
public static IReadOnlyList<DispatcherPipelineContextSnapshot> HandlerSnapshots
{
get
{
lock (SyncRoot)
{
return _handlerSnapshots.ToArray();
}
}
}
/// <summary>
/// 为新的 behavior 测试实例分配稳定编号。
/// </summary>
public static int AllocateBehaviorInstanceId()
{
return Interlocked.Increment(ref _nextBehaviorInstanceId);
}
/// <summary>
/// 为新的 handler 测试实例分配稳定编号。
/// </summary>
public static int AllocateHandlerInstanceId()
{
return Interlocked.Increment(ref _nextHandlerInstanceId);
}
/// <summary>
/// 记录 behavior 在当前分发中观察到的上下文。
/// </summary>
public static void RecordBehavior(string dispatchId, int instanceId, IArchitectureContext context)
{
lock (SyncRoot)
{
_behaviorSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context));
}
}
/// <summary>
/// 记录 handler 在当前分发中观察到的上下文。
/// </summary>
public static void RecordHandler(string dispatchId, int instanceId, IArchitectureContext context)
{
lock (SyncRoot)
{
_handlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context));
}
}
/// <summary>
/// 清空历史记录与实例编号,避免跨测试污染断言。
/// </summary>
public static void Reset()
{
lock (SyncRoot)
{
_nextBehaviorInstanceId = 0;
_nextHandlerInstanceId = 0;
_behaviorSnapshots.Clear();
_handlerSnapshots.Clear();
}
}
}

View File

@ -0,0 +1,14 @@
using GFramework.Core.Abstractions.Architectures;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 描述单次分发阶段记录下来的上下文与实例身份。
/// </summary>
/// <param name="DispatchId">触发本次记录的请求标识。</param>
/// <param name="InstanceId">当次 handler 或 behavior 实例编号。</param>
/// <param name="Context">当次分发注入的架构上下文。</param>
internal sealed record DispatcherPipelineContextSnapshot(
string DispatchId,
int InstanceId,
IArchitectureContext Context);

View File

@ -0,0 +1,8 @@
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 为双行为 pipeline 顺序回归提供最小请求。
/// </summary>
internal sealed record DispatcherPipelineOrderCacheRequest : IRequest<int>;

View File

@ -0,0 +1,21 @@
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 为双行为顺序回归提供最终请求处理器。
/// </summary>
internal sealed class DispatcherPipelineOrderCacheRequestHandler : IRequestHandler<DispatcherPipelineOrderCacheRequest, int>
{
/// <summary>
/// 记录处理器执行并返回固定结果。
/// </summary>
/// <param name="request">当前请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>固定整数结果。</returns>
public ValueTask<int> Handle(DispatcherPipelineOrderCacheRequest request, CancellationToken cancellationToken)
{
DispatcherPipelineOrderState.Record("Handler");
return ValueTask.FromResult(3);
}
}

View File

@ -0,0 +1,27 @@
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 作为内层行为验证缓存 executor 复用后仍保持注册顺序。
/// </summary>
internal sealed class DispatcherPipelineOrderInnerBehavior : IPipelineBehavior<DispatcherPipelineOrderCacheRequest, int>
{
/// <summary>
/// 记录内层行为的前后执行节点。
/// </summary>
/// <param name="request">当前请求。</param>
/// <param name="next">下一个处理阶段。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>下游处理器结果。</returns>
public async ValueTask<int> Handle(
DispatcherPipelineOrderCacheRequest request,
MessageHandlerDelegate<DispatcherPipelineOrderCacheRequest, int> next,
CancellationToken cancellationToken)
{
DispatcherPipelineOrderState.Record("Inner:Before");
var result = await next(request, cancellationToken).ConfigureAwait(false);
DispatcherPipelineOrderState.Record("Inner:After");
return result;
}
}

View File

@ -0,0 +1,27 @@
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 作为外层行为验证缓存 executor 复用后仍保持注册顺序。
/// </summary>
internal sealed class DispatcherPipelineOrderOuterBehavior : IPipelineBehavior<DispatcherPipelineOrderCacheRequest, int>
{
/// <summary>
/// 记录外层行为的前后执行节点。
/// </summary>
/// <param name="request">当前请求。</param>
/// <param name="next">下一个处理阶段。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>下游处理器结果。</returns>
public async ValueTask<int> Handle(
DispatcherPipelineOrderCacheRequest request,
MessageHandlerDelegate<DispatcherPipelineOrderCacheRequest, int> next,
CancellationToken cancellationToken)
{
DispatcherPipelineOrderState.Record("Outer:Before");
var result = await next(request, cancellationToken).ConfigureAwait(false);
DispatcherPipelineOrderState.Record("Outer:After");
return result;
}
}

View File

@ -0,0 +1,50 @@
using System.Threading;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 记录双行为 pipeline 的实际执行顺序。
/// </summary>
internal static class DispatcherPipelineOrderState
{
private static readonly Lock SyncRoot = new();
private static readonly List<string> _steps = [];
/// <summary>
/// 获取按执行顺序追加的步骤快照。
/// 共享状态通过 <c>SyncRoot</c> 串行化,避免并行行为测试互相污染步骤列表。
/// </summary>
public static IReadOnlyList<string> Steps
{
get
{
lock (SyncRoot)
{
return _steps.ToArray();
}
}
}
/// <summary>
/// 记录一个新的 pipeline 执行步骤。
/// </summary>
/// <param name="step">要追加的步骤名称。</param>
public static void Record(string step)
{
lock (SyncRoot)
{
_steps.Add(step);
}
}
/// <summary>
/// 清空当前记录,供下一次断言使用。
/// </summary>
public static void Reset()
{
lock (SyncRoot)
{
_steps.Clear();
}
}
}

View File

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 记录缓存 stream binding 复用场景下每次分发注入到 handler 的上下文与实例身份。
/// </summary>
internal sealed class DispatcherStreamContextRefreshHandler
: CqrsContextAwareHandlerBase,
IStreamRequestHandler<DispatcherStreamContextRefreshRequest, int>
{
private readonly int _instanceId = DispatcherStreamContextRefreshState.AllocateHandlerInstanceId();
/// <summary>
/// 记录当前 handler 实例收到的上下文,并返回稳定元素。
/// </summary>
/// <param name="request">当前流请求。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>包含一个固定元素的异步流。</returns>
public async IAsyncEnumerable<int> Handle(
DispatcherStreamContextRefreshRequest request,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
DispatcherStreamContextRefreshState.Record(request.DispatchId, _instanceId, Context);
yield return 11;
await ValueTask.CompletedTask.ConfigureAwait(false);
}
}

View File

@ -0,0 +1,9 @@
using GFramework.Cqrs.Abstractions.Cqrs;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 为 stream dispatch binding 上下文刷新回归提供带分发标识的最小流请求。
/// </summary>
/// <param name="DispatchId">当前分发的稳定标识,便于断言缓存 binding 复用时观察到的是同一次建流。</param>
internal sealed record DispatcherStreamContextRefreshRequest(string DispatchId) : IStreamRequest<int>;

View File

@ -0,0 +1,67 @@
using System.Threading;
using GFramework.Core.Abstractions.Architectures;
namespace GFramework.Cqrs.Tests.Cqrs;
/// <summary>
/// 记录 stream dispatch binding 缓存回归中每次分发实际使用的上下文与实例身份。
/// </summary>
internal static class DispatcherStreamContextRefreshState
{
private static readonly Lock _syncRoot = new();
private static int _nextHandlerInstanceId;
private static readonly List<DispatcherPipelineContextSnapshot> _handlerSnapshots = [];
/// <summary>
/// 获取每次建流时记录的快照副本。
/// </summary>
/// <returns>当前已记录的 handler 上下文快照副本。</returns>
/// <remarks>共享状态通过 <c>_syncRoot</c> 串行化,避免并行测试写入抖动。</remarks>
public static IReadOnlyList<DispatcherPipelineContextSnapshot> HandlerSnapshots
{
get
{
lock (_syncRoot)
{
return _handlerSnapshots.ToArray();
}
}
}
/// <summary>
/// 为新的 handler 测试实例分配稳定编号。
/// </summary>
/// <returns>单调递增的 handler 实例编号。</returns>
public static int AllocateHandlerInstanceId()
{
return Interlocked.Increment(ref _nextHandlerInstanceId);
}
/// <summary>
/// 记录 handler 在当前建流中观察到的上下文。
/// </summary>
/// <param name="dispatchId">触发本次记录的稳定分发标识。</param>
/// <param name="instanceId">观察到该上下文的 handler 实例编号。</param>
/// <param name="context">当前分发注入到 handler 的架构上下文。</param>
/// <remarks>写入过程通过 <c>_syncRoot</c> 串行化,确保快照列表保持稳定顺序。</remarks>
public static void Record(string dispatchId, int instanceId, IArchitectureContext context)
{
lock (_syncRoot)
{
_handlerSnapshots.Add(new DispatcherPipelineContextSnapshot(dispatchId, instanceId, context));
}
}
/// <summary>
/// 清空历史记录与实例编号,避免跨测试污染断言。
/// </summary>
/// <remarks>重置过程通过 <c>_syncRoot</c> 串行化,避免读取端观察到半清理状态。</remarks>
public static void Reset()
{
lock (_syncRoot)
{
_nextHandlerInstanceId = 0;
_handlerSnapshots.Clear();
}
}
}

View File

@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using GFramework.Core.Abstractions.Architectures;
using GFramework.Core.Abstractions.Ioc;
using GFramework.Core.Abstractions.Logging;
@ -34,7 +35,7 @@ internal sealed class CqrsDispatcher(
.GetMethod(nameof(InvokeRequestHandlerAsync), BindingFlags.NonPublic | BindingFlags.Static)!;
private static readonly MethodInfo RequestPipelineInvokerMethodDefinition = typeof(CqrsDispatcher)
.GetMethod(nameof(InvokeRequestPipelineAsync), BindingFlags.NonPublic | BindingFlags.Static)!;
.GetMethod(nameof(InvokeRequestPipelineExecutorAsync), BindingFlags.NonPublic | BindingFlags.Static)!;
private static readonly MethodInfo NotificationHandlerInvokerMethodDefinition = typeof(CqrsDispatcher)
.GetMethod(nameof(InvokeNotificationHandlerAsync), BindingFlags.NonPublic | BindingFlags.Static)!;
@ -61,7 +62,7 @@ internal sealed class CqrsDispatcher(
var notificationType = notification.GetType();
var dispatchBinding = NotificationDispatchBindings.GetOrAdd(
notificationType,
CreateNotificationDispatchBinding);
static notificationType => CreateNotificationDispatchBinding(notificationType));
var handlers = container.GetAll(dispatchBinding.HandlerType);
if (handlers.Count == 0)
@ -108,7 +109,8 @@ internal sealed class CqrsDispatcher(
if (behaviors.Count == 0)
return await dispatchBinding.RequestInvoker(handler, request, cancellationToken).ConfigureAwait(false);
return await dispatchBinding.PipelineInvoker(handler, behaviors, request, cancellationToken)
return await dispatchBinding.GetPipelineExecutor(behaviors.Count)
.Invoke(handler, behaviors, request, cancellationToken)
.ConfigureAwait(false);
}
@ -132,7 +134,7 @@ internal sealed class CqrsDispatcher(
var dispatchBinding = StreamDispatchBindings.GetOrAdd(
requestType,
typeof(TResponse),
CreateStreamDispatchBinding);
static (requestType, responseType) => CreateStreamDispatchBinding(requestType, responseType));
var handler = container.Get(dispatchBinding.HandlerType)
?? throw new InvalidOperationException(
$"No CQRS stream handler registered for {requestType.FullName}.");
@ -168,7 +170,7 @@ internal sealed class CqrsDispatcher(
typeof(IRequestHandler<,>).MakeGenericType(requestType, typeof(TResponse)),
typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)),
CreateRequestInvoker<TResponse>(requestType),
CreateRequestPipelineInvoker<TResponse>(requestType));
requestType);
}
/// <summary>
@ -179,7 +181,8 @@ internal sealed class CqrsDispatcher(
var bindingBox = RequestDispatchBindings.GetOrAdd(
requestType,
typeof(TResponse),
CreateRequestDispatchBindingBox<TResponse>);
static (cachedRequestType, cachedResponseType) =>
CreateRequestDispatchBindingBox<TResponse>(cachedRequestType, cachedResponseType));
return bindingBox.Get<TResponse>();
}
@ -227,18 +230,6 @@ internal sealed class CqrsDispatcher(
return (RequestInvoker<TResponse>)Delegate.CreateDelegate(typeof(RequestInvoker<TResponse>), method);
}
/// <summary>
/// 生成带管道行为的请求处理委托,避免每次发送都重复反射。
/// </summary>
private static RequestPipelineInvoker<TResponse> CreateRequestPipelineInvoker<TResponse>(Type requestType)
{
var method = RequestPipelineInvokerMethodDefinition
.MakeGenericMethod(requestType, typeof(TResponse));
return (RequestPipelineInvoker<TResponse>)Delegate.CreateDelegate(
typeof(RequestPipelineInvoker<TResponse>),
method);
}
/// <summary>
/// 生成通知处理器调用委托,避免每次发布都重复反射。
/// </summary>
@ -274,29 +265,20 @@ internal sealed class CqrsDispatcher(
}
/// <summary>
/// 执行包含管道行为链的请求处理。
/// 执行指定行为数量的强类型 request pipeline executor。
/// 该入口本身是缓存的固定 executor 形状;每次分发只绑定当前 handler 与 behaviors 实例。
/// </summary>
private static ValueTask<TResponse> InvokeRequestPipelineAsync<TRequest, TResponse>(
private static ValueTask<TResponse> InvokeRequestPipelineExecutorAsync<TRequest, TResponse>(
object handler,
IReadOnlyList<object> behaviors,
object request,
CancellationToken cancellationToken)
where TRequest : IRequest<TResponse>
{
var typedHandler = (IRequestHandler<TRequest, TResponse>)handler;
var typedRequest = (TRequest)request;
MessageHandlerDelegate<TRequest, TResponse> next =
(message, token) => typedHandler.Handle(message, token);
for (var i = behaviors.Count - 1; i >= 0; i--)
{
var behavior = (IPipelineBehavior<TRequest, TResponse>)behaviors[i];
var currentNext = next;
next = (message, token) => behavior.Handle(message, currentNext, token);
}
return next(typedRequest, cancellationToken);
var invocation = new RequestPipelineInvocation<TRequest, TResponse>(
(IRequestHandler<TRequest, TResponse>)handler,
behaviors);
return invocation.InvokeAsync((TRequest)request, cancellationToken);
}
/// <summary>
@ -424,15 +406,21 @@ internal sealed class CqrsDispatcher(
/// <summary>
/// 保存普通请求分发路径所需的 handler 服务类型、pipeline 服务类型与强类型调用委托。
/// 该绑定同时覆盖“直接请求处理”和“带 pipeline 的请求处理”两条路径。
/// 该绑定同时覆盖“直接请求处理”和“按行为数量缓存 pipeline executor 形状”的两条路径。
/// </summary>
/// <typeparam name="TResponse">请求响应类型。</typeparam>
private sealed class RequestDispatchBinding<TResponse>(
Type handlerType,
Type behaviorType,
RequestInvoker<TResponse> requestInvoker,
RequestPipelineInvoker<TResponse> pipelineInvoker)
Type requestType)
{
// 线程安全:该缓存按 behaviorCount 复用 pipeline executor 形状GetPipelineExecutor 通过 ConcurrentDictionary
// 的 GetOrAdd 支持并发读写。缓存项只保存委托形状,不保留 handler/behavior 实例;若行为数量组合持续增长,
// 字典会随之增长且当前实现不提供回收。
private readonly ConcurrentDictionary<int, RequestPipelineExecutor<TResponse>> _pipelineExecutors = new();
private readonly RequestPipelineInvoker<TResponse> _pipelineInvoker = CreateRequestPipelineInvoker<TResponse>(requestType);
/// <summary>
/// 获取请求处理器在容器中的服务类型。
/// </summary>
@ -449,8 +437,173 @@ internal sealed class CqrsDispatcher(
public RequestInvoker<TResponse> RequestInvoker { get; } = requestInvoker;
/// <summary>
/// 获取执行 pipeline 行为链的强类型委托。
/// 获取指定行为数量对应的 pipeline executor。
/// executor 形状会按请求/响应类型与行为数量缓存,但不会缓存 handler 或 behavior 实例。
/// </summary>
public RequestPipelineInvoker<TResponse> PipelineInvoker { get; } = pipelineInvoker;
public RequestPipelineExecutor<TResponse> GetPipelineExecutor(int behaviorCount)
{
ArgumentOutOfRangeException.ThrowIfNegative(behaviorCount);
return _pipelineExecutors.GetOrAdd<RequestPipelineExecutorFactoryState<TResponse>>(
behaviorCount,
static (count, state) => CreateRequestPipelineExecutor(count, state.PipelineInvoker),
new RequestPipelineExecutorFactoryState<TResponse>(_pipelineInvoker));
}
/// <summary>
/// 仅供测试读取指定行为数量是否已存在缓存 executor。
/// </summary>
public object? GetPipelineExecutorForTesting(int behaviorCount)
{
_pipelineExecutors.TryGetValue(behaviorCount, out var executor);
return executor;
}
}
/// <summary>
/// 为指定请求/响应类型与固定行为数量创建 pipeline executor。
/// 行为数量用于表达缓存形状,实际分发仍会消费本次容器解析出的 handler 与 behaviors 实例。
/// </summary>
private static RequestPipelineExecutor<TResponse> CreateRequestPipelineExecutor<TResponse>(
int behaviorCount,
RequestPipelineInvoker<TResponse> invoker)
{
ArgumentOutOfRangeException.ThrowIfNegative(behaviorCount);
return new RequestPipelineExecutor<TResponse>(behaviorCount, invoker);
}
/// <summary>
/// 为指定请求/响应类型创建可跨多个 behaviorCount 复用的 typed pipeline invoker。
/// </summary>
private static RequestPipelineInvoker<TResponse> CreateRequestPipelineInvoker<TResponse>(Type requestType)
{
var method = RequestPipelineInvokerMethodDefinition
.MakeGenericMethod(requestType, typeof(TResponse));
return (RequestPipelineInvoker<TResponse>)Delegate.CreateDelegate(
typeof(RequestPipelineInvoker<TResponse>),
method);
}
/// <summary>
/// 保存固定行为数量下的 typed pipeline executor 形状。
/// 该对象自身可跨分发复用,但每次调用都只绑定当前 handler 与 behavior 实例。
/// </summary>
/// <typeparam name="TResponse">请求响应类型。</typeparam>
private sealed class RequestPipelineExecutor<TResponse>(
int behaviorCount,
RequestPipelineInvoker<TResponse> invoker)
{
/// <summary>
/// 获取此 executor 预期处理的行为数量。
/// </summary>
public int BehaviorCount { get; } = behaviorCount;
/// <summary>
/// 使用当前 handler / behaviors / request 执行缓存的 pipeline 形状。
/// </summary>
public ValueTask<TResponse> Invoke(
object handler,
IReadOnlyList<object> behaviors,
object request,
CancellationToken cancellationToken)
{
if (behaviors.Count != BehaviorCount)
{
throw new InvalidOperationException(
$"Cached request pipeline executor expected {BehaviorCount} behaviors, but received {behaviors.Count}.");
}
return invoker(handler, behaviors, request, cancellationToken);
}
}
/// <summary>
/// 为 pipeline executor 缓存携带当前请求类型,避免按行为数量建缓存时创建闭包。
/// </summary>
/// <typeparam name="TResponse">请求响应类型。</typeparam>
private readonly record struct RequestPipelineExecutorFactoryState<TResponse>(
RequestPipelineInvoker<TResponse> PipelineInvoker);
/// <summary>
/// 保存单次 request pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。
/// 该对象只存在于本次分发,不会跨请求保留容器解析出的实例。
/// </summary>
private sealed class RequestPipelineInvocation<TRequest, TResponse>(
IRequestHandler<TRequest, TResponse> handler,
IReadOnlyList<object> behaviors)
where TRequest : IRequest<TResponse>
{
private readonly IRequestHandler<TRequest, TResponse> _handler = handler;
private readonly IReadOnlyList<object> _behaviors = behaviors;
private readonly MessageHandlerDelegate<TRequest, TResponse>?[] _continuations =
new MessageHandlerDelegate<TRequest, TResponse>?[behaviors.Count + 1];
/// <summary>
/// 从 pipeline 起点执行当前请求。
/// </summary>
public ValueTask<TResponse> InvokeAsync(TRequest request, CancellationToken cancellationToken)
{
return GetContinuation(0)(request, cancellationToken);
}
/// <summary>
/// 获取指定阶段的 continuation并在首次请求时为该阶段绑定一次不可变调用入口。
/// 同一行为多次调用 <c>next</c> 时会命中相同 continuation保持与传统链式委托一致的语义。
/// 线程模型上,该缓存仅假定单次分发链按顺序推进;若某个 behavior 并发调用多个 <c>next</c>
/// 这里可能重复创建等价 continuation但不会跨分发共享也不会缓存容器解析出的实例。
/// </summary>
private MessageHandlerDelegate<TRequest, TResponse> GetContinuation(int index)
{
var continuation = _continuations[index];
if (continuation is not null)
{
return continuation;
}
continuation = index == _behaviors.Count
? InvokeHandlerAsync
: new RequestPipelineContinuation<TRequest, TResponse>(this, index).InvokeAsync;
_continuations[index] = continuation;
return continuation;
}
/// <summary>
/// 执行指定索引的 pipeline behavior。
/// </summary>
private ValueTask<TResponse> InvokeBehaviorAsync(
int index,
TRequest request,
CancellationToken cancellationToken)
{
var behavior = (IPipelineBehavior<TRequest, TResponse>)_behaviors[index];
return behavior.Handle(request, GetContinuation(index + 1), cancellationToken);
}
/// <summary>
/// 调用最终请求处理器。
/// </summary>
private ValueTask<TResponse> InvokeHandlerAsync(TRequest request, CancellationToken cancellationToken)
{
return _handler.Handle(request, cancellationToken);
}
/// <summary>
/// 将固定阶段索引绑定为标准 <see cref="MessageHandlerDelegate{TRequest,TResponse}" />。
/// 该包装只在单次分发生命周期内存在,用于把缓存 shape 套入当前实例。
/// </summary>
private sealed class RequestPipelineContinuation<TCurrentRequest, TCurrentResponse>(
RequestPipelineInvocation<TCurrentRequest, TCurrentResponse> invocation,
int index)
where TCurrentRequest : IRequest<TCurrentResponse>
{
/// <summary>
/// 执行当前阶段并跳转到下一个 continuation。
/// </summary>
public ValueTask<TCurrentResponse> InvokeAsync(
TCurrentRequest request,
CancellationToken cancellationToken)
{
return invocation.InvokeBehaviorAsync(index, request, cancellationToken);
}
}
}
}

View File

@ -83,7 +83,8 @@ internal static class CqrsHandlerRegistrar
{
var assemblyMetadata = AssemblyMetadataCache.GetOrAdd(
assembly,
key => AnalyzeAssemblyRegistrationMetadata(key, logger));
logger,
static (key, state) => AnalyzeAssemblyRegistrationMetadata(key, state));
var registryTypes = assemblyMetadata.RegistryTypes;
if (registryTypes.Count == 0)
@ -442,7 +443,8 @@ internal static class CqrsHandlerRegistrar
{
return LoadableTypesCache.GetOrAdd(
assembly,
key => LoadAndSortTypes(key, logger));
logger,
static (key, state) => LoadAndSortTypes(key, state));
}
/// <summary>

View File

@ -20,7 +20,7 @@
- `GeWuYou.GFramework.Cqrs`
- 默认 runtime 与业务侧常用基类。
- `GeWuYou.GFramework.Cqrs.SourceGenerators`
- 可选。为消费端程序集生成 `ICqrsHandlerRegistry`,运行时优先走生成注册表;缺失或不适用时,回退到反射扫描
- 可选。为消费端程序集生成 `ICqrsHandlerRegistry`,运行时优先走生成注册表;只有缺失、不适用,或 fallback 仍需补齐剩余 handler 时,才继续进入反射路径
- `GFramework.Core`
- 架构上下文中实际调用 `ICqrsRuntime`,并在模块初始化时注册 CQRS 基础设施。
@ -137,7 +137,10 @@ var playerId = await this.SendAsync(new CreatePlayerCommand(new CreatePlayerInpu
- 同一程序集按稳定键去重,避免重复注册。
- 优先尝试消费端程序集上的 `ICqrsHandlerRegistry` 生成注册器。
- 生成注册器不可用,或声明了 `CqrsReflectionFallbackAttribute` 时,回退到反射扫描。
- 生成注册器不可用或元数据损坏时,记录告警并回退到反射扫描。
- 当程序集声明了 `CqrsReflectionFallbackAttribute` 时,运行时会先执行生成注册器,再只补它未覆盖的 handler。
- `CqrsReflectionFallbackAttribute` 现在可以多次声明,并同时承载 `Type[]``string[]` 两类 fallback 清单。
- 运行时会优先复用 fallback 特性里直接提供的 `Type` 条目,只对字符串条目执行定向 `Assembly.GetType(...)` 查找;只有旧版空 marker 才会退回整程序集扫描。
- 处理器以 transient 方式注册,避免上下文感知处理器在并发请求间共享可变上下文。
如果你走标准 `GFramework.Core` 架构初始化路径,这些步骤通常由框架自动完成;裸容器或测试环境则需要显式补齐 runtime 与注册入口。

View File

@ -314,6 +314,128 @@ public class CqrsHandlerRegistryGeneratorTests
""";
private const string HiddenMultiDimensionalArrayResponseSource = """
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 sealed record HiddenResponse();
private sealed record HiddenRequest() : IRequest<HiddenResponse[,]>;
private sealed class HiddenHandler : IRequestHandler<HiddenRequest, HiddenResponse[,]> { }
}
}
""";
private const string HiddenJaggedArrayResponseSource = """
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 sealed record HiddenResponse();
private sealed record HiddenRequest() : IRequest<HiddenResponse[][]>;
private sealed class HiddenHandler : IRequestHandler<HiddenRequest, HiddenResponse[][]> { }
}
}
""";
private const string HiddenGenericEnvelopeResponseSource = """
using System;
@ -963,6 +1085,44 @@ public class CqrsHandlerRegistryGeneratorTests
}
""";
private const string ExternalProtectedMultiDimensionalTypeDependencySource = """
using GFramework.Cqrs.Abstractions.Cqrs;
namespace Dep;
public abstract class VisibilityScope
{
protected internal sealed record ProtectedResponse();
protected internal sealed record ProtectedRequest() : IRequest<ProtectedResponse[,]>;
}
public abstract class HandlerBase :
IRequestHandler<VisibilityScope.ProtectedRequest, VisibilityScope.ProtectedResponse[,]>
{
}
""";
private const string ExternalProtectedGenericDefinitionDependencySource = """
using GFramework.Cqrs.Abstractions.Cqrs;
namespace Dep;
public abstract class VisibilityScope
{
protected internal sealed class ProtectedEnvelope<T>
{
}
protected internal sealed record ProtectedRequest() : IRequest<ProtectedEnvelope<string>>;
}
public abstract class HandlerBase :
IRequestHandler<VisibilityScope.ProtectedRequest, VisibilityScope.ProtectedEnvelope<string>>
{
}
""";
private const string LegacyFallbackMarkerHiddenHandlerSource = """
using System;
@ -1590,6 +1750,50 @@ public class CqrsHandlerRegistryGeneratorTests
("CqrsHandlerRegistry.g.cs", HiddenGenericEnvelopeResponseExpected));
}
/// <summary>
/// 验证精确重建路径会保留隐藏元素类型的多维数组秩信息,
/// 使生成注册器继续走定向运行时类型重建,而不是退回宽松接口发现。
/// </summary>
[Test]
public void Generates_Precise_Service_Type_For_Hidden_MultiDimensional_Array_Type_Arguments()
{
var generatedSource = RunGenerator(HiddenMultiDimensionalArrayResponseSource);
Assert.Multiple(() =>
{
Assert.That(
generatedSource,
Does.Contain("registryAssembly.GetType(\"TestApp.Container+HiddenResponse\", throwOnError: false, ignoreCase: false);"));
Assert.That(
generatedSource,
Does.Contain("typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType("));
Assert.That(generatedSource, Does.Contain(".MakeArrayType(2)"));
Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute("));
});
}
/// <summary>
/// 验证精确重建路径会递归覆盖交错数组,
/// 确保隐藏元素类型的每一层数组都继续通过数组发射分支稳定重建。
/// </summary>
[Test]
public void Generates_Precise_Service_Type_For_Hidden_Jagged_Array_Type_Arguments()
{
var generatedSource = RunGenerator(HiddenJaggedArrayResponseSource);
Assert.Multiple(() =>
{
Assert.That(
generatedSource,
Does.Contain("registryAssembly.GetType(\"TestApp.Container+HiddenResponse\", throwOnError: false, ignoreCase: false);"));
Assert.That(
generatedSource,
Does.Contain("typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType("));
Assert.That(generatedSource, Does.Contain(".MakeArrayType().MakeArrayType()"));
Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute("));
});
}
/// <summary>
/// 验证当 handler 合同把 pointer 响应类型放进 CQRS 泛型参数时,
/// 生成器会保守回退而不是继续发射不可构造的精确注册代码。
@ -1682,6 +1886,76 @@ public class CqrsHandlerRegistryGeneratorTests
Is.EqualTo(ExternalAssemblyPreciseLookupExpected));
}
/// <summary>
/// 验证当外部程序集隐藏元素类型以多维数组形式参与 CQRS 合同时,
/// 生成器仍会保留外部程序集定向查找与数组秩信息,而不是退回 fallback 元数据。
/// </summary>
[Test]
public void Generates_Precise_Assembly_Type_Lookups_For_Inaccessible_External_MultiDimensional_Array_Elements()
{
var contractsReference = MetadataReferenceTestBuilder.CreateFromSource(
"Contracts",
ExternalProtectedTypeContractsSource);
var dependencyReference = MetadataReferenceTestBuilder.CreateFromSource(
"Dependency",
ExternalProtectedMultiDimensionalTypeDependencySource,
contractsReference);
var generatedSource = RunGenerator(
ExternalProtectedTypeLookupSource,
contractsReference,
dependencyReference);
Assert.Multiple(() =>
{
Assert.That(
generatedSource,
Does.Contain(
"ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedResponse\")"));
Assert.That(
generatedSource,
Does.Contain("typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType("));
Assert.That(generatedSource, Does.Contain(".MakeArrayType(2)"));
Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute("));
});
}
/// <summary>
/// 验证当外部程序集隐藏泛型定义以“隐藏定义 + 可见类型实参”的形式参与 CQRS 合同时,
/// 生成器会继续输出定向程序集查找与运行时泛型重建,而不是退回字符串 fallback 元数据。
/// </summary>
[Test]
public void Generates_Precise_Assembly_Type_Lookups_For_Inaccessible_External_Generic_Definitions_With_Visible_Type_Arguments()
{
var contractsReference = MetadataReferenceTestBuilder.CreateFromSource(
"Contracts",
ExternalProtectedTypeContractsSource);
var dependencyReference = MetadataReferenceTestBuilder.CreateFromSource(
"Dependency",
ExternalProtectedGenericDefinitionDependencySource,
contractsReference);
var generatedSource = RunGenerator(
ExternalProtectedTypeLookupSource,
contractsReference,
dependencyReference);
Assert.Multiple(() =>
{
Assert.That(
generatedSource,
Does.Contain(
"ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedRequest\")"));
Assert.That(
generatedSource,
Does.Contain(
"ResolveReferencedAssemblyType(\"Dependency, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\", \"Dep.VisibilityScope+ProtectedEnvelope`1\")"));
Assert.That(
generatedSource,
Does.Contain("typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<,>).MakeGenericType("));
Assert.That(generatedSource, Does.Contain(".MakeGenericType(typeof(string))"));
Assert.That(generatedSource, Does.Not.Contain("CqrsReflectionFallbackAttribute("));
});
}
/// <summary>
/// 验证即使 runtime 仍暴露旧版无参 fallback marker生成器也会优先在生成注册器内部处理隐藏 handler
/// 不再输出 fallback marker。

View File

@ -0,0 +1,91 @@
# CQRS 重写迁移验证归档(至 RP-062
## 说明
- 本文件归档原 active tracking 中累积的历史验证命令与阶段性验证结论。
- `boot` 默认恢复入口应回到 `ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md`,不要再从这里挑选旧命令作为当前下一步。
## 原验证记录
- `RP-043` 之前的详细阶段记录、定向验证命令和阶段性决策均已移入主题内归档
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"`
- 结果:通过
- 备注:`5/5` 测试通过;本轮新增 cached executor 上下文刷新回归,确认 executor 复用时仍按当次分发重新注入上下文
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsReflectionFallbackAttributeTests"`
- 结果:通过
- 备注:`5/5` 测试通过;本轮锁定 fallback attribute 的公开归一化合同与空参数防御语义
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"`
- 结果:通过
- 备注:`7/7` 测试通过;本轮新增 cached notification / stream binding 上下文刷新回归,确认 binding 复用时仍按当次分发重新注入上下文
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherContextValidationTests"`
- 结果:通过
- 备注:`3/3` 测试通过;本轮锁定默认 dispatcher 对非 `IArchitectureContext` 上下文的 request / notification / stream 失败语义,且未引入新增 warning
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarFallbackFailureTests"`
- 结果:通过
- 备注:`3/3` 测试通过;本轮锁定 registrar 在 fallback 元数据失效时的 warning 语义,且保持 generated registry 主路径不回退
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false`
- 结果:通过
- 备注:`63/63` 测试通过;当前沙箱限制了 MSBuild named pipe验证需在提权环境下运行
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- 结果:通过
- 备注:`14/14` 测试通过;本轮覆盖 pointer / function pointer 合同拒绝、fallback 诊断与现有精确注册路径
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Reports_Compilation_Error_And_Skips_Precise_Registration_For_Hidden_Pointer_Response|FullyQualifiedName~Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute|FullyQualifiedName~Emits_Assembly_Level_Fallback_Metadata_When_Fallback_Is_Required_And_Runtime_Contract_Is_Available"`
- 结果:通过
- 备注:`3/3` 测试通过;本轮直接覆盖 PR #261 指向的 3 个 pointer / function pointer 回归场景
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
- 结果:通过
- 备注:`11/11` 测试通过;本轮覆盖 registrar 的 supported handler interface 缓存与 duplicate mapping 去重路径
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- 结果:通过
- 备注:`17/17` 测试通过;本轮覆盖字符串 fallback 合同兼容路径与直接 `Type` fallback 元数据优先级
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
- 结果:通过
- 备注:`13/13` 测试通过;本轮覆盖 mixed fallback metadata 的 registrar 消费路径
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- 结果:通过
- 备注:`18/18` 测试通过;本轮覆盖 mixed fallback metadata 的双特性发射路径
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-pr-review.json`
- 结果:通过
- 备注:确认当前分支对应 `PR #302`latest head review 仍有 `3` 条 open AI threads其中 MegaLinter 仅报告 `dotnet-format` restore failure 噪音
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- 结果:通过
- 备注:`18/18` 测试通过;本轮直接覆盖 fallback preamble 排版与特性个数断言收紧
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
- 结果:通过
- 备注:`13/13` 测试通过;本轮确认 mixed fallback metadata 的 registrar 消费路径未回归
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- 结果:通过
- 备注:`21/21` 测试通过;本轮新增多维数组、交错数组与外部程序集隐藏元素类型的 precise runtime type lookup 回归
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- 结果:通过
- 备注:`22/22` 测试通过;本轮新增“外部程序集隐藏泛型定义 + 可见类型实参”的 precise registration 回归,确认仍走定向运行时类型重建
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsDispatcherCacheTests"`
- 结果:通过
- 备注:`4/4` 测试通过;本轮覆盖 request pipeline executor 的首次创建、复用与双行为顺序回归
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`;本轮确认 dispatcher request pipeline 形状缓存未破坏 `net8.0` / `net9.0` / `net10.0` 目标构建
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~RegisterHandlers_Should_Cache_Assembly_Metadata_Across_Containers|FullyQualifiedName~RegisterHandlers_Should_Cache_Loadable_Types_Across_Containers|FullyQualifiedName~Dispatcher_Should_Cache_Request_Pipeline_Executors_Per_Behavior_Count"`
- 结果:通过
- 备注:`3/3` 测试通过;本轮确认无捕获缓存工厂没有破坏 registrar / dispatcher 现有缓存行为
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- 结果:通过
- 备注:`22/22` 测试通过;本轮新增外部程序集隐藏泛型定义的 precise registration 回归
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`;本轮确认删除 pointer runtime-reconstruction 残留后生成器项目仍可正常构建
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- 结果:通过
- 备注:`22/22` 测试通过;本轮确认 pointer / function pointer 拒绝语义保持不变,且未回归既有 precise runtime type lookup 场景

View File

@ -0,0 +1,35 @@
# CQRS 重写迁移追踪归档RP-046 至 RP-061
## 说明
- 本文件承接从 active trace 中迁出的已完成阶段细节。
- `boot` 默认恢复入口应回到 `ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md`,不要从本归档直接挑选旧阶段作为当前恢复点。
## 覆盖范围
- `CQRS-REWRITE-RP-046``CQRS-REWRITE-RP-061`
- 对应 active trace 清理前的 `2026-04-20``2026-04-29` 历史阶段记录
## 归档摘要
- `RP-046`generated registry 激活反射收敛,补齐私有无参构造兼容回归
- `RP-047`pointer precise runtime type 方案探索,后续已被 `RP-050` 明确覆盖并废弃
- `RP-048`registrar handler-interface 反射缓存
- `RP-049`registrar duplicate mapping 索引收敛
- `RP-050`pointer / function pointer 泛型合同拒绝
- `RP-051`direct fallback 元数据优先级收敛
- `RP-052`mixed fallback 元数据拆分
- `RP-053`precise runtime type lookup 数组回归补强
- `RP-054`:低风险并行批次收口
- `RP-055`:缓存工厂闭包收敛
- `RP-056`pointer runtime-reconstruction 残留清理
- `RP-057`cached executor 上下文刷新回归
- `RP-058`delegated fallback attribute 合同测试
- `RP-059`notification / stream binding 上下文刷新回归
- `RP-060`dispatcher 上下文前置条件失败语义回归
- `RP-061`registrar fallback 失败分支回归
## 备注
- 若后续需要恢复这些阶段的详细上下文,应以对应提交、测试文件与本主题源码为准。
- 当前 active trace 已不再保留这些阶段的逐段叙述,以保证 `boot` 能直接落到 `RP-062`

View File

@ -7,7 +7,7 @@ CQRS 迁移与收敛。
## 当前恢复点
- 恢复点编号:`CQRS-REWRITE-RP-052`
- 恢复点编号:`CQRS-REWRITE-RP-062`
- 当前阶段:`Phase 8`
- 当前焦点:
- 当前功能历史已归档active 跟踪仅保留 `Phase 8` 主线的恢复入口
@ -15,12 +15,24 @@ CQRS 迁移与收敛。
- `CqrsReflectionFallbackAttribute` 现允许多实例,以承载 `Type[]` 与字符串 fallback 元数据的组合输出
- 已将 generator 的程序集级 fallback 元数据进一步收敛:当全部 fallback handlers 都可直接引用且 runtime 暴露 `params Type[]` 合同时,生成器现优先发射 `typeof(...)` 形式的 fallback 元数据
- 当 runtime 不支持多实例 fallback 特性或缺少对应构造函数时mixed fallback 场景仍会整体保守回退到字符串元数据,避免仅部分 handler 走 `Type[]` 时漏掉剩余需按名称恢复的 handlers
- 已完成 request pipeline executor 形状缓存:`CqrsDispatcher` 现会在单个 request binding 内按 `behaviorCount` 复用强类型 pipeline executor而不是每次 `SendAsync` 都重建整条 `next` 委托链
- 已补充 dispatcher pipeline executor 缓存与双行为顺序回归,锁定缓存复用后仍保持现有行为执行顺序
- 已补充 cached request pipeline executor 的上下文刷新回归,锁定 executor 复用时仍会为当次 handler / singleton behavior 重新注入当前 `ArchitectureContext`
- 已补充 cached notification / stream dispatch binding 的上下文刷新回归,锁定 binding 复用时仍会为当次 handler 重新注入当前 `ArchitectureContext`
- 已补充非 `IArchitectureContext` 的 dispatcher 失败语义回归,锁定 context-aware request / notification / stream handler 在注入前置条件不满足时会显式抛出异常
- 已补充 registrar fallback 失败分支回归,锁定 named fallback 无法解析、named fallback 解析抛异常、direct fallback 跨程序集三类 warning 语义
- 已完成 generated registry 激活路径收敛:`CqrsHandlerRegistrar` 现优先复用缓存工厂委托,避免重复 `ConstructorInfo.Invoke`
- 已补充私有无参构造 generated registry 的回归测试,确保兼容现有生成器产物
- 已修正 pointer / function pointer 泛型合同的错误覆盖:生成器不再为这两类类型发射 precise runtime type 重建代码
- 已补充非法 CQRS 泛型合同的输入诊断断言,明确 `CS0306` 与 fallback / diagnostic 路径的组合语义
- 已为 registrar 的 reflection 注册路径补充 handler-interface 元数据缓存,减少跨容器重复注册时的 `GetInterfaces()` 反射
- 已将 registrar 的重复映射判定从线性扫描 `IServiceCollection` 收敛为本地映射索引,减少 fallback 注册路径的重复查找
- 已完成一轮 `static lambda + state` 微收敛:`CqrsDispatcher``CqrsHandlerRegistrar` 现会在弱缓存 / 并发缓存入口优先使用无捕获工厂,继续压低热路径上的额外闭包分配
- 已补充 `CqrsReflectionFallbackAttribute` 叶子级合同测试,锁定空 marker、字符串 fallback 名称归一化、直接 `Type` fallback 归一化与空参数防御语义
- 已完成 `PR #304` review follow-up 收敛:`CqrsDispatcher` 现补齐 pipeline executor / continuation 缓存的线程模型文档,并把 request pipeline invoker 从按 `behaviorCount` 重复创建收敛为 binding 内复用
- 已补齐 `CqrsDispatcherContextValidationTests` 三个上下文校验 handler 的 XML `param` / `returns` 注释,以及 `DispatcherNotificationContextRefreshNotification``DispatcherStreamContextRefreshRequest``DispatchId` XML 参数注释,收敛上一轮 PR review 遗留的文档类 minor feedback
- 已收紧 CQRS / generator 回归测试的脆弱断言日志断言改为语义匹配precise runtime type lookup 回归改为锁定数组秩、外部类型查找与“未发射 fallback metadata”这些稳定语义
- 已为 dispatcher cache / context refresh / pipeline order 三组测试状态容器补齐并发保护,并将 `CqrsDispatcherCacheTests` 标记为 `NonParallelizable`,避免静态缓存与共享快照在并行测试中相互污染
- 中期上继续 `Phase 8` 主线:参考 `ai-libs/Mediator`,继续扩大 generator 覆盖,并选择下一个收益明确的 dispatch / invoker 反射收敛点
## 当前状态摘要
@ -71,6 +83,52 @@ CQRS 迁移与收敛。
- latest reviewed commit 当前剩余 `3` 条 open AI review threads`2` 条 Greptile、`1` 条 CodeRabbit
- 本地核对后确认 `dotnet-format` 仍只有 `Restore operation failed` 噪音,没有附带当前仍成立的文件级格式诊断
- 已按 review triage 修正 generator source preamble 的多实例 fallback 特性排版、移除死参数,并补强 mixed/direct fallback 发射回归断言与 XML 文档
- `2026-04-30` 已重新执行 `$gframework-pr-review`
- 当前分支对应 `PR #304`,状态为 `OPEN`
- latest reviewed commit 当前剩余 `7` 条 CodeRabbit nitpick 与 `2` 条 Greptile open threads集中在测试脆弱断言、共享测试状态并发保护以及 `CqrsDispatcher` 的缓存线程模型文档
- 本地核对后已确认这些评论仍对应当前代码MegaLinter 继续只暴露 `dotnet-format``Restore operation failed` 环境噪音CTRF 汇总为 `2203/2203` passed
- 已在本地完成 follow-uprequest pipeline invoker 改为 binding 级复用、共享测试状态切换到 `System.Threading.Lock` 保护、顺序测试改为受控记录接口、`CqrsDispatcherCacheTests` 标记为 `NonParallelizable`,并补齐相关 XML / 线程模型注释
- `2026-04-29` 已完成一轮 precise runtime type lookup 的数组回归补强:
- `GFramework.SourceGenerators.Tests` 已新增多维数组、交错数组、外部程序集隐藏元素类型三类回归
- 当前生成器在 precise runtime type lookup 下已稳定保留数组秩信息,并递归发射交错数组的 `MakeArrayType()`
- 本轮定向测试未暴露数组发射缺陷,因此未改动 fallback 合同选择逻辑,也未调整 direct / named / mixed fallback 排版路径
- `2026-04-29` 已补齐一轮外部程序集隐藏泛型定义回归覆盖:
- `GFramework.SourceGenerators.Tests` 已新增“外部程序集隐藏泛型定义 + 可见类型实参”的 precise registration 回归
- 当前生成器会继续为这类 handler 合同发射 `ResolveReferencedAssemblyType(...) + MakeGenericType(...)` 组合,而不是退回字符串 fallback 元数据
- 本轮定向测试未暴露新的实现缺口,因此未改动 direct / named / mixed fallback 选择逻辑,也未调整 generator runtime type 建模实现
- `2026-04-29` 已完成一轮缓存工厂闭包收敛:
- `CqrsDispatcher` 现会在 notification / stream / request binding 与 pipeline executor 缓存入口优先使用无捕获工厂
- `CqrsHandlerRegistrar` 现会在程序集元数据缓存与可加载类型缓存入口复用 `static` 工厂 + 显式状态参数
- 本轮未改动公开语义,也未修改 fallback 合同与 handler / behavior 生命周期边界
- `2026-04-29` 已完成一轮 request pipeline executor 形状缓存:
- `CqrsDispatcher` 现会继续按 `requestType + responseType` 缓存 request dispatch binding并在 binding 内按 `behaviorCount` 缓存强类型 pipeline executor
- 每次分发只绑定当前 handler / behaviors 实例,不缓存容器解析结果,因此不改变 transient 生命周期与上下文注入语义
- `GFramework.Cqrs.Tests` 已补充 executor 首次创建 / 后续复用与双行为顺序回归
- `2026-04-29` 已完成一轮 cached executor 上下文刷新回归补强:
- `GFramework.Cqrs.Tests` 已新增 `DispatcherPipelineContextRefresh*` 测试替身,分别记录 request handler 与 pipeline behavior 在每次分发中实际观察到的实例身份与 `ArchitectureContext`
- `CqrsDispatcherCacheTests` 现明确断言:同一个 cached request pipeline executor 在重复分发时会继续命中同一 executor 形状,但不会跨分发保留旧上下文
- 本轮定向测试未暴露新的 runtime 缺口,因此没有改动 `GFramework.Cqrs/Internal/CqrsDispatcher.cs`
- `2026-04-29` 已完成一轮 cached notification / stream binding 上下文刷新回归补强:
- `GFramework.Cqrs.Tests` 已新增 `DispatcherNotificationContextRefresh*``DispatcherStreamContextRefresh*` 测试替身,分别记录 notification handler 与 stream handler 在重复分发时观察到的实例身份与 `ArchitectureContext`
- `CqrsDispatcherCacheTests` 现明确断言:同一个 cached notification / stream dispatch binding 在重复分发时会继续命中同一 binding但不会跨分发保留旧上下文
- 本轮定向测试未暴露新的 runtime 缺口,因此没有改动 `GFramework.Cqrs/Internal/CqrsDispatcher.cs`
- `2026-04-29` 已完成一轮 dispatcher 上下文前置条件失败语义回归:
- `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` 已通过公开工厂 `CqrsRuntimeFactory.CreateRuntime(...)` 锁定默认 dispatcher 的失败语义
- 当 context-aware request / notification / stream handler 遇到仅实现 `ICqrsContext`、但未实现 `IArchitectureContext` 的上下文时dispatcher 会在调用前显式抛出 `InvalidOperationException`
- 本轮只补测试,不改 runtime 实现与文档口径
- `2026-04-29` 已接受一轮 delegated registrar fallback 失败分支测试:
- `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs` 已覆盖 named fallback 无法解析、named fallback 解析抛异常、direct fallback 跨程序集三类 warning 语义
- 主线程已复核该新文件并重新执行定向测试,确认当前 registrar 在 fallback 元数据失效时仍保持“跳过条目 + 记录告警”的既有语义
- `2026-04-29` 已接受一轮 delegated 叶子级 fallback 合同测试:
- `GFramework.Cqrs.Tests/Cqrs/CqrsReflectionFallbackAttributeTests.cs` 已锁定空 marker、字符串 fallback 名称去空/去重/排序、直接 `Type` fallback 去空/去重/排序与空参数数组防御语义
- 当前 runtime 读取程序集级 fallback 元数据时所依赖的 attribute 归一化合同,现已有独立叶子级测试文件覆盖
- `2026-04-29` 已完成一轮 CQRS 入口文档对齐:
- `GFramework.Cqrs/README.md``docs/zh-CN/core/cqrs.md``docs/zh-CN/api-reference/index.md` 现已明确 generated registry 优先、targeted fallback 补齐剩余 handler 的当前语义
- `2026-04-29` 已完成一轮 generator pointer runtime-reconstruction 残留清理:
- `CqrsHandlerRegistryGenerator` 的运行时类型引用模型已移除不可达的 pointer 子结构
- `SourceEmission` 不再保留 `MakePointerType()` 源码发射分支,`RuntimeTypeReferences` 也已删掉对应的外部程序集递归扫描死代码
- pointer / function pointer 的拒绝语义保持不变direct / named / mixed fallback 逻辑未改动
- 当前工作区相对 `origin/main` 的累计 diff 已达到 `14 files`,仍低于本轮 `gframework-batch-boot 50` 的主要 stop condition
- 当前主线优先级:
- generator 覆盖面继续扩大
- dispatch/invoker 反射占比继续下降
@ -80,62 +138,28 @@ CQRS 迁移与收敛。
- 当前 `dotnet build GFramework.sln -c Release` 在 WSL 环境仍会受顶层 `GFramework.csproj` 的 Windows NuGet fallback 配置影响
- 当前 `GFramework.Cqrs.Tests` 仍直接引用 `GFramework.Core`,说明测试已按模块意图拆分,但 runtime 物理迁移尚未完全切断依赖
- `RegisterMediatorBehavior``MediatorCoroutineExtensions``ContextAwareMediator*Extensions` 仍作为兼容层存在,未来真正移除时仍需单独规划弃用窗口
## 活跃文档
- 历史跟踪归档:[cqrs-rewrite-history-through-rp043.md](../archive/todos/cqrs-rewrite-history-through-rp043.md)
- 验证历史归档:[cqrs-rewrite-validation-history-through-rp062.md](../archive/todos/cqrs-rewrite-validation-history-through-rp062.md)
- 历史 trace 归档:[cqrs-rewrite-history-through-rp043.md](../archive/traces/cqrs-rewrite-history-through-rp043.md)
- `RP-046``RP-061` trace 归档:[cqrs-rewrite-history-rp046-through-rp061.md](../archive/traces/cqrs-rewrite-history-rp046-through-rp061.md)
## 验证说明
- `RP-043` 之前的详细阶段记录、定向验证命令和阶段性决策均已移入主题内归档
- active 跟踪文件只保留当前恢复点、当前活跃事实、风险和下一步,避免 `boot` 在默认入口中重复扫描 1000+ 行历史 trace
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false`
- `RP-046``RP-062` 的历史验证命令与阶段性结果已移入验证归档active tracking 只保留当前恢复入口需要的最新验证
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json`
- 结果:通过
- 备注:`63/63` 测试通过;当前沙箱限制了 MSBuild named pipe验证需在提权环境下运行
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- 备注:确认当前分支对应 `PR #304`,并定位到仍需本地复核的 CodeRabbit / Greptile open thread
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
- 结果:通过
- 备注:`14/14` 测试通过;本轮覆盖 pointer / function pointer 合同拒绝、fallback 诊断与现有精确注册路径
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Reports_Compilation_Error_And_Skips_Precise_Registration_For_Hidden_Pointer_Response|FullyQualifiedName~Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute|FullyQualifiedName~Emits_Assembly_Level_Fallback_Metadata_When_Fallback_Is_Required_And_Runtime_Contract_Is_Available"`
- 备注:`0 warning / 0 error`;本轮确认 XML 文档补齐、`NonParallelizable``_syncRoot` 命名与 `ai-plan` 收敛未引入新增编译问题
- `bash scripts/validate-csharp-naming.sh`
- 结果:通过
- 备注:`3/3` 测试通过;本轮直接覆盖 PR #261 指向的 3 个 pointer / function pointer 回归场景
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
- 结果:通过
- 备注:`11/11` 测试通过;本轮覆盖 registrar 的 supported handler interface 缓存与 duplicate mapping 去重路径
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- 结果:通过
- 备注:`17/17` 测试通过;本轮覆盖字符串 fallback 合同兼容路径与直接 `Type` fallback 元数据优先级
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
- 结果:通过
- 备注:`13/13` 测试通过;本轮覆盖 mixed fallback metadata 的 registrar 消费路径
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- 结果:通过
- 备注:`18/18` 测试通过;本轮覆盖 mixed fallback metadata 的双特性发射路径
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-pr-review.json`
- 结果:通过
- 备注:确认当前分支对应 `PR #302`latest head review 仍有 `3` 条 open AI threads其中 MegaLinter 仅报告 `dotnet-format` restore failure 噪音
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
- 结果:通过
- 备注:`0 warning / 0 error`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- 结果:通过
- 备注:`18/18` 测试通过;本轮直接覆盖 fallback preamble 排版与特性个数断言收紧
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
- 结果:通过
- 备注:`13/13` 测试通过;本轮确认 mixed fallback metadata 的 registrar 消费路径未回归
- 备注:使用显式 `GIT_DIR` / `GIT_WORK_TREE` 绑定重跑后,`1045` 个 tracked C# 文件的命名校验全部通过;本轮 `_syncRoot` 改名未引入命名规则回归
## 下一步
1. 继续 `Phase 8` 主线,优先再找一个收益明确的 generator 覆盖缺口,继续减少仍必须依赖字符串 fallback 元数据的 handler 类型形态
2. 若继续文档主线,优先再扫 `docs/zh-CN/api-reference` 与教程入口页,补齐仍过时的 CQRS API / 命名空间表述
3. 若后续再出现新的 PR review 或 review thread 变化,再重新执行 `$gframework-pr-review` 作为独立验证步骤
1. push 当前 follow-up 提交后,重新执行 `$gframework-pr-review`,确认 `PR #304` 的 latest unresolved threads 是否已刷新为已解决,或仅剩新增有效项

View File

@ -1,154 +1,37 @@
# CQRS 重写迁移追踪
## 2026-04-29
## 2026-04-30
### 阶段:mixed fallback 元数据拆分CQRS-REWRITE-RP-052
### 阶段:PR #304 剩余 review follow-up 收敛CQRS-REWRITE-RP-062
- 延续 `gframework-batch-boot 50``Phase 8` 主线,本轮把上一批的“全部可直接引用 fallback handlers 走 `Type[]`”继续推进到 mixed 场景
- 先复核现状后确认:
- `CqrsHandlerRegistrar` 已天然支持读取多个 `CqrsReflectionFallbackAttribute` 实例
- 上一批真正阻止 mixed 场景继续收敛的点,是 runtime attribute 本身尚未开放多实例,以及 generator 只能二选一发射单个 fallback 特性
- 已在 `GFramework.Cqrs/CqrsReflectionFallbackAttribute.cs` 中将特性约束改为 `AllowMultiple = true`,并补充注释说明多个实例的用途
- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中扩展 fallback 合同探测:
- 探测 runtime 是否支持 `params string[]`
- 探测 runtime 是否支持 `params Type[]`
- 探测 runtime 是否允许多个 `CqrsReflectionFallbackAttribute` 实例
- 已在 `CqrsHandlerRegistryGenerator.Models.cs``CqrsHandlerRegistryGenerator.SourceEmission.cs` 中重构 fallback 发射模型:
- fallback 元数据现在可表示为一个或多个程序集级特性实例
- 当 fallback handlers 全部可直接引用时,继续优先输出单个 `Type[]` 特性
- 当 fallback 同时包含可直接引用与仅能按名称恢复的 handlers且 runtime 支持多实例时,拆分输出一条 `Type[]` 特性和一条字符串特性
- 若 runtime 不支持多实例或缺少相应构造函数,仍整体回退到字符串元数据,避免 mixed 场景漏注册
- 已补充 runtime 与 generator 双侧回归:
- `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs` 新增 mixed fallback metadata 用例,锁定 registrar 只对字符串条目调用一次 `Assembly.GetType(...)`
- `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 新增 mixed fallback emission 用例,锁定 generator 会输出两个程序集级 fallback 特性实例
- 同步更新:
- `GFramework.Cqrs.SourceGenerators/README.md`
- `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
- 说明 mixed 场景现在会拆分 `Type` 元数据与字符串元数据
- 定向验证已通过:
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
- `13/13` passed
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- `18/18` passed
- 随后按 `$gframework-pr-review` 重新拉取当前分支 PR 审查数据:
- 当前 worktree `feat/cqrs-optimization` 已对应 `PR #302`
- latest head commit 仍有 `3` 条 open AI review threadsGreptile 指向 generator preamble 的死参数与多实例 fallback 特性空行CodeRabbit 指向 mixed/direct fallback 测试断言过宽
- MegaLinter 仍只暴露 `dotnet-format``Restore operation failed`,未给出本地仍成立的格式文件线索,因此按环境噪音处理
- 本轮已继续收口 `RP-052` 的 follow-up
- 在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.SourceEmission.cs` 中移除已不再参与判断的 `generationEnvironment` 透传参数
- 调整多实例 fallback 特性发射时的换行策略,避免最后一个 fallback 特性与 `CqrsHandlerRegistryAttribute` 之间保留多余空行
- 在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 中补强 direct/mixed fallback 发射断言,锁定特性实例个数、拒绝空 marker并确保 mixed 场景的程序集级 preamble 排版稳定
- 在 `GFramework.Cqrs.Tests/Cqrs/ReflectionFallbackNotificationContainer.cs` 中为 `DirectFallbackHandlerType` 补齐 `<returns>` XML 文档
- 本轮 review follow-up 验证已通过:
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
- `0 warning / 0 error`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- `18/18` passed
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
- `13/13` passed
- 本轮再次执行 `$gframework-pr-review`,确认当前分支 `feat/cqrs-optimization` 仍对应 `PR #304`
- 本地复核后继续收敛了上一轮遗留的 review 项:
- `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarFallbackFailureTests.cs` 已补 `NonParallelizable`
- `GFramework.Cqrs.Tests/Cqrs/DispatcherStreamContextRefreshState.cs` 已改用 `_syncRoot` 命名,并补齐缺失的 XML 文档标签
- `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` 三个内部 `Handle(...)` 已补齐 XML `param` / `returns`
- `DispatcherNotificationContextRefreshNotification``DispatcherStreamContextRefreshRequest` 已补 `DispatchId` XML 参数注释
- `cqrs-rewrite` active tracking / trace 已压缩为当前恢复入口,并将已完成阶段的详细历史移入 archive
- 验证:
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
## 2026-04-20
## 活跃事实
### 阶段direct fallback 元数据优先级收敛CQRS-REWRITE-RP-051
- 当前主题仍处于 `Phase 8`
- `PR #304` 的本地 follow-up 已再次收口一轮,后续需要在 push 后重新观察 GitHub 的 unresolved thread 刷新结果
- 已完成阶段的详细执行历史不再留在 active trace默认恢复入口只保留当前恢复点、活跃事实、风险与下一步
- 重新按 `gframework-batch-boot 50` 恢复 `Phase 8` 后,先复核当前 worktree 的恢复入口、`origin/main` 基线与分支规模:
- worktree 仍映射到 `cqrs-rewrite`
- 基线按批处理约定固定为 `origin/main`
- 本轮开始前分支累计 diff 为 `0 files / 0 lines`
- 结合当前代码热点与历史归档后,选择本轮批次目标为“继续收敛 generator fallback 元数据,进一步减少 runtime 按字符串类型名回查 handler 的场景”
- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中新增 runtime fallback 合同探测:
- 识别 `CqrsReflectionFallbackAttribute` 是否支持 `params string[]`
- 识别 `CqrsReflectionFallbackAttribute` 是否支持 `params Type[]`
- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.Models.cs`
`CqrsHandlerRegistryGenerator.SourceEmission.cs` 中收敛 fallback 发射策略:
- 当本轮所有 fallback handlers 都可被生成代码直接引用,且 runtime 支持 `params Type[]` 时,生成器现优先发射 `typeof(...)` 形式的程序集级 fallback 元数据
- 当 fallback handlers 中仍存在不能直接引用的实现类型时,生成器继续整体回退到字符串元数据,避免 mixed 场景下部分 handler 走 `Type[]`、其余 handler 丢失恢复入口
- 已在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 补充回归:
- 锁定 runtime 同时暴露字符串与 `Type` 两类 fallback 构造函数时,生成器优先选择直接 `Type` 元数据
- 保留现有字符串 fallback 合同测试,确保旧 contract 兼容路径不回退
- 同步更新:
- `GFramework.Cqrs.SourceGenerators/README.md`
- `docs/zh-CN/source-generators/cqrs-handler-registry-generator.md`
- 说明“可直接引用的 fallback handlers 会优先走 `typeof(...)` 元数据,减少运行时字符串回查”
- 定向验证已通过:
- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release`
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- `17/17` passed
- 额外修正:
- active tracking 中原先引用的 `ai-plan/migration/CQRS_MODULE_SPLIT_PLAN.md` 在当前 worktree 已不存在;本轮已移除该失效路径,后续以 active tracking / trace 作为默认恢复入口
## 当前风险
### 阶段pointer / function pointer 泛型合同拒绝CQRS-REWRITE-RP-050
- 当前 `dotnet build GFramework.sln -c Release` 在 WSL 环境仍会受顶层 `GFramework.csproj` 的 Windows NuGet fallback 配置影响
- 远端 review thread 在本地提交前不会自动刷新GitHub 上看到的 open 状态可能暂时滞后于当前代码
- 重新执行 `$gframework-pr-review` 后,确认当前分支对应 `PR #261`,状态仍为 `OPEN`
- latest reviewed commit 当前剩余 `1` 条 open CodeRabbit thread指向 `RP-047` 历史记录仍把 `MakePointerType()` precise registration 写成现行路径
- 本地核对后确认该评论有效:当前 pointer / function pointer 语义已由 `RP-050` 收敛为 fallback / diagnostic 路径,历史追踪必须显式标注 `RP-047` 已废弃,避免后续恢复时误回滚到旧方案
- 已在 `GFramework.Cqrs.SourceGenerators/Cqrs/CqrsHandlerRegistryGenerator.cs` 中收紧 `TryCreateRuntimeTypeReference``CanReferenceFromGeneratedRegistry`
- pointer / function pointer 现统一视为不可精确生成的 CQRS 泛型合同,生成器会保守回退到既有 fallback / diagnostic 路径,而不再发射运行时 `MakeGenericType(...)` 风险代码
- 已在 `GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs` 中补充输入源诊断分离,并将相关测试改为显式断言 `CS0306` 与 fallback / diagnostic 结果
- 已同步修正 `ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md``RP-047` 段落,明确其已被 `RP-050` 覆盖,且不得恢复 `MakePointerType()` precise registration
- 定向验证已通过:
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Reports_Compilation_Error_And_Skips_Precise_Registration_For_Hidden_Pointer_Response|FullyQualifiedName~Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute|FullyQualifiedName~Emits_Assembly_Level_Fallback_Metadata_When_Fallback_Is_Required_And_Runtime_Contract_Is_Available"`
- `3/3` passed
- 扩展验证已通过:
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- `14/14` passed
## Archive Context
### 阶段registrar duplicate mapping 索引收敛CQRS-REWRITE-RP-049
- 已将 `CqrsHandlerRegistrar` 的重复 handler mapping 判定从逐条线性扫描 `IServiceCollection` 收敛为单次构建的本地映射索引
- reflection fallback 或重复类型输入场景下,后续 duplicate mapping 判定改为 `HashSet` 命中,不再重复遍历已有服务描述符
- `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs` 已补充“程序集枚举返回重复 handler 类型时仍只注册一份映射”的回归
- 定向验证已通过:
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
- `11/11` passed
- 当前沙箱限制 MSBuild named pipe因此验证在提权环境下执行
### 阶段registrar handler-interface 反射缓存CQRS-REWRITE-RP-048
- 已在 `CqrsHandlerRegistrar` 中新增按 `Type` 弱键缓存的 supported handler interface 元数据reflection 注册路径现会复用已筛选且排序好的接口列表
- 同一 handler 类型跨容器重复注册时,不再重复执行 `GetInterfaces()` 与支持接口筛选;缓存仍保持卸载安全,不会长期钉住 collectible 类型
- `GFramework.Cqrs.Tests/Cqrs/CqrsHandlerRegistrarTests.cs` 已补充 registrar 静态缓存清理与 supported interface 缓存复用回归
- 定向验证已通过:
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~GFramework.Cqrs.Tests.Cqrs.CqrsHandlerRegistrarTests"`
- `10/10` passed
- 当前沙箱限制 MSBuild named pipe因此验证在提权环境下执行
### 阶段pointer precise runtime type 覆盖扩展CQRS-REWRITE-RP-047已由 RP-050 覆盖)
- 曾在 `CqrsHandlerRegistryGenerator` 中尝试补充 pointer 类型的 runtime type 递归建模与源码发射,计划通过 `MakePointerType()` 还原隐藏 pointer 响应类型
- 该方案后续已被 `RP-050` 明确废弃pointer / function pointer 不能作为 CQRS 泛型合同的 precise registration 输入,当前实现统一回到 fallback / diagnostic 路径,不能恢复到 `MakePointerType()` 精确注册
- 已同步收紧 function pointer 签名的可直接生成判定,只有当签名中的返回值与参数类型均可从 generated registry 安全引用时才走静态注册
- 已保留含隐藏类型 function pointer handler 的 fallback / 诊断回归覆盖,确保 pointer 支持扩展不会误删原有程序集级 fallback 契约边界
- 后续若需恢复当前 pointer / function pointer 行为,应以 `RP-050` 为权威记录,而不是继续沿用本阶段的旧设计假设
- 定向验证与 `CqrsHandlerRegistryGeneratorTests` 全组验证均已通过:
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~Generates_Precise_Service_Type_For_Hidden_Pointer_Response|FullyQualifiedName~Reports_Diagnostic_And_Skips_Registry_When_Fallback_Metadata_Is_Required_But_Runtime_Contract_Lacks_Fallback_Attribute|FullyQualifiedName~Emits_Assembly_Level_Fallback_Metadata_When_Fallback_Is_Required_And_Runtime_Contract_Is_Available"`
- `3/3` passed
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests"`
- `14/14` passed
- 当前沙箱限制 MSBuild named pipe因此验证在提权环境下执行
### 阶段generated registry 激活反射收敛CQRS-REWRITE-RP-046
- 已在 `CqrsHandlerRegistrar` 中将 generated registry 的无参构造激活改为类型级缓存工厂
- 默认路径优先使用一次性动态方法直接创建 registry避免后续每次命中缓存仍走 `ConstructorInfo.Invoke`
- 若运行环境不允许动态方法,则保留原有反射激活回退,确保 generated registry 路径不因运行时限制失效
- 已补充“私有无参构造 generated registry 仍可激活”的回归测试,覆盖现有生成器产物兼容性
- 定向验证已通过:
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-restore -p:RestoreFallbackFolders= -m:1 -nodeReuse:false`
- `63/63` passed
- 当前沙箱限制 MSBuild named pipe因此验证在提权环境下执行
### Archive Context
- 历史跟踪归档:
- `ai-plan/public/cqrs-rewrite/archive/todos/cqrs-rewrite-history-through-rp043.md`
- 历史 trace 归档:
- `ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-through-rp043.md`
- `ai-plan/public/cqrs-rewrite/archive/traces/cqrs-rewrite-history-rp046-through-rp061.md`
### 当前下一步
## 当前下一步
1. 回到 `Phase 8` 主线,优先再找一个 generator 覆盖缺口,继续减少仍需程序集级字符串 fallback 元数据的 handler 场景
2. 若继续文档主线,优先补齐 `docs/zh-CN/api-reference` 与教程入口页中仍过时的 CQRS API / 命名空间表述
3. 若后续 review thread 或 PR 状态再次变化,再重新执行 `$gframework-pr-review` 复核远端信号
1. push 当前 follow-up 提交后,重新执行 `$gframework-pr-review`,确认 `PR #304` 的 latest unresolved threads 是否已刷新为已解决,或仅剩新增有效项

View File

@ -29,7 +29,7 @@ description: GFramework 的 API 阅读入口,按模块映射 README、专题
| 模块族 | 先看什么 | 继续深入 | XML 文档关注点 |
| --- | --- | --- | --- |
| `Core` / `Core.Abstractions` | [Core 模块](../core/index.md) | [Core 抽象层说明](../abstractions/core-abstractions.md)、[快速开始](../getting-started/quick-start.md) | 架构入口、生命周期、命令 / 查询 / 事件 / 状态 / 资源 / 日志 / 配置 / 并发契约 |
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | [CQRS 运行时](../core/cqrs.md) | [CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)、[协程系统](../core/coroutine.md) | request / notification / handler / pipeline / registry / fallback contract |
| `Cqrs` / `Cqrs.Abstractions` / `Cqrs.SourceGenerators` | [CQRS 运行时](../core/cqrs.md) | [CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)、[协程系统](../core/coroutine.md) | request / notification / handler / pipeline / generated registry / targeted fallback contract |
| `Game` / `Game.Abstractions` / `Game.SourceGenerators` | [Game 模块总览](../game/index.md) | [Game 抽象层说明](../abstractions/game-abstractions.md)、[配置系统](../game/config-system.md) | 配置、数据、设置、场景、UI、存储、序列化契约 |
| `Godot` / `Godot.SourceGenerators` | [Godot 模块总览](../godot/index.md) | [Godot 项目生成器](../source-generators/godot-project-generator.md)、[GetNode 生成器](../source-generators/get-node-generator.md)、[BindNodeSignal 生成器](../source-generators/bind-node-signal-generator.md) | 节点扩展、场景 / UI 适配、配置 / 存储 / 设置接线、Godot 生成器入口 |
| `Ecs.Arch` / `Ecs.Arch.Abstractions` | [ECS 模块总览](../ecs/index.md) | [Arch ECS 集成](../ecs/arch.md)、[Ecs.Arch 抽象层说明](../abstractions/ecs-arch-abstractions.md) | ECS 模块契约、系统适配、配置对象和运行时装配边界 |

View File

@ -19,7 +19,7 @@ description: Cqrs 模块族的运行时、契约层、生成器入口,以及
| --- | --- | --- |
| `GeWuYou.GFramework.Cqrs.Abstractions` | 纯契约层,定义 request、notification、stream、handler、pipeline、runtime seam | 需要把消息契约放到更稳定的共享层,或只依赖接口做解耦 |
| `GeWuYou.GFramework.Cqrs` | 默认 runtime提供 dispatcher、handler 基类、上下文扩展和程序集注册流程 | 大多数直接消费 CQRS 的业务模块 |
| `GeWuYou.GFramework.Cqrs.SourceGenerators` | 编译期生成 `ICqrsHandlerRegistry`缩小运行时反射扫描范围 | handler 较多,想把注册映射前移到编译期 |
| `GeWuYou.GFramework.Cqrs.SourceGenerators` | 编译期生成 `ICqrsHandlerRegistry`让运行时先走生成注册器,再只对剩余 handler 做定向 fallback | handler 较多,想把注册映射前移到编译期 |
## 最小接入路径
@ -156,8 +156,12 @@ protected override void OnInitialize()
1. 优先读取消费端程序集上的 `CqrsHandlerRegistryAttribute`
2. 存在生成注册器时优先使用 `ICqrsHandlerRegistry`
3. 生成注册器不可用或元数据异常时记录告警并回退到反射路径
4. 如果程序集带有 `CqrsReflectionFallbackAttribute`,只补扫剩余 handler
5. 同一程序集按稳定键去重,避免重复注册
4. 当生成注册器携带 `CqrsReflectionFallbackAttribute` 元数据时,运行时会先完成生成注册器注册,再补剩余 handler
5. `CqrsReflectionFallbackAttribute` 可以同时携带 `Type[]``string[]` 两类清单;运行时会优先复用直接 `Type` 条目,只对名称条目做定向 `Assembly.GetType(...)` 查找
6. 只有旧版空 marker 或生成注册器不可用时,才会回到整程序集反射扫描
7. 同一程序集按稳定键去重,避免重复注册
换句话说,声明 fallback 特性本身不等于“整包反射扫描”。当前推荐理解是生成注册器负责能静态表达的部分fallback 只补它覆盖不到的 handler。
`Cqrs.SourceGenerators` 的专题入口见[CQRS Handler Registry 生成器](../source-generators/cqrs-handler-registry-generator.md)。
@ -205,8 +209,8 @@ RegisterCqrsPipelineBehavior<LoggingBehavior<,>>();
| `GFramework.Cqrs.Abstractions/Cqrs/` | `ICqrsRuntime``ICqrsHandlerRegistrar``IPipelineBehavior<,>``IRequestHandler<,>``Unit` | 请求、处理器和 runtime seam 的最小契约 |
| `GFramework.Cqrs/Command` `Query` `Notification` `Request` `Extensions` | `CommandBase<TInput, TResponse>``QueryBase<TInput, TResponse>``NotificationBase<TInput>``RequestBase<TInput, TResponse>``ContextAwareCqrsExtensions` | 业务侧常用基类和上下文发送入口 |
| `GFramework.Cqrs/Cqrs/` | `AbstractCommandHandler<,>``AbstractQueryHandler<,>``AbstractRequestHandler<,>``AbstractStreamCommandHandler<,>``AbstractStreamQueryHandler<,>``LoggingBehavior<,>` | 默认处理器基类、上下文注入、流式处理与行为管道 |
| `GFramework.Cqrs` 根入口与 `Internal/` | `CqrsRuntimeFactory``ICqrsHandlerRegistry``CqrsHandlerRegistryAttribute``CqrsReflectionFallbackAttribute``DefaultCqrsRegistrationService` | runtime 创建入口、registry 协议、fallback 语义和程序集去重规则 |
| `GFramework.Cqrs.SourceGenerators/Cqrs/` | `CqrsHandlerRegistryGenerator``RuntimeTypeReferenceSpec``OrderedRegistrationKind` | 生成注册器、精确 type lookup 和 fallback 诊断边界 |
| `GFramework.Cqrs` 根入口与 `Internal/` | `CqrsRuntimeFactory``ICqrsHandlerRegistry``CqrsHandlerRegistryAttribute``CqrsReflectionFallbackAttribute``DefaultCqrsRegistrationService` | runtime 创建入口、generated-registry 优先级、targeted fallback 语义和程序集去重规则 |
| `GFramework.Cqrs.SourceGenerators/Cqrs/` | `CqrsHandlerRegistryGenerator``RuntimeTypeReferenceSpec``OrderedRegistrationKind` | 生成注册器、可直接引用类型判定、mixed fallback 发射与诊断边界 |
## 继续阅读