mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
feat(cqrs): 前移请求调用器生成注册
- 新增 generated request invoker provider seam,并让 registrar 与 dispatcher 复用编译期请求调用元数据 - 扩展 CQRS source generator 发射 request invoker provider 成员与最小 request invoker 方法 - 补充 runtime 与 source-generator 回归测试,并更新 cqrs-rewrite 追踪到 RP-067
This commit is contained in:
parent
7209fdc32d
commit
0c65cd8e38
@ -5,11 +5,16 @@ namespace GFramework.Cqrs.SourceGenerators.Cqrs;
|
||||
/// </summary>
|
||||
public sealed partial class CqrsHandlerRegistryGenerator
|
||||
{
|
||||
private readonly record struct RequestInvokerRegistrationSpec(
|
||||
string RequestTypeDisplayName,
|
||||
string ResponseTypeDisplayName);
|
||||
|
||||
private readonly record struct HandlerRegistrationSpec(
|
||||
string HandlerInterfaceDisplayName,
|
||||
string ImplementationTypeDisplayName,
|
||||
string HandlerInterfaceLogName,
|
||||
string ImplementationLogName);
|
||||
string ImplementationLogName,
|
||||
RequestInvokerRegistrationSpec? RequestInvokerRegistration);
|
||||
|
||||
private readonly record struct ReflectedImplementationRegistrationSpec(
|
||||
string HandlerInterfaceDisplayName,
|
||||
@ -24,14 +29,24 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
bool HasReflectedImplementationRegistrations,
|
||||
bool HasPreciseReflectedRegistrations,
|
||||
bool HasReflectionTypeLookups,
|
||||
bool HasExternalAssemblyTypeLookups)
|
||||
bool HasExternalAssemblyTypeLookups,
|
||||
bool SupportsRequestInvokerProvider,
|
||||
ImmutableArray<RequestInvokerEmissionSpec> RequestInvokerEmissions)
|
||||
{
|
||||
public bool RequiresRegistryAssemblyVariable =>
|
||||
HasReflectedImplementationRegistrations ||
|
||||
HasPreciseReflectedRegistrations ||
|
||||
HasReflectionTypeLookups;
|
||||
|
||||
public bool HasRequestInvokerProvider => SupportsRequestInvokerProvider && !RequestInvokerEmissions.IsDefaultOrEmpty;
|
||||
}
|
||||
|
||||
private readonly record struct RequestInvokerEmissionSpec(
|
||||
string RequestTypeDisplayName,
|
||||
string ResponseTypeDisplayName,
|
||||
string HandlerInterfaceDisplayName,
|
||||
int MethodIndex);
|
||||
|
||||
/// <summary>
|
||||
/// 标记某条 handler 注册语句在生成阶段采用的表达策略。
|
||||
/// </summary>
|
||||
@ -312,5 +327,6 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
bool GenerationEnabled,
|
||||
bool SupportsNamedReflectionFallbackTypes,
|
||||
bool SupportsDirectReflectionFallbackTypes,
|
||||
bool SupportsMultipleReflectionFallbackAttributes);
|
||||
bool SupportsMultipleReflectionFallbackAttributes,
|
||||
bool SupportsRequestInvokerProvider);
|
||||
}
|
||||
|
||||
@ -25,10 +25,11 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
/// 该方法本身不报告诊断;“fallback 必需但 runtime 契约缺失”的错误由调用方在进入本方法前处理。
|
||||
/// </remarks>
|
||||
private static string GenerateSource(
|
||||
GenerationEnvironment generationEnvironment,
|
||||
IReadOnlyList<ImplementationRegistrationSpec> registrations,
|
||||
ReflectionFallbackEmissionSpec reflectionFallbackEmission)
|
||||
{
|
||||
var sourceShape = CreateGeneratedRegistrySourceShape(registrations);
|
||||
var sourceShape = CreateGeneratedRegistrySourceShape(generationEnvironment, registrations);
|
||||
var builder = new StringBuilder();
|
||||
AppendGeneratedSourcePreamble(builder, reflectionFallbackEmission);
|
||||
AppendGeneratedRegistryType(builder, registrations, sourceShape);
|
||||
@ -41,6 +42,7 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
/// <param name="registrations">已整理并排序的 handler 注册描述。</param>
|
||||
/// <returns>当前生成输出需要启用的结构分支。</returns>
|
||||
private static GeneratedRegistrySourceShape CreateGeneratedRegistrySourceShape(
|
||||
GenerationEnvironment generationEnvironment,
|
||||
IReadOnlyList<ImplementationRegistrationSpec> registrations)
|
||||
{
|
||||
var hasReflectedImplementationRegistrations = registrations.Any(static registration =>
|
||||
@ -52,12 +54,44 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
var hasExternalAssemblyTypeLookups = registrations.Any(static registration =>
|
||||
registration.PreciseReflectedRegistrations.Any(static preciseRegistration =>
|
||||
preciseRegistration.ServiceTypeArguments.Any(ContainsExternalAssemblyTypeLookup)));
|
||||
var requestInvokerEmissions = CreateRequestInvokerEmissions(
|
||||
generationEnvironment.SupportsRequestInvokerProvider,
|
||||
registrations);
|
||||
|
||||
return new GeneratedRegistrySourceShape(
|
||||
hasReflectedImplementationRegistrations,
|
||||
hasPreciseReflectedRegistrations,
|
||||
hasReflectionTypeLookups,
|
||||
hasExternalAssemblyTypeLookups);
|
||||
hasExternalAssemblyTypeLookups,
|
||||
generationEnvironment.SupportsRequestInvokerProvider,
|
||||
requestInvokerEmissions);
|
||||
}
|
||||
|
||||
private static ImmutableArray<RequestInvokerEmissionSpec> CreateRequestInvokerEmissions(
|
||||
bool supportsRequestInvokerProvider,
|
||||
IReadOnlyList<ImplementationRegistrationSpec> registrations)
|
||||
{
|
||||
if (!supportsRequestInvokerProvider)
|
||||
return ImmutableArray<RequestInvokerEmissionSpec>.Empty;
|
||||
|
||||
var builder = ImmutableArray.CreateBuilder<RequestInvokerEmissionSpec>();
|
||||
var methodIndex = 0;
|
||||
foreach (var registration in registrations)
|
||||
{
|
||||
foreach (var directRegistration in registration.DirectRegistrations)
|
||||
{
|
||||
if (directRegistration.RequestInvokerRegistration is not { } requestInvokerRegistration)
|
||||
continue;
|
||||
|
||||
builder.Add(new RequestInvokerEmissionSpec(
|
||||
requestInvokerRegistration.RequestTypeDisplayName,
|
||||
requestInvokerRegistration.ResponseTypeDisplayName,
|
||||
directRegistration.HandlerInterfaceDisplayName,
|
||||
methodIndex++));
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -160,10 +194,26 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
builder.Append(GeneratedTypeName);
|
||||
builder.Append(" : global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.AppendLine(".ICqrsHandlerRegistry");
|
||||
builder.Append(".ICqrsHandlerRegistry");
|
||||
if (sourceShape.HasRequestInvokerProvider)
|
||||
{
|
||||
builder.Append(", global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.Append(".ICqrsRequestInvokerProvider, global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.Append(".IEnumeratesCqrsRequestInvokerDescriptors");
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
builder.AppendLine("{");
|
||||
AppendRegisterMethod(builder, registrations, sourceShape);
|
||||
|
||||
if (sourceShape.HasRequestInvokerProvider)
|
||||
{
|
||||
builder.AppendLine();
|
||||
AppendRequestInvokerProviderMembers(builder, sourceShape.RequestInvokerEmissions);
|
||||
}
|
||||
|
||||
if (sourceShape.HasExternalAssemblyTypeLookups)
|
||||
{
|
||||
builder.AppendLine();
|
||||
@ -223,6 +273,103 @@ public sealed partial class CqrsHandlerRegistryGenerator
|
||||
builder.AppendLine(" }");
|
||||
}
|
||||
|
||||
private static void AppendRequestInvokerProviderMembers(
|
||||
StringBuilder builder,
|
||||
ImmutableArray<RequestInvokerEmissionSpec> requestInvokerEmissions)
|
||||
{
|
||||
AppendRequestInvokerDescriptorArray(builder, requestInvokerEmissions);
|
||||
builder.AppendLine();
|
||||
AppendRequestInvokerProviderMethods(builder);
|
||||
|
||||
for (var index = 0; index < requestInvokerEmissions.Length; index++)
|
||||
{
|
||||
builder.AppendLine();
|
||||
AppendRequestInvokerMethod(builder, requestInvokerEmissions[index]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendRequestInvokerDescriptorArray(
|
||||
StringBuilder builder,
|
||||
ImmutableArray<RequestInvokerEmissionSpec> requestInvokerEmissions)
|
||||
{
|
||||
builder.AppendLine(" private static readonly global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry[] RequestInvokerDescriptors =");
|
||||
builder.AppendLine(" [");
|
||||
|
||||
for (var index = 0; index < requestInvokerEmissions.Length; index++)
|
||||
{
|
||||
var emission = requestInvokerEmissions[index];
|
||||
builder.Append(" new global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.Append(".CqrsRequestInvokerDescriptorEntry(typeof(");
|
||||
builder.Append(emission.RequestTypeDisplayName);
|
||||
builder.Append("), typeof(");
|
||||
builder.Append(emission.ResponseTypeDisplayName);
|
||||
builder.Append("), new global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.Append(".CqrsRequestInvokerDescriptor(typeof(");
|
||||
builder.Append(emission.HandlerInterfaceDisplayName);
|
||||
builder.Append("), typeof(");
|
||||
builder.Append(GeneratedTypeName);
|
||||
builder.Append(").GetMethod(nameof(InvokeRequestHandler");
|
||||
builder.Append(emission.MethodIndex);
|
||||
builder.Append("), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!))");
|
||||
builder.AppendLine(index == requestInvokerEmissions.Length - 1 ? string.Empty : ",");
|
||||
}
|
||||
|
||||
builder.AppendLine(" ];");
|
||||
}
|
||||
|
||||
private static void AppendRequestInvokerProviderMethods(StringBuilder builder)
|
||||
{
|
||||
builder.Append(" public global::System.Collections.Generic.IReadOnlyList<global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.AppendLine(".CqrsRequestInvokerDescriptorEntry> GetDescriptors()");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" return RequestInvokerDescriptors;");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine();
|
||||
builder.Append(" public bool TryGetDescriptor(global::System.Type requestType, global::System.Type responseType, out global::");
|
||||
builder.Append(CqrsRuntimeNamespace);
|
||||
builder.AppendLine(".CqrsRequestInvokerDescriptor? descriptor)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" if (requestType is null)");
|
||||
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(requestType));");
|
||||
builder.AppendLine(" if (responseType is null)");
|
||||
builder.AppendLine(" throw new global::System.ArgumentNullException(nameof(responseType));");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(" foreach (var entry in RequestInvokerDescriptors)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" if (entry.RequestType == requestType && entry.ResponseType == responseType)");
|
||||
builder.AppendLine(" {");
|
||||
builder.AppendLine(" descriptor = entry.Descriptor;");
|
||||
builder.AppendLine(" return true;");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine(" }");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(" descriptor = null;");
|
||||
builder.AppendLine(" return false;");
|
||||
builder.AppendLine(" }");
|
||||
}
|
||||
|
||||
private static void AppendRequestInvokerMethod(StringBuilder builder, RequestInvokerEmissionSpec emission)
|
||||
{
|
||||
builder.Append(" private static global::System.Threading.Tasks.ValueTask<");
|
||||
builder.Append(emission.ResponseTypeDisplayName);
|
||||
builder.Append("> InvokeRequestHandler");
|
||||
builder.Append(emission.MethodIndex);
|
||||
builder.Append("(object handler, object request, global::System.Threading.CancellationToken cancellationToken)");
|
||||
builder.AppendLine();
|
||||
builder.AppendLine(" {");
|
||||
builder.Append(" var typedHandler = (");
|
||||
builder.Append(emission.HandlerInterfaceDisplayName);
|
||||
builder.AppendLine(")handler;");
|
||||
builder.Append(" var typedRequest = (");
|
||||
builder.Append(emission.RequestTypeDisplayName);
|
||||
builder.AppendLine(")request;");
|
||||
builder.AppendLine(" return typedHandler.Handle(typedRequest, cancellationToken);");
|
||||
builder.AppendLine(" }");
|
||||
}
|
||||
|
||||
private static void AppendDirectRegistrations(
|
||||
StringBuilder builder,
|
||||
ImplementationRegistrationSpec registration)
|
||||
|
||||
@ -15,6 +15,13 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
private const string INotificationHandlerMetadataName = $"{CqrsContractsNamespace}.INotificationHandler`1";
|
||||
private const string IStreamRequestHandlerMetadataName = $"{CqrsContractsNamespace}.IStreamRequestHandler`2";
|
||||
private const string ICqrsHandlerRegistryMetadataName = $"{CqrsRuntimeNamespace}.ICqrsHandlerRegistry";
|
||||
private const string ICqrsRequestInvokerProviderMetadataName = $"{CqrsRuntimeNamespace}.ICqrsRequestInvokerProvider";
|
||||
private const string IEnumeratesCqrsRequestInvokerDescriptorsMetadataName =
|
||||
$"{CqrsRuntimeNamespace}.IEnumeratesCqrsRequestInvokerDescriptors";
|
||||
private const string CqrsRequestInvokerDescriptorMetadataName =
|
||||
$"{CqrsRuntimeNamespace}.CqrsRequestInvokerDescriptor";
|
||||
private const string CqrsRequestInvokerDescriptorEntryMetadataName =
|
||||
$"{CqrsRuntimeNamespace}.CqrsRequestInvokerDescriptorEntry";
|
||||
|
||||
private const string CqrsHandlerRegistryAttributeMetadataName =
|
||||
$"{CqrsRuntimeNamespace}.CqrsHandlerRegistryAttribute";
|
||||
@ -66,6 +73,11 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
CqrsHandlerRegistryAttributeMetadataName) is not null &&
|
||||
compilation.GetTypeByMetadataName(ILoggerMetadataName) is not null &&
|
||||
compilation.GetTypeByMetadataName(IServiceCollectionMetadataName) is not null;
|
||||
var supportsRequestInvokerProvider =
|
||||
compilation.GetTypeByMetadataName(ICqrsRequestInvokerProviderMetadataName) is not null &&
|
||||
compilation.GetTypeByMetadataName(IEnumeratesCqrsRequestInvokerDescriptorsMetadataName) is not null &&
|
||||
compilation.GetTypeByMetadataName(CqrsRequestInvokerDescriptorMetadataName) is not null &&
|
||||
compilation.GetTypeByMetadataName(CqrsRequestInvokerDescriptorEntryMetadataName) is not null;
|
||||
var stringType = compilation.GetSpecialType(SpecialType.System_String);
|
||||
var typeType = compilation.GetTypeByMetadataName("System.Type");
|
||||
var supportsNamedReflectionFallbackTypes = reflectionFallbackAttributeType is not null &&
|
||||
@ -85,7 +97,8 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
generationEnabled,
|
||||
supportsNamedReflectionFallbackTypes,
|
||||
supportsDirectReflectionFallbackTypes,
|
||||
supportsMultipleReflectionFallbackAttributes);
|
||||
supportsMultipleReflectionFallbackAttributes,
|
||||
supportsRequestInvokerProvider);
|
||||
}
|
||||
|
||||
private static bool IsHandlerCandidate(SyntaxNode node)
|
||||
@ -218,7 +231,10 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
handlerInterface.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||||
implementationTypeDisplayName,
|
||||
GetLogDisplayName(handlerInterface),
|
||||
implementationLogName));
|
||||
implementationLogName,
|
||||
TryCreateRequestInvokerRegistrationSpec(handlerInterface, out var requestInvokerRegistration)
|
||||
? requestInvokerRegistration
|
||||
: null));
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -237,6 +253,34 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当当前直接注册项属于请求处理器时,提取 request invoker provider 所需的请求/响应类型显示名。
|
||||
/// </summary>
|
||||
private static bool TryCreateRequestInvokerRegistrationSpec(
|
||||
INamedTypeSymbol handlerInterface,
|
||||
out RequestInvokerRegistrationSpec requestInvokerRegistration)
|
||||
{
|
||||
if (!string.Equals(
|
||||
handlerInterface.OriginalDefinition.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||||
$"global::{CqrsContractsNamespace}.IRequestHandler<TRequest, TResponse>",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
requestInvokerRegistration = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (handlerInterface.TypeArguments.Length != 2)
|
||||
{
|
||||
requestInvokerRegistration = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
requestInvokerRegistration = new RequestInvokerRegistrationSpec(
|
||||
handlerInterface.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
|
||||
handlerInterface.TypeArguments[1].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行 CQRS handler registry 生成管线的最终发射阶段,负责将候选 handler 分析结果汇总为单个
|
||||
/// <c>CqrsHandlerRegistry.g.cs</c>,并在需要时附带程序集级 reflection fallback 元数据。
|
||||
@ -296,7 +340,7 @@ public sealed partial class CqrsHandlerRegistryGenerator : IIncrementalGenerator
|
||||
|
||||
context.AddSource(
|
||||
HintName,
|
||||
GenerateSource(registrations, reflectionFallbackEmission));
|
||||
GenerateSource(generationEnvironment, registrations, reflectionFallbackEmission));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -0,0 +1,177 @@
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Architectures;
|
||||
using GFramework.Core.Ioc;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 验证 generated request invoker provider 的 registrar 接线与 dispatcher 消费语义。
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[NonParallelizable]
|
||||
internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 在每个用例前重置 registrar / dispatcher 的静态缓存,避免跨用例共享状态影响断言。
|
||||
/// </summary>
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider();
|
||||
ClearRegistrarCaches();
|
||||
ClearDispatcherCaches();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在每个用例后清理静态缓存。
|
||||
/// </summary>
|
||||
[TearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
ClearRegistrarCaches();
|
||||
ClearDispatcherCaches();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 registrar 激活 generated registry 后,会把 request invoker provider 注册到容器中。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void RegisterHandlers_Should_Register_Generated_Request_Invoker_Provider()
|
||||
{
|
||||
var generatedAssembly = CreateGeneratedRequestInvokerAssembly();
|
||||
var container = new MicrosoftDiContainer();
|
||||
|
||||
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||
|
||||
var providers = container.GetAll<ICqrsRequestInvokerProvider>();
|
||||
|
||||
Assert.That(
|
||||
providers.Select(static provider => provider.GetType()),
|
||||
Is.EqualTo([typeof(GeneratedRequestInvokerProviderRegistry)]));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 dispatcher 在首次创建 request binding 时,会优先消费 generated request invoker provider。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task SendAsync_Should_Use_Generated_Request_Invoker_When_Provider_Is_Registered()
|
||||
{
|
||||
var generatedAssembly = CreateGeneratedRequestInvokerAssembly();
|
||||
var container = new MicrosoftDiContainer();
|
||||
|
||||
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||
container.Freeze();
|
||||
|
||||
var context = new ArchitectureContext(container);
|
||||
var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload"));
|
||||
|
||||
var requestBindings = GetDispatcherCacheField("RequestDispatchBindings");
|
||||
var binding = GetRequestDispatchBindingValue(
|
||||
requestBindings,
|
||||
typeof(GeneratedRequestInvokerRequest),
|
||||
typeof(string));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(response, Is.EqualTo("generated:payload"));
|
||||
Assert.That(binding, Is.Not.Null);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建带有 generated request invoker registry 元数据的程序集替身。
|
||||
/// </summary>
|
||||
private static Mock<Assembly> CreateGeneratedRequestInvokerAssembly()
|
||||
{
|
||||
var generatedAssembly = new Mock<Assembly>();
|
||||
generatedAssembly
|
||||
.SetupGet(static assembly => assembly.FullName)
|
||||
.Returns("GFramework.Cqrs.Tests.Cqrs.GeneratedRequestInvokerAssembly, Version=1.0.0.0");
|
||||
generatedAssembly
|
||||
.Setup(static assembly => assembly.GetCustomAttributes(typeof(CqrsHandlerRegistryAttribute), false))
|
||||
.Returns([new CqrsHandlerRegistryAttribute(typeof(GeneratedRequestInvokerProviderRegistry))]);
|
||||
return generatedAssembly;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空 registrar 静态缓存。
|
||||
/// </summary>
|
||||
private static void ClearRegistrarCaches()
|
||||
{
|
||||
ClearCache(GetRegistrarCacheField("AssemblyMetadataCache"));
|
||||
ClearCache(GetRegistrarCacheField("RegistryActivationMetadataCache"));
|
||||
ClearCache(GetRegistrarCacheField("LoadableTypesCache"));
|
||||
ClearCache(GetRegistrarCacheField("SupportedHandlerInterfacesCache"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空 dispatcher 静态缓存。
|
||||
/// </summary>
|
||||
private static void ClearDispatcherCaches()
|
||||
{
|
||||
ClearCache(GetDispatcherCacheField("NotificationDispatchBindings"));
|
||||
ClearCache(GetDispatcherCacheField("RequestDispatchBindings"));
|
||||
ClearCache(GetDispatcherCacheField("StreamDispatchBindings"));
|
||||
ClearCache(GetDispatcherCacheField("GeneratedRequestInvokers"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过反射读取 registrar 的静态缓存字段。
|
||||
/// </summary>
|
||||
private static object GetRegistrarCacheField(string fieldName)
|
||||
{
|
||||
var field = typeof(CqrsReflectionFallbackAttribute).Assembly
|
||||
.GetType("GFramework.Cqrs.Internal.CqrsHandlerRegistrar", throwOnError: true)!
|
||||
.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static);
|
||||
|
||||
Assert.That(field, Is.Not.Null, $"Missing registrar cache field {fieldName}.");
|
||||
return field!.GetValue(null)
|
||||
?? throw new InvalidOperationException($"Registrar cache field {fieldName} returned null.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过反射读取 dispatcher 的静态缓存字段。
|
||||
/// </summary>
|
||||
private static object GetDispatcherCacheField(string fieldName)
|
||||
{
|
||||
var field = typeof(CqrsReflectionFallbackAttribute).Assembly
|
||||
.GetType("GFramework.Cqrs.Internal.CqrsDispatcher", throwOnError: true)!
|
||||
.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static);
|
||||
|
||||
Assert.That(field, Is.Not.Null, $"Missing dispatcher cache field {fieldName}.");
|
||||
return field!.GetValue(null)
|
||||
?? throw new InvalidOperationException($"Dispatcher cache field {fieldName} returned null.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空目标缓存实例。
|
||||
/// </summary>
|
||||
private static void ClearCache(object cache)
|
||||
{
|
||||
_ = cache.GetType()
|
||||
.GetMethod("Clear", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!
|
||||
.Invoke(cache, Array.Empty<object>());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取指定请求/响应类型对当前缓存的 request dispatch binding。
|
||||
/// </summary>
|
||||
private static object? GetRequestDispatchBindingValue(object requestBindings, Type requestType, Type responseType)
|
||||
{
|
||||
var bindingBox = requestBindings.GetType()
|
||||
.GetMethod("GetValueOrDefaultForTesting", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!
|
||||
.Invoke(requestBindings, [requestType, responseType]);
|
||||
if (bindingBox is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return bindingBox.GetType()
|
||||
.GetMethod("Get", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!
|
||||
.MakeGenericMethod(responseType)
|
||||
.Invoke(bindingBox, Array.Empty<object>());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
using System.Reflection;
|
||||
using GFramework.Core.Abstractions.Logging;
|
||||
using GFramework.Core.Ioc;
|
||||
using GFramework.Core.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 模拟同时提供 handler 注册与 request invoker 元数据的 generated registry。
|
||||
/// </summary>
|
||||
internal sealed class GeneratedRequestInvokerProviderRegistry :
|
||||
ICqrsHandlerRegistry,
|
||||
ICqrsRequestInvokerProvider,
|
||||
IEnumeratesCqrsRequestInvokerDescriptors
|
||||
{
|
||||
private static readonly CqrsRequestInvokerDescriptor Descriptor = new(
|
||||
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||
typeof(GeneratedRequestInvokerProviderRegistry).GetMethod(
|
||||
nameof(InvokeGenerated),
|
||||
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||
|
||||
private static readonly CqrsRequestInvokerDescriptorEntry DescriptorEntry = new(
|
||||
typeof(GeneratedRequestInvokerRequest),
|
||||
typeof(string),
|
||||
Descriptor);
|
||||
|
||||
/// <summary>
|
||||
/// 将测试请求处理器注册到目标服务集合。
|
||||
/// </summary>
|
||||
/// <param name="services">承载处理器映射的服务集合。</param>
|
||||
/// <param name="logger">用于记录注册诊断的日志器。</param>
|
||||
public void Register(IServiceCollection services, ILogger logger)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(services);
|
||||
ArgumentNullException.ThrowIfNull(logger);
|
||||
|
||||
services.AddTransient(
|
||||
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||
typeof(GeneratedRequestInvokerRequestHandler));
|
||||
logger.Debug(
|
||||
$"Registered CQRS handler {typeof(GeneratedRequestInvokerRequestHandler).FullName} as {typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>).FullName}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试返回指定 request/response 类型对对应的 generated invoker 描述符。
|
||||
/// </summary>
|
||||
/// <param name="requestType">请求运行时类型。</param>
|
||||
/// <param name="responseType">响应运行时类型。</param>
|
||||
/// <param name="descriptor">命中时返回的描述符。</param>
|
||||
/// <returns>若类型对匹配当前测试请求则返回 <see langword="true" />。</returns>
|
||||
public bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out CqrsRequestInvokerDescriptor? descriptor)
|
||||
{
|
||||
if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string))
|
||||
{
|
||||
descriptor = Descriptor;
|
||||
return true;
|
||||
}
|
||||
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前 registry 暴露的全部 generated request invoker 描述符。
|
||||
/// </summary>
|
||||
/// <returns>单条测试 request invoker 描述符条目。</returns>
|
||||
public IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors()
|
||||
{
|
||||
return [DescriptorEntry];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 模拟 generated request invoker 直接执行后的返回值。
|
||||
/// </summary>
|
||||
/// <param name="handler">当前请求处理器实例。</param>
|
||||
/// <param name="request">当前测试请求。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>带有 generated 前缀的结果,便于断言 dispatcher 走了 provider 路径。</returns>
|
||||
private static ValueTask<string> InvokeGenerated(object handler, object request, CancellationToken cancellationToken)
|
||||
{
|
||||
_ = handler as IRequestHandler<GeneratedRequestInvokerRequest, string>
|
||||
?? throw new InvalidOperationException("Generated invoker received an incompatible handler instance.");
|
||||
var typedRequest = (GeneratedRequestInvokerRequest)request;
|
||||
return ValueTask.FromResult($"generated:{typedRequest.Value}");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 用于验证 generated request invoker provider 接线的测试请求。
|
||||
/// </summary>
|
||||
internal sealed record GeneratedRequestInvokerRequest(string Value) : IRequest<string>;
|
||||
@ -0,0 +1,21 @@
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs.Tests.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 供 generated request invoker provider 测试使用的请求处理器。
|
||||
/// </summary>
|
||||
internal sealed class GeneratedRequestInvokerRequestHandler : IRequestHandler<GeneratedRequestInvokerRequest, string>
|
||||
{
|
||||
/// <summary>
|
||||
/// 返回带有运行时处理器前缀的结果,便于和 generated invoker 自定义结果区分。
|
||||
/// </summary>
|
||||
/// <param name="request">当前测试请求。</param>
|
||||
/// <param name="cancellationToken">取消令牌。</param>
|
||||
/// <returns>运行时处理器生成的响应字符串。</returns>
|
||||
public ValueTask<string> Handle(GeneratedRequestInvokerRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
return ValueTask.FromResult($"runtime:{request.Value}");
|
||||
}
|
||||
}
|
||||
31
GFramework.Cqrs/CqrsRequestInvokerDescriptor.cs
Normal file
31
GFramework.Cqrs/CqrsRequestInvokerDescriptor.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System.Reflection;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 描述单个 request/response 类型对在运行时分发时需要复用的元数据。
|
||||
/// </summary>
|
||||
/// <param name="handlerType">当前请求处理器在容器中的服务类型。</param>
|
||||
/// <param name="invokerMethod">
|
||||
/// 执行单个请求处理器的开放静态方法。
|
||||
/// dispatcher 会在首次创建 request binding 时,把该方法绑定成内部使用的强类型委托。
|
||||
/// </param>
|
||||
/// <remarks>
|
||||
/// dispatcher 会继续自行构造 pipeline behavior 服务类型并负责上下文注入;
|
||||
/// 该描述符只前移请求处理器服务类型与直接调用方法元数据。
|
||||
/// </remarks>
|
||||
public sealed class CqrsRequestInvokerDescriptor(
|
||||
Type handlerType,
|
||||
MethodInfo invokerMethod)
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取请求处理器在容器中的服务类型。
|
||||
/// </summary>
|
||||
public Type HandlerType { get; } = handlerType ?? throw new ArgumentNullException(nameof(handlerType));
|
||||
|
||||
/// <summary>
|
||||
/// 获取执行请求处理器的开放静态方法。
|
||||
/// </summary>
|
||||
public MethodInfo InvokerMethod { get; } = invokerMethod ?? throw new ArgumentNullException(nameof(invokerMethod));
|
||||
}
|
||||
12
GFramework.Cqrs/CqrsRequestInvokerDescriptorEntry.cs
Normal file
12
GFramework.Cqrs/CqrsRequestInvokerDescriptorEntry.cs
Normal file
@ -0,0 +1,12 @@
|
||||
namespace GFramework.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 描述单个 request/response 类型对与其 generated invoker 元数据之间的映射条目。
|
||||
/// </summary>
|
||||
/// <param name="RequestType">请求运行时类型。</param>
|
||||
/// <param name="ResponseType">响应运行时类型。</param>
|
||||
/// <param name="Descriptor">对应的 generated request invoker 描述符。</param>
|
||||
public sealed record CqrsRequestInvokerDescriptorEntry(
|
||||
Type RequestType,
|
||||
Type ResponseType,
|
||||
CqrsRequestInvokerDescriptor Descriptor);
|
||||
26
GFramework.Cqrs/ICqrsRequestInvokerProvider.cs
Normal file
26
GFramework.Cqrs/ICqrsRequestInvokerProvider.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
namespace GFramework.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 定义由源码生成器或手写注册器提供的 request invoker 元数据契约。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该 seam 允许运行时在首次创建 request dispatch binding 时,
|
||||
/// 直接复用编译期已知的请求/响应类型映射,而不是总是通过反射闭合泛型方法生成调用委托。
|
||||
/// 当当前程序集没有提供匹配项时,dispatcher 仍会回退到既有的反射绑定创建路径。
|
||||
/// </remarks>
|
||||
public interface ICqrsRequestInvokerProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// 尝试为指定请求/响应类型对提供运行时元数据。
|
||||
/// </summary>
|
||||
/// <param name="requestType">请求运行时类型。</param>
|
||||
/// <param name="responseType">响应运行时类型。</param>
|
||||
/// <param name="descriptor">命中时返回的 request invoker 元数据。</param>
|
||||
/// <returns>若当前 provider 可处理该请求/响应类型对则返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||
bool TryGetDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
out CqrsRequestInvokerDescriptor? descriptor);
|
||||
}
|
||||
18
GFramework.Cqrs/IEnumeratesCqrsRequestInvokerDescriptors.cs
Normal file
18
GFramework.Cqrs/IEnumeratesCqrsRequestInvokerDescriptors.cs
Normal file
@ -0,0 +1,18 @@
|
||||
namespace GFramework.Cqrs;
|
||||
|
||||
/// <summary>
|
||||
/// 为 generated request invoker provider 暴露可枚举描述符集合的内部辅助契约。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// registrar 在激活 generated registry 后,会通过该接口读取当前程序集声明的 request invoker 描述符,
|
||||
/// 并把它们登记到 dispatcher 的进程级弱缓存中。
|
||||
/// 该接口不改变公开分发语义,只服务于 generated invoker 元数据的运行时接线。
|
||||
/// </remarks>
|
||||
public interface IEnumeratesCqrsRequestInvokerDescriptors
|
||||
{
|
||||
/// <summary>
|
||||
/// 返回当前 provider 可声明的全部 request invoker 描述符条目。
|
||||
/// </summary>
|
||||
/// <returns>按 provider 定义顺序枚举的描述符条目集合。</returns>
|
||||
IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors();
|
||||
}
|
||||
@ -18,6 +18,11 @@ internal sealed class CqrsDispatcher(
|
||||
ILogger logger,
|
||||
INotificationPublisher notificationPublisher) : ICqrsRuntime
|
||||
{
|
||||
// 卸载安全的进程级缓存:当 generated registry 提供 request invoker 元数据时,
|
||||
// registrar 会按请求/响应类型对把它们写入这里;若类型被卸载,条目会自然失效。
|
||||
private static readonly WeakTypePairCache<GeneratedRequestInvokerMetadata>
|
||||
GeneratedRequestInvokers = new();
|
||||
|
||||
// 卸载安全的进程级缓存:通知类型只以弱键语义保留。
|
||||
// 若插件/热重载程序集中的通知类型被卸载,对应分发绑定会自然失效,下次命中时再重新计算。
|
||||
private static readonly WeakKeyCache<Type, NotificationDispatchBinding>
|
||||
@ -169,6 +174,17 @@ internal sealed class CqrsDispatcher(
|
||||
/// </summary>
|
||||
private static RequestDispatchBinding<TResponse> CreateRequestDispatchBinding<TResponse>(Type requestType)
|
||||
{
|
||||
var generatedDescriptor = TryGetGeneratedRequestInvokerDescriptor<TResponse>(requestType);
|
||||
if (generatedDescriptor is not null)
|
||||
{
|
||||
var resolvedGeneratedDescriptor = generatedDescriptor.Value;
|
||||
return new RequestDispatchBinding<TResponse>(
|
||||
resolvedGeneratedDescriptor.HandlerType,
|
||||
typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)),
|
||||
resolvedGeneratedDescriptor.Invoker,
|
||||
requestType);
|
||||
}
|
||||
|
||||
return new RequestDispatchBinding<TResponse>(
|
||||
typeof(IRequestHandler<,>).MakeGenericType(requestType, typeof(TResponse)),
|
||||
typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)),
|
||||
@ -203,6 +219,48 @@ internal sealed class CqrsDispatcher(
|
||||
return RequestDispatchBindingBox.Create(CreateRequestDispatchBinding<TResponse>(requestType));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试从容器已注册的 generated request invoker provider 中获取指定请求/响应类型对的元数据。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">当前请求响应类型。</typeparam>
|
||||
/// <param name="requestType">请求运行时类型。</param>
|
||||
/// <returns>命中时返回强类型化后的描述符;否则返回 <see langword="null" />。</returns>
|
||||
private static RequestInvokerDescriptor<TResponse>? TryGetGeneratedRequestInvokerDescriptor<TResponse>(Type requestType)
|
||||
{
|
||||
return GeneratedRequestInvokers.TryGetValue(requestType, typeof(TResponse), out var metadata) &&
|
||||
metadata is not null
|
||||
? CreateRequestInvokerDescriptor<TResponse>(requestType, metadata)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 把 provider 返回的弱类型描述符转换为 dispatcher 内部使用的强类型 request invoker 描述符。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">当前请求响应类型。</typeparam>
|
||||
/// <param name="requestType">请求运行时类型。</param>
|
||||
/// <param name="descriptor">provider 返回的弱类型描述符。</param>
|
||||
/// <returns>可直接用于创建 request dispatch binding 的强类型描述符。</returns>
|
||||
/// <exception cref="InvalidOperationException">当 provider 返回的委托签名与当前请求/响应类型对不匹配时抛出。</exception>
|
||||
private static RequestInvokerDescriptor<TResponse> CreateRequestInvokerDescriptor<TResponse>(
|
||||
Type requestType,
|
||||
GeneratedRequestInvokerMetadata descriptor)
|
||||
{
|
||||
if (!descriptor.InvokerMethod.IsStatic)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Generated CQRS request invoker provider returned a non-static invoker method for request type {requestType.FullName} and response type {typeof(TResponse).FullName}.");
|
||||
}
|
||||
|
||||
if (Delegate.CreateDelegate(typeof(RequestInvoker<TResponse>), descriptor.InvokerMethod) is not
|
||||
RequestInvoker<TResponse> invoker)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Generated CQRS request invoker provider returned an incompatible invoker for request type {requestType.FullName} and response type {typeof(TResponse).FullName}.");
|
||||
}
|
||||
|
||||
return new RequestInvokerDescriptor<TResponse>(descriptor.HandlerType, invoker);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为指定通知类型构造完整分发绑定,把服务类型与调用委托聚合到同一缓存项。
|
||||
/// </summary>
|
||||
@ -579,6 +637,46 @@ internal sealed class CqrsDispatcher(
|
||||
private readonly record struct RequestPipelineExecutorFactoryState<TResponse>(
|
||||
RequestPipelineInvoker<TResponse> PipelineInvoker);
|
||||
|
||||
/// <summary>
|
||||
/// 记录 registrar 写入的 generated request invoker 元数据。
|
||||
/// </summary>
|
||||
/// <param name="HandlerType">请求处理器在容器中的服务类型。</param>
|
||||
/// <param name="InvokerMethod">执行请求处理器的开放静态方法。</param>
|
||||
private sealed record GeneratedRequestInvokerMetadata(
|
||||
Type HandlerType,
|
||||
MethodInfo InvokerMethod);
|
||||
|
||||
/// <summary>
|
||||
/// 保存 provider 返回的请求处理器服务类型与强类型 request invoker。
|
||||
/// </summary>
|
||||
/// <typeparam name="TResponse">当前请求响应类型。</typeparam>
|
||||
private readonly record struct RequestInvokerDescriptor<TResponse>(
|
||||
Type HandlerType,
|
||||
RequestInvoker<TResponse> Invoker);
|
||||
|
||||
/// <summary>
|
||||
/// 供 registrar 在 generated registry 激活后登记 request invoker 元数据。
|
||||
/// </summary>
|
||||
/// <param name="requestType">请求运行时类型。</param>
|
||||
/// <param name="responseType">响应运行时类型。</param>
|
||||
/// <param name="descriptor">要登记的 generated request invoker 描述符。</param>
|
||||
internal static void RegisterGeneratedRequestInvokerDescriptor(
|
||||
Type requestType,
|
||||
Type responseType,
|
||||
CqrsRequestInvokerDescriptor descriptor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(requestType);
|
||||
ArgumentNullException.ThrowIfNull(responseType);
|
||||
ArgumentNullException.ThrowIfNull(descriptor);
|
||||
|
||||
_ = GeneratedRequestInvokers.GetOrAdd(
|
||||
requestType,
|
||||
responseType,
|
||||
(_, _) => new GeneratedRequestInvokerMetadata(
|
||||
descriptor.HandlerType,
|
||||
descriptor.InvokerMethod));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存单次 request pipeline 分发所需的当前 handler、behavior 列表和 continuation 缓存。
|
||||
/// 该对象只存在于本次分发,不会跨请求保留容器解析出的实例。
|
||||
|
||||
@ -239,6 +239,62 @@ internal static class CqrsHandlerRegistrar
|
||||
logger.Debug(
|
||||
$"Registering CQRS handlers for assembly {assemblyName} via generated registry {registry.GetType().FullName}.");
|
||||
registry.Register(services, logger);
|
||||
RegisterGeneratedRequestInvokerProvider(services, registry, assemblyName, logger);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当 generated registry 同时提供 request invoker 元数据时,把该 provider 注册到当前容器中。
|
||||
/// </summary>
|
||||
/// <param name="services">目标服务集合。</param>
|
||||
/// <param name="registry">当前已激活的 generated registry。</param>
|
||||
/// <param name="assemblyName">当前程序集的稳定名称。</param>
|
||||
/// <param name="logger">日志记录器。</param>
|
||||
/// <remarks>
|
||||
/// provider 作为 registry 的附加能力注册到容器后,dispatcher 才能在首次请求分发时优先消费编译期生成的 invoker 元数据。
|
||||
/// 若 registry 不实现该契约,则保持现有纯反射 request binding 创建语义。
|
||||
/// </remarks>
|
||||
private static void RegisterGeneratedRequestInvokerProvider(
|
||||
IServiceCollection services,
|
||||
ICqrsHandlerRegistry registry,
|
||||
string assemblyName,
|
||||
ILogger logger)
|
||||
{
|
||||
if (registry is not ICqrsRequestInvokerProvider provider)
|
||||
return;
|
||||
|
||||
services.AddSingleton(typeof(ICqrsRequestInvokerProvider), provider);
|
||||
RegisterGeneratedRequestInvokerDescriptors(provider, assemblyName, logger);
|
||||
logger.Debug(
|
||||
$"Registered CQRS request invoker provider {provider.GetType().FullName} for assembly {assemblyName}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取 generated request invoker provider 中当前可见的描述符,并写入 dispatcher 的进程级弱缓存。
|
||||
/// </summary>
|
||||
/// <param name="provider">当前已激活的 request invoker provider。</param>
|
||||
/// <param name="assemblyName">当前程序集的稳定名称。</param>
|
||||
/// <param name="logger">日志记录器。</param>
|
||||
/// <remarks>
|
||||
/// 运行时当前只要求 provider 暴露可枚举的描述符集合,而不是在 dispatcher 首次命中时再回调容器。
|
||||
/// 这样 request dispatch binding 的静态缓存创建仍然只依赖类型键,不需要依赖具体容器实例。
|
||||
/// </remarks>
|
||||
private static void RegisterGeneratedRequestInvokerDescriptors(
|
||||
ICqrsRequestInvokerProvider provider,
|
||||
string assemblyName,
|
||||
ILogger logger)
|
||||
{
|
||||
if (provider is not IEnumeratesCqrsRequestInvokerDescriptors descriptorSource)
|
||||
return;
|
||||
|
||||
foreach (var descriptorEntry in descriptorSource.GetDescriptors())
|
||||
{
|
||||
CqrsDispatcher.RegisterGeneratedRequestInvokerDescriptor(
|
||||
descriptorEntry.RequestType,
|
||||
descriptorEntry.ResponseType,
|
||||
descriptorEntry.Descriptor);
|
||||
logger.Debug(
|
||||
$"Registered generated CQRS request invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from assembly {assemblyName}.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1689,6 +1689,107 @@ public class CqrsHandlerRegistryGeneratorTests
|
||||
}
|
||||
""";
|
||||
|
||||
private const string RequestInvokerProviderSource = """
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
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>
|
||||
{
|
||||
ValueTask<TResponse> Handle(TRequest request, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public interface ICqrsRequestInvokerProvider
|
||||
{
|
||||
bool TryGetDescriptor(Type requestType, Type responseType, out CqrsRequestInvokerDescriptor? descriptor);
|
||||
}
|
||||
|
||||
public interface IEnumeratesCqrsRequestInvokerDescriptors
|
||||
{
|
||||
IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors();
|
||||
}
|
||||
|
||||
public sealed class CqrsRequestInvokerDescriptor
|
||||
{
|
||||
public CqrsRequestInvokerDescriptor(Type handlerType, MethodInfo invokerMethod) { }
|
||||
}
|
||||
|
||||
public sealed class CqrsRequestInvokerDescriptorEntry
|
||||
{
|
||||
public CqrsRequestInvokerDescriptorEntry(Type requestType, Type responseType, CqrsRequestInvokerDescriptor descriptor)
|
||||
{
|
||||
RequestType = requestType;
|
||||
ResponseType = responseType;
|
||||
Descriptor = descriptor;
|
||||
}
|
||||
|
||||
public Type RequestType { get; }
|
||||
|
||||
public Type ResponseType { get; }
|
||||
|
||||
public CqrsRequestInvokerDescriptor Descriptor { get; }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
|
||||
public sealed class CqrsHandlerRegistryAttribute : Attribute
|
||||
{
|
||||
public CqrsHandlerRegistryAttribute(Type registryType) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
|
||||
public sealed record VisibleRequest(string Value) : IRequest<string>;
|
||||
|
||||
public sealed class VisibleHandler : IRequestHandler<VisibleRequest, string>
|
||||
{
|
||||
public ValueTask<string> Handle(VisibleRequest request, CancellationToken cancellationToken)
|
||||
{
|
||||
return ValueTask.FromResult(request.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
/// 验证生成器会为当前程序集中的 request、notification 和 stream 处理器生成稳定顺序的注册器。
|
||||
/// </summary>
|
||||
@ -2240,6 +2341,55 @@ public class CqrsHandlerRegistryGeneratorTests
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证当 runtime 暴露 request invoker provider 契约时,生成器会让 generated registry 同时发射
|
||||
/// request invoker 描述符与对应的开放静态 invoker 方法。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available()
|
||||
{
|
||||
var execution = ExecuteGenerator(RequestInvokerProviderSource);
|
||||
var inputCompilationErrors = execution.InputCompilationDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
var generatedCompilationErrors = execution.GeneratedCompilationDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
var generatorErrors = execution.GeneratorDiagnostics
|
||||
.Where(static diagnostic => diagnostic.Severity == DiagnosticSeverity.Error)
|
||||
.ToArray();
|
||||
var generatedSource = execution.GeneratedSources[0].content;
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(inputCompilationErrors, Is.Empty);
|
||||
Assert.That(generatedCompilationErrors, Is.Empty);
|
||||
Assert.That(generatorErrors, Is.Empty);
|
||||
Assert.That(execution.GeneratedSources, Has.Length.EqualTo(1));
|
||||
Assert.That(execution.GeneratedSources[0].filename, Is.EqualTo("CqrsHandlerRegistry.g.cs"));
|
||||
Assert.That(
|
||||
generatedSource,
|
||||
Does.Contain(
|
||||
"internal sealed class __GFrameworkGeneratedCqrsHandlerRegistry : global::GFramework.Cqrs.ICqrsHandlerRegistry, global::GFramework.Cqrs.ICqrsRequestInvokerProvider, global::GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors"));
|
||||
Assert.That(
|
||||
generatedSource,
|
||||
Does.Contain(
|
||||
"new global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(typeof(global::TestApp.VisibleRequest), typeof(string),"));
|
||||
Assert.That(
|
||||
generatedSource,
|
||||
Does.Contain(
|
||||
"new global::GFramework.Cqrs.CqrsRequestInvokerDescriptor(typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<global::TestApp.VisibleRequest, string>), typeof(__GFrameworkGeneratedCqrsHandlerRegistry).GetMethod(nameof(InvokeRequestHandler0), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!)"));
|
||||
Assert.That(
|
||||
generatedSource,
|
||||
Does.Contain(
|
||||
"private static global::System.Threading.Tasks.ValueTask<string> InvokeRequestHandler0(object handler, object request, global::System.Threading.CancellationToken cancellationToken)"));
|
||||
Assert.That(
|
||||
generatedSource,
|
||||
Does.Contain(
|
||||
"public global::System.Collections.Generic.IReadOnlyList<global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> GetDescriptors()"));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证日志字符串转义会覆盖换行、反斜杠和双引号,避免生成代码中的字符串字面量被意外截断。
|
||||
/// </summary>
|
||||
|
||||
@ -7,7 +7,7 @@ CQRS 迁移与收敛。
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`CQRS-REWRITE-RP-066`
|
||||
- 恢复点编号:`CQRS-REWRITE-RP-067`
|
||||
- 当前阶段:`Phase 8`
|
||||
- 当前焦点:
|
||||
- 已完成一轮 `CQRS vs Mediator` 只读评估归档,结论已沉淀到 `archive/todos/cqrs-vs-mediator-assessment-rp063.md`
|
||||
@ -40,6 +40,15 @@ CQRS 迁移与收敛。
|
||||
- `GFramework.Core.Abstractions/README.md`、`docs/zh-CN/abstractions/core-abstractions.md` 与
|
||||
`docs/zh-CN/core/cqrs.md` 现已明确:旧命名空间下的 `ICqrsRuntime` 仅作为 compatibility alias 保留,
|
||||
新代码应直接依赖 `GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime`
|
||||
- 已完成一轮 `dispatch/invoker` 生成前移的最小 request 切片:
|
||||
- `GFramework.Cqrs` 新增 `ICqrsRequestInvokerProvider`、`IEnumeratesCqrsRequestInvokerDescriptors`、
|
||||
`CqrsRequestInvokerDescriptor` 与 `CqrsRequestInvokerDescriptorEntry`
|
||||
- generated registry 若实现 request invoker provider 契约,`CqrsHandlerRegistrar` 现会在激活 registry 后把 provider 注册进容器,
|
||||
并把 provider 枚举出的 request invoker 描述符写入 dispatcher 的进程级弱缓存
|
||||
- `CqrsDispatcher` 现会在首次创建 request dispatch binding 时优先命中 generated request invoker 描述符;
|
||||
未命中时仍回退到既有 `MakeGenericMethod + Delegate.CreateDelegate` 路径
|
||||
- `GFramework.Cqrs.Tests` 已补充 `CqrsGeneratedRequestInvokerProviderTests`,锁定 registrar 接线和 dispatcher 消费 generated invoker 的最小语义
|
||||
- `GFramework.SourceGenerators.Tests` 已补充 generator 回归,锁定当 runtime 暴露新契约时,generated registry 会额外发射 request invoker provider 成员与 invoker 方法
|
||||
- 已将 mixed fallback 场景进一步收敛:当 runtime 允许同一程序集声明多个 `CqrsReflectionFallbackAttribute` 实例时,generator 现会把可直接引用的 fallback handlers 与仅能按名称恢复的 fallback handlers 拆分发射
|
||||
- `CqrsReflectionFallbackAttribute` 现允许多实例,以承载 `Type[]` 与字符串 fallback 元数据的组合输出
|
||||
- 已将 generator 的程序集级 fallback 元数据进一步收敛:当全部 fallback handlers 都可直接引用且 runtime 暴露 `params Type[]` 合同时,生成器现优先发射 `typeof(...)` 形式的 fallback 元数据
|
||||
@ -228,6 +237,15 @@ CQRS 迁移与收敛。
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
|
||||
- 结果:通过
|
||||
- 备注:`0 warning / 0 error`;确认 legacy alias helper 收敛与文档更新未引入 `GFramework.Core` 模块构建告警
|
||||
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsHandlerRegistrarTests|FullyQualifiedName~CqrsDispatcherCacheTests"`
|
||||
- 结果:通过
|
||||
- 备注:`22/22` 通过;确认 generated request invoker provider 的 registrar 接线、dispatcher 消费与现有 request/notification/stream cache 语义未回归
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"`
|
||||
- 结果:通过
|
||||
- 备注:`1/1` 通过;锁定 generator 会在 runtime 合同可用时发射 request invoker provider 成员
|
||||
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
|
||||
- 结果:通过
|
||||
- 备注:`0 warning / 0 error`;确认 request invoker provider seam 与 dispatcher/registrar 接线未引入新增构建告警
|
||||
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
|
||||
- 结果:通过
|
||||
- 备注:`0 warning / 0 error`;确认三份 `Mediator` 命名收口后的 CQRS 测试项目构建仍然干净
|
||||
@ -238,5 +256,5 @@ CQRS 迁移与收敛。
|
||||
## 下一步
|
||||
|
||||
1. 基于已落地的 notification publisher seam,评估是否需要第二阶段公开配置面、并行 publisher 或 telemetry decorator
|
||||
2. 继续以 `dispatch/invoker` 生成前移为优先对象,优先尝试 “generated request invoker provider + dispatcher fallback” 这条最小实现切片
|
||||
2. 基于已落地的 request invoker provider,评估是否继续把 notification / stream 的 invoker 也前移,或先补 provider 发现/诊断与文档入口
|
||||
3. 单独规划旧 `Command` / `Query` API 的收口顺序;`LegacyICqrsRuntime` compatibility slice 已收口到显式 helper 与专门测试,可暂时移出最高优先级
|
||||
|
||||
@ -2,6 +2,39 @@
|
||||
|
||||
## 2026-04-30
|
||||
|
||||
### 阶段:generated request invoker provider 最小落地(CQRS-REWRITE-RP-067)
|
||||
|
||||
- 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main`
|
||||
- 在 `RP-066` 提交后复算 branch diff,相对 `origin/main` 增长到 `22 files`,仍明显低于 `50 files` stop condition,因此继续下一批
|
||||
- 本轮 critical path 保持在主线程,本地完成 `dispatch/invoker` 生成前移的最小 request 切片;尝试委派 source-generator 测试给 worker 时因 subagent 名额已满失败,因此主线程直接接管该测试修改
|
||||
- 本轮关键设计调整:
|
||||
- 不按 `requestType.Assembly` 做 provider 发现,避免“请求定义在 A、handler 与 generated registry 在 B”时漏掉 generated invoker
|
||||
- generated registry 若实现 `ICqrsRequestInvokerProvider`,registrar 会在激活 registry 后把 provider 注册进容器,并通过 `IEnumeratesCqrsRequestInvokerDescriptors` 把描述符写入 dispatcher 的进程级弱缓存
|
||||
- dispatcher 首次创建 request dispatch binding 时只按 `requestType + responseType` 读取静态弱缓存,不依赖具体容器实例;未命中时仍走既有反射创建路径
|
||||
- 已完成实现:
|
||||
- `GFramework.Cqrs` 新增 `ICqrsRequestInvokerProvider`、`IEnumeratesCqrsRequestInvokerDescriptors`、
|
||||
`CqrsRequestInvokerDescriptor` 与 `CqrsRequestInvokerDescriptorEntry`
|
||||
- `CqrsHandlerRegistrar` 现会识别 generated registry 的 request invoker provider 能力,并登记 provider 与 request invoker 描述符
|
||||
- `CqrsDispatcher` 新增 generated request invoker 弱缓存,并在 request binding 创建时优先消费该元数据
|
||||
- `CqrsHandlerRegistryGenerator` 在 runtime 合同可用时,会让 generated registry 额外实现 request invoker provider 相关接口,并发射 descriptor 列表、`TryGetDescriptor(...)`、`GetDescriptors()` 与 request invoker 静态方法
|
||||
- 已补充测试:
|
||||
- `CqrsGeneratedRequestInvokerProviderTests` 锁定 registrar 会注册 generated request invoker provider,且 dispatcher 走 generated invoker 后会返回 `generated:` 前缀结果
|
||||
- `CqrsHandlerRegistryGeneratorTests` 锁定 generated source 会包含 request invoker provider 接口、descriptor 条目与 `InvokeRequestHandler0(...)` 方法
|
||||
|
||||
### 验证
|
||||
|
||||
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests|FullyQualifiedName~CqrsHandlerRegistrarTests|FullyQualifiedName~CqrsDispatcherCacheTests"`
|
||||
- 结果:通过,`22/22` passed
|
||||
- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"`
|
||||
- 结果:通过,`1/1` passed
|
||||
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
|
||||
### 当前下一步
|
||||
|
||||
1. 评估 notification / stream invoker 是否值得沿同一 provider 模式继续前移,或先补 request provider 的公开说明与诊断语义
|
||||
2. 继续在保持 branch diff 低于阈值的前提下推进下一批;当前相对 `origin/main` 的 branch diff 为 `22 files`
|
||||
|
||||
### 阶段:LegacyICqrsRuntime compatibility slice 收口(CQRS-REWRITE-RP-066)
|
||||
|
||||
- 继续按 `gframework-batch-boot 50` 执行,基线仍为本地现有 `origin/main`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user