mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
Merge pull request #304 from GeWuYou/feat/cqrs-optimization
Feat/cqrs optimization
This commit is contained in:
commit
5eea12b5ba
@ -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);
|
||||
}
|
||||
|
||||
@ -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))
|
||||
{
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
101
GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs
Normal file
101
GFramework.Cqrs.Tests/Cqrs/CqrsRegistrationServiceTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>;
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
@ -0,0 +1,8 @@
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 为双行为 pipeline 顺序回归提供最小请求。
|
||||
/// </summary>
|
||||
internal sealed record DispatcherPipelineOrderCacheRequest : IRequest<int>;
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
50
GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderState.cs
Normal file
50
GFramework.Cqrs.Tests/Cqrs/DispatcherPipelineOrderState.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>;
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 与注册入口。
|
||||
|
||||
@ -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。
|
||||
|
||||
@ -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 场景
|
||||
@ -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`。
|
||||
@ -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-up:request 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 是否已刷新为已解决,或仅剩新增有效项
|
||||
|
||||
@ -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 threads:Greptile 指向 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 是否已刷新为已解决,或仅剩新增有效项
|
||||
|
||||
@ -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 模块契约、系统适配、配置对象和运行时装配边界 |
|
||||
|
||||
@ -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 发射与诊断边界 |
|
||||
|
||||
## 继续阅读
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user