mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-12 13:14:30 +08:00
fix(cqrs): 硬化 generated invoker descriptor 契约
- 修复 generated request/stream invoker descriptor 枚举阶段对异常、重复 pair 与不一致元数据的防守行为 - 补充 generated invoker provider descriptor 边界回归测试,锁定回退与首条生效语义
This commit is contained in:
parent
8990749d91
commit
7fa9d5ff17
@ -448,6 +448,126 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
|||||||
Assert.That(results, Is.EqualTo([3, 4]));
|
Assert.That(results, Is.EqualTo([3, 4]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证当 generated request invoker provider 的 descriptor 枚举抛出异常时,
|
||||||
|
/// registrar 会跳过 generated descriptor 预热并回退到反射路径。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task SendAsync_Should_Fall_Back_To_Runtime_Path_When_Request_Descriptor_Enumeration_Throws()
|
||||||
|
{
|
||||||
|
var generatedAssembly = CreateGeneratedAssembly(
|
||||||
|
typeof(ThrowingEnumeratingRequestInvokerProviderRegistry),
|
||||||
|
"GFramework.Cqrs.Tests.Cqrs.ThrowingEnumeratingRequestInvokerAssembly, Version=1.0.0.0");
|
||||||
|
var container = new MicrosoftDiContainer();
|
||||||
|
|
||||||
|
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||||
|
container.Freeze();
|
||||||
|
|
||||||
|
var context = new ArchitectureContext(container);
|
||||||
|
var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false);
|
||||||
|
Assert.That(response, Is.EqualTo("runtime:payload"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证当 generated stream invoker provider 的 descriptor 枚举抛出异常时,
|
||||||
|
/// registrar 会跳过 generated descriptor 预热并回退到反射建流路径。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task CreateStream_Should_Fall_Back_To_Runtime_Path_When_Stream_Descriptor_Enumeration_Throws()
|
||||||
|
{
|
||||||
|
var generatedAssembly = CreateGeneratedAssembly(
|
||||||
|
typeof(ThrowingEnumeratingStreamInvokerProviderRegistry),
|
||||||
|
"GFramework.Cqrs.Tests.Cqrs.ThrowingEnumeratingStreamInvokerAssembly, Version=1.0.0.0");
|
||||||
|
var container = new MicrosoftDiContainer();
|
||||||
|
|
||||||
|
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||||
|
container.Freeze();
|
||||||
|
|
||||||
|
var context = new ArchitectureContext(container);
|
||||||
|
var results = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false);
|
||||||
|
Assert.That(results, Is.EqualTo([3, 4]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证当 request descriptor 枚举返回重复 request-response pair 时,
|
||||||
|
/// registrar 会稳定保留首个有效描述符,并忽略后续重复项。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task SendAsync_Should_Use_First_Generated_Request_Descriptor_When_Duplicates_Are_Enumerated()
|
||||||
|
{
|
||||||
|
var generatedAssembly = CreateGeneratedAssembly(
|
||||||
|
typeof(DuplicateEnumeratingRequestInvokerProviderRegistry),
|
||||||
|
"GFramework.Cqrs.Tests.Cqrs.DuplicateEnumeratingRequestInvokerAssembly, Version=1.0.0.0");
|
||||||
|
var container = new MicrosoftDiContainer();
|
||||||
|
|
||||||
|
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||||
|
container.Freeze();
|
||||||
|
|
||||||
|
var context = new ArchitectureContext(container);
|
||||||
|
var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false);
|
||||||
|
Assert.That(response, Is.EqualTo("generated:payload"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证当 stream descriptor 枚举返回重复 request-response pair 时,
|
||||||
|
/// registrar 会稳定保留首个有效描述符,并忽略后续重复项。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task CreateStream_Should_Use_First_Generated_Stream_Descriptor_When_Duplicates_Are_Enumerated()
|
||||||
|
{
|
||||||
|
var generatedAssembly = CreateGeneratedAssembly(
|
||||||
|
typeof(DuplicateEnumeratingStreamInvokerProviderRegistry),
|
||||||
|
"GFramework.Cqrs.Tests.Cqrs.DuplicateEnumeratingStreamInvokerAssembly, Version=1.0.0.0");
|
||||||
|
var container = new MicrosoftDiContainer();
|
||||||
|
|
||||||
|
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||||
|
container.Freeze();
|
||||||
|
|
||||||
|
var context = new ArchitectureContext(container);
|
||||||
|
var results = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false);
|
||||||
|
Assert.That(results, Is.EqualTo([30, 31]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证当 request descriptor 枚举项与 provider 的 TryGetDescriptor 结果不一致时,
|
||||||
|
/// registrar 会忽略该坏 descriptor,并继续回退到反射路径。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task SendAsync_Should_Fall_Back_To_Runtime_Path_When_Enumerated_Request_Descriptor_Does_Not_Match_Provider()
|
||||||
|
{
|
||||||
|
var generatedAssembly = CreateGeneratedAssembly(
|
||||||
|
typeof(MismatchedEnumeratingRequestInvokerProviderRegistry),
|
||||||
|
"GFramework.Cqrs.Tests.Cqrs.MismatchedEnumeratingRequestInvokerAssembly, Version=1.0.0.0");
|
||||||
|
var container = new MicrosoftDiContainer();
|
||||||
|
|
||||||
|
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||||
|
container.Freeze();
|
||||||
|
|
||||||
|
var context = new ArchitectureContext(container);
|
||||||
|
var response = await context.SendRequestAsync(new GeneratedRequestInvokerRequest("payload")).ConfigureAwait(false);
|
||||||
|
Assert.That(response, Is.EqualTo("runtime:payload"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证当 stream descriptor 枚举项与 provider 的 TryGetDescriptor 结果不一致时,
|
||||||
|
/// registrar 会忽略该坏 descriptor,并继续回退到反射建流路径。
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public async Task CreateStream_Should_Fall_Back_To_Runtime_Path_When_Enumerated_Stream_Descriptor_Does_Not_Match_Provider()
|
||||||
|
{
|
||||||
|
var generatedAssembly = CreateGeneratedAssembly(
|
||||||
|
typeof(MismatchedEnumeratingStreamInvokerProviderRegistry),
|
||||||
|
"GFramework.Cqrs.Tests.Cqrs.MismatchedEnumeratingStreamInvokerAssembly, Version=1.0.0.0");
|
||||||
|
var container = new MicrosoftDiContainer();
|
||||||
|
|
||||||
|
CqrsTestRuntime.RegisterHandlers(container, generatedAssembly.Object);
|
||||||
|
container.Freeze();
|
||||||
|
|
||||||
|
var context = new ArchitectureContext(container);
|
||||||
|
var results = await DrainAsync(context.CreateStream(new GeneratedStreamInvokerRequest(3))).ConfigureAwait(false);
|
||||||
|
Assert.That(results, Is.EqualTo([3, 4]));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 模拟返回实例 request invoker 方法的 generated registry。
|
/// 模拟返回实例 request invoker 方法的 generated registry。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -860,6 +980,382 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模拟 descriptor 枚举阶段抛出异常的 request invoker provider。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class ThrowingEnumeratingRequestInvokerProviderRegistry :
|
||||||
|
ICqrsHandlerRegistry,
|
||||||
|
ICqrsRequestInvokerProvider,
|
||||||
|
IEnumeratesCqrsRequestInvokerDescriptors
|
||||||
|
{
|
||||||
|
private static readonly CqrsRequestInvokerDescriptor Descriptor = new(
|
||||||
|
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||||
|
typeof(GeneratedRequestInvokerProviderRegistry).GetMethod(
|
||||||
|
"InvokeGenerated",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Register(IServiceCollection services, ILogger logger)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
|
ArgumentNullException.ThrowIfNull(logger);
|
||||||
|
|
||||||
|
services.AddTransient(
|
||||||
|
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||||
|
typeof(GeneratedRequestInvokerRequestHandler));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool TryGetDescriptor(
|
||||||
|
Type requestType,
|
||||||
|
Type responseType,
|
||||||
|
out CqrsRequestInvokerDescriptor? descriptor)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(requestType);
|
||||||
|
ArgumentNullException.ThrowIfNull(responseType);
|
||||||
|
|
||||||
|
if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string))
|
||||||
|
{
|
||||||
|
descriptor = Descriptor;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptor = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors()
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("request descriptors failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模拟 descriptor 枚举阶段抛出异常的 stream invoker provider。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class ThrowingEnumeratingStreamInvokerProviderRegistry :
|
||||||
|
ICqrsHandlerRegistry,
|
||||||
|
ICqrsStreamInvokerProvider,
|
||||||
|
IEnumeratesCqrsStreamInvokerDescriptors
|
||||||
|
{
|
||||||
|
private static readonly CqrsStreamInvokerDescriptor Descriptor = new(
|
||||||
|
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||||
|
typeof(GeneratedStreamInvokerProviderRegistry).GetMethod(
|
||||||
|
"InvokeGenerated",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Register(IServiceCollection services, ILogger logger)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
|
ArgumentNullException.ThrowIfNull(logger);
|
||||||
|
|
||||||
|
services.AddTransient(
|
||||||
|
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||||
|
typeof(GeneratedStreamInvokerRequestHandler));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool TryGetDescriptor(
|
||||||
|
Type requestType,
|
||||||
|
Type responseType,
|
||||||
|
out CqrsStreamInvokerDescriptor? descriptor)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(requestType);
|
||||||
|
ArgumentNullException.ThrowIfNull(responseType);
|
||||||
|
|
||||||
|
if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int))
|
||||||
|
{
|
||||||
|
descriptor = Descriptor;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptor = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors()
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("stream descriptors failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模拟返回重复 request descriptor 条目的 generated registry。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class DuplicateEnumeratingRequestInvokerProviderRegistry :
|
||||||
|
ICqrsHandlerRegistry,
|
||||||
|
ICqrsRequestInvokerProvider,
|
||||||
|
IEnumeratesCqrsRequestInvokerDescriptors
|
||||||
|
{
|
||||||
|
private static readonly CqrsRequestInvokerDescriptor PrimaryDescriptor = new(
|
||||||
|
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||||
|
typeof(GeneratedRequestInvokerProviderRegistry).GetMethod(
|
||||||
|
"InvokeGenerated",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||||
|
|
||||||
|
private static readonly CqrsRequestInvokerDescriptor SecondaryDescriptor = new(
|
||||||
|
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||||
|
typeof(DuplicateEnumeratingRequestInvokerProviderRegistry).GetMethod(
|
||||||
|
nameof(InvokeAlternativeGenerated),
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Register(IServiceCollection services, ILogger logger)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
|
ArgumentNullException.ThrowIfNull(logger);
|
||||||
|
|
||||||
|
services.AddTransient(
|
||||||
|
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||||
|
typeof(GeneratedRequestInvokerRequestHandler));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool TryGetDescriptor(
|
||||||
|
Type requestType,
|
||||||
|
Type responseType,
|
||||||
|
out CqrsRequestInvokerDescriptor? descriptor)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(requestType);
|
||||||
|
ArgumentNullException.ThrowIfNull(responseType);
|
||||||
|
|
||||||
|
if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string))
|
||||||
|
{
|
||||||
|
descriptor = PrimaryDescriptor;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptor = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors()
|
||||||
|
{
|
||||||
|
return
|
||||||
|
[
|
||||||
|
new CqrsRequestInvokerDescriptorEntry(typeof(GeneratedRequestInvokerRequest), typeof(string), PrimaryDescriptor),
|
||||||
|
new CqrsRequestInvokerDescriptorEntry(typeof(GeneratedRequestInvokerRequest), typeof(string), SecondaryDescriptor)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ValueTask<string> InvokeAlternativeGenerated(
|
||||||
|
object handler,
|
||||||
|
object request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return ValueTask.FromResult("duplicate:payload");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模拟返回重复 stream descriptor 条目的 generated registry。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class DuplicateEnumeratingStreamInvokerProviderRegistry :
|
||||||
|
ICqrsHandlerRegistry,
|
||||||
|
ICqrsStreamInvokerProvider,
|
||||||
|
IEnumeratesCqrsStreamInvokerDescriptors
|
||||||
|
{
|
||||||
|
private static readonly CqrsStreamInvokerDescriptor PrimaryDescriptor = new(
|
||||||
|
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||||
|
typeof(GeneratedStreamInvokerProviderRegistry).GetMethod(
|
||||||
|
"InvokeGenerated",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||||
|
|
||||||
|
private static readonly CqrsStreamInvokerDescriptor SecondaryDescriptor = new(
|
||||||
|
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||||
|
typeof(DuplicateEnumeratingStreamInvokerProviderRegistry).GetMethod(
|
||||||
|
nameof(InvokeAlternativeGenerated),
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Register(IServiceCollection services, ILogger logger)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
|
ArgumentNullException.ThrowIfNull(logger);
|
||||||
|
|
||||||
|
services.AddTransient(
|
||||||
|
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||||
|
typeof(GeneratedStreamInvokerRequestHandler));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool TryGetDescriptor(
|
||||||
|
Type requestType,
|
||||||
|
Type responseType,
|
||||||
|
out CqrsStreamInvokerDescriptor? descriptor)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(requestType);
|
||||||
|
ArgumentNullException.ThrowIfNull(responseType);
|
||||||
|
|
||||||
|
if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int))
|
||||||
|
{
|
||||||
|
descriptor = PrimaryDescriptor;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptor = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors()
|
||||||
|
{
|
||||||
|
return
|
||||||
|
[
|
||||||
|
new CqrsStreamInvokerDescriptorEntry(typeof(GeneratedStreamInvokerRequest), typeof(int), PrimaryDescriptor),
|
||||||
|
new CqrsStreamInvokerDescriptorEntry(typeof(GeneratedStreamInvokerRequest), typeof(int), SecondaryDescriptor)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object InvokeAlternativeGenerated(object handler, object request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return new[] { 900, 901 }.ToAsyncEnumerable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模拟枚举出的 request descriptor 与 provider 显式查询结果不一致的 generated registry。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class MismatchedEnumeratingRequestInvokerProviderRegistry :
|
||||||
|
ICqrsHandlerRegistry,
|
||||||
|
ICqrsRequestInvokerProvider,
|
||||||
|
IEnumeratesCqrsRequestInvokerDescriptors
|
||||||
|
{
|
||||||
|
private static readonly CqrsRequestInvokerDescriptor ProviderDescriptor = new(
|
||||||
|
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||||
|
typeof(GeneratedRequestInvokerProviderRegistry).GetMethod(
|
||||||
|
"InvokeGenerated",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||||
|
|
||||||
|
private static readonly CqrsRequestInvokerDescriptor EnumeratedDescriptor = new(
|
||||||
|
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||||
|
typeof(MismatchedEnumeratingRequestInvokerProviderRegistry).GetMethod(
|
||||||
|
nameof(InvokeAlternativeGenerated),
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Register(IServiceCollection services, ILogger logger)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
|
ArgumentNullException.ThrowIfNull(logger);
|
||||||
|
|
||||||
|
services.AddTransient(
|
||||||
|
typeof(IRequestHandler<GeneratedRequestInvokerRequest, string>),
|
||||||
|
typeof(GeneratedRequestInvokerRequestHandler));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool TryGetDescriptor(
|
||||||
|
Type requestType,
|
||||||
|
Type responseType,
|
||||||
|
out CqrsRequestInvokerDescriptor? descriptor)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(requestType);
|
||||||
|
ArgumentNullException.ThrowIfNull(responseType);
|
||||||
|
|
||||||
|
if (requestType == typeof(GeneratedRequestInvokerRequest) && responseType == typeof(string))
|
||||||
|
{
|
||||||
|
descriptor = ProviderDescriptor;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptor = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyList<CqrsRequestInvokerDescriptorEntry> GetDescriptors()
|
||||||
|
{
|
||||||
|
return
|
||||||
|
[
|
||||||
|
new CqrsRequestInvokerDescriptorEntry(
|
||||||
|
typeof(GeneratedRequestInvokerRequest),
|
||||||
|
typeof(string),
|
||||||
|
EnumeratedDescriptor)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ValueTask<string> InvokeAlternativeGenerated(
|
||||||
|
object handler,
|
||||||
|
object request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return ValueTask.FromResult("mismatched:payload");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模拟枚举出的 stream descriptor 与 provider 显式查询结果不一致的 generated registry。
|
||||||
|
/// </summary>
|
||||||
|
private sealed class MismatchedEnumeratingStreamInvokerProviderRegistry :
|
||||||
|
ICqrsHandlerRegistry,
|
||||||
|
ICqrsStreamInvokerProvider,
|
||||||
|
IEnumeratesCqrsStreamInvokerDescriptors
|
||||||
|
{
|
||||||
|
private static readonly CqrsStreamInvokerDescriptor ProviderDescriptor = new(
|
||||||
|
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||||
|
typeof(GeneratedStreamInvokerProviderRegistry).GetMethod(
|
||||||
|
"InvokeGenerated",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||||
|
|
||||||
|
private static readonly CqrsStreamInvokerDescriptor EnumeratedDescriptor = new(
|
||||||
|
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||||
|
typeof(MismatchedEnumeratingStreamInvokerProviderRegistry).GetMethod(
|
||||||
|
nameof(InvokeAlternativeGenerated),
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static)!);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Register(IServiceCollection services, ILogger logger)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(services);
|
||||||
|
ArgumentNullException.ThrowIfNull(logger);
|
||||||
|
|
||||||
|
services.AddTransient(
|
||||||
|
typeof(IStreamRequestHandler<GeneratedStreamInvokerRequest, int>),
|
||||||
|
typeof(GeneratedStreamInvokerRequestHandler));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool TryGetDescriptor(
|
||||||
|
Type requestType,
|
||||||
|
Type responseType,
|
||||||
|
out CqrsStreamInvokerDescriptor? descriptor)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(requestType);
|
||||||
|
ArgumentNullException.ThrowIfNull(responseType);
|
||||||
|
|
||||||
|
if (requestType == typeof(GeneratedStreamInvokerRequest) && responseType == typeof(int))
|
||||||
|
{
|
||||||
|
descriptor = ProviderDescriptor;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptor = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IReadOnlyList<CqrsStreamInvokerDescriptorEntry> GetDescriptors()
|
||||||
|
{
|
||||||
|
return
|
||||||
|
[
|
||||||
|
new CqrsStreamInvokerDescriptorEntry(
|
||||||
|
typeof(GeneratedStreamInvokerRequest),
|
||||||
|
typeof(int),
|
||||||
|
EnumeratedDescriptor)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object InvokeAlternativeGenerated(object handler, object request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return new[] { 700, 701 }.ToAsyncEnumerable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 创建带有 generated request invoker registry 元数据的程序集替身。
|
/// 创建带有 generated request invoker registry 元数据的程序集替身。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -16,6 +16,13 @@ namespace GFramework.Cqrs.Internal;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal static class CqrsHandlerRegistrar
|
internal static class CqrsHandlerRegistrar
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 描述 generated invoker descriptor 在 registrar 预热阶段使用的 request/response 类型对键。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="RequestType">请求运行时类型。</param>
|
||||||
|
/// <param name="ResponseType">响应运行时类型。</param>
|
||||||
|
private readonly record struct InvokerDescriptorKey(Type RequestType, Type ResponseType);
|
||||||
|
|
||||||
// 卸载安全的进程级缓存:程序集元数据只按弱键复用。
|
// 卸载安全的进程级缓存:程序集元数据只按弱键复用。
|
||||||
// 若程序集来自 collectible AssemblyLoadContext,被回收后会重新分析,而不会被静态缓存永久钉住。
|
// 若程序集来自 collectible AssemblyLoadContext,被回收后会重新分析,而不会被静态缓存永久钉住。
|
||||||
private static readonly WeakKeyCache<Assembly, AssemblyRegistrationMetadata> AssemblyMetadataCache =
|
private static readonly WeakKeyCache<Assembly, AssemblyRegistrationMetadata> AssemblyMetadataCache =
|
||||||
@ -321,8 +328,48 @@ internal static class CqrsHandlerRegistrar
|
|||||||
if (provider is not IEnumeratesCqrsRequestInvokerDescriptors descriptorSource)
|
if (provider is not IEnumeratesCqrsRequestInvokerDescriptors descriptorSource)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach (var descriptorEntry in descriptorSource.GetDescriptors())
|
IReadOnlyList<CqrsRequestInvokerDescriptorEntry>? descriptors;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
|
descriptors = descriptorSource.GetDescriptors();
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.Warn(
|
||||||
|
$"Failed to enumerate generated CQRS request invoker descriptors from provider {provider.GetType().FullName} in assembly {assemblyName}. Falling back to runtime reflection for request invokers: {exception.Message}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descriptors is null)
|
||||||
|
{
|
||||||
|
logger.Warn(
|
||||||
|
$"Ignoring generated CQRS request invoker descriptors from provider {provider.GetType().FullName} in assembly {assemblyName} because GetDescriptors() returned null.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var registeredKeys = new HashSet<InvokerDescriptorKey>();
|
||||||
|
foreach (var descriptorEntry in descriptors)
|
||||||
|
{
|
||||||
|
if (descriptorEntry is null)
|
||||||
|
{
|
||||||
|
logger.Warn(
|
||||||
|
$"Ignoring null generated CQRS request invoker descriptor entry from provider {provider.GetType().FullName} in assembly {assemblyName}.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var descriptorKey = new InvokerDescriptorKey(
|
||||||
|
descriptorEntry.RequestType,
|
||||||
|
descriptorEntry.ResponseType);
|
||||||
|
if (!registeredKeys.Add(descriptorKey))
|
||||||
|
{
|
||||||
|
logger.Warn(
|
||||||
|
$"Ignoring duplicate generated CQRS request invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName}.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryValidateEnumeratedRequestInvokerDescriptor(provider, descriptorEntry, assemblyName, logger))
|
||||||
|
continue;
|
||||||
|
|
||||||
CqrsDispatcher.RegisterGeneratedRequestInvokerDescriptor(
|
CqrsDispatcher.RegisterGeneratedRequestInvokerDescriptor(
|
||||||
descriptorEntry.RequestType,
|
descriptorEntry.RequestType,
|
||||||
descriptorEntry.ResponseType,
|
descriptorEntry.ResponseType,
|
||||||
@ -376,8 +423,48 @@ internal static class CqrsHandlerRegistrar
|
|||||||
if (provider is not IEnumeratesCqrsStreamInvokerDescriptors descriptorSource)
|
if (provider is not IEnumeratesCqrsStreamInvokerDescriptors descriptorSource)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
foreach (var descriptorEntry in descriptorSource.GetDescriptors())
|
IReadOnlyList<CqrsStreamInvokerDescriptorEntry>? descriptors;
|
||||||
|
try
|
||||||
{
|
{
|
||||||
|
descriptors = descriptorSource.GetDescriptors();
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.Warn(
|
||||||
|
$"Failed to enumerate generated CQRS stream invoker descriptors from provider {provider.GetType().FullName} in assembly {assemblyName}. Falling back to runtime reflection for stream invokers: {exception.Message}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descriptors is null)
|
||||||
|
{
|
||||||
|
logger.Warn(
|
||||||
|
$"Ignoring generated CQRS stream invoker descriptors from provider {provider.GetType().FullName} in assembly {assemblyName} because GetDescriptors() returned null.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var registeredKeys = new HashSet<InvokerDescriptorKey>();
|
||||||
|
foreach (var descriptorEntry in descriptors)
|
||||||
|
{
|
||||||
|
if (descriptorEntry is null)
|
||||||
|
{
|
||||||
|
logger.Warn(
|
||||||
|
$"Ignoring null generated CQRS stream invoker descriptor entry from provider {provider.GetType().FullName} in assembly {assemblyName}.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var descriptorKey = new InvokerDescriptorKey(
|
||||||
|
descriptorEntry.RequestType,
|
||||||
|
descriptorEntry.ResponseType);
|
||||||
|
if (!registeredKeys.Add(descriptorKey))
|
||||||
|
{
|
||||||
|
logger.Warn(
|
||||||
|
$"Ignoring duplicate generated CQRS stream invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName}.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryValidateEnumeratedStreamInvokerDescriptor(provider, descriptorEntry, assemblyName, logger))
|
||||||
|
continue;
|
||||||
|
|
||||||
CqrsDispatcher.RegisterGeneratedStreamInvokerDescriptor(
|
CqrsDispatcher.RegisterGeneratedStreamInvokerDescriptor(
|
||||||
descriptorEntry.RequestType,
|
descriptorEntry.RequestType,
|
||||||
descriptorEntry.ResponseType,
|
descriptorEntry.ResponseType,
|
||||||
@ -387,6 +474,96 @@ internal static class CqrsHandlerRegistrar
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验 request descriptor 枚举项是否与 provider 的显式查询结果保持一致。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="provider">当前正在预热的 request invoker provider。</param>
|
||||||
|
/// <param name="descriptorEntry">当前枚举到的描述符条目。</param>
|
||||||
|
/// <param name="assemblyName">当前程序集的稳定名称。</param>
|
||||||
|
/// <param name="logger">日志记录器。</param>
|
||||||
|
/// <returns>当该枚举项可安全写入 dispatcher 缓存时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||||
|
private static bool TryValidateEnumeratedRequestInvokerDescriptor(
|
||||||
|
ICqrsRequestInvokerProvider provider,
|
||||||
|
CqrsRequestInvokerDescriptorEntry descriptorEntry,
|
||||||
|
string assemblyName,
|
||||||
|
ILogger logger)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!provider.TryGetDescriptor(
|
||||||
|
descriptorEntry.RequestType,
|
||||||
|
descriptorEntry.ResponseType,
|
||||||
|
out var resolvedDescriptor) ||
|
||||||
|
resolvedDescriptor is null)
|
||||||
|
{
|
||||||
|
logger.Warn(
|
||||||
|
$"Ignoring generated CQRS request invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName} because TryGetDescriptor did not return a matching descriptor.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ReferenceEquals(resolvedDescriptor.InvokerMethod, descriptorEntry.Descriptor.InvokerMethod) ||
|
||||||
|
resolvedDescriptor.HandlerType != descriptorEntry.Descriptor.HandlerType)
|
||||||
|
{
|
||||||
|
logger.Warn(
|
||||||
|
$"Ignoring generated CQRS request invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName} because the enumerated descriptor does not match TryGetDescriptor.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.Warn(
|
||||||
|
$"Ignoring generated CQRS request invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName} because TryGetDescriptor threw: {exception.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 校验 stream descriptor 枚举项是否与 provider 的显式查询结果保持一致。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="provider">当前正在预热的 stream invoker provider。</param>
|
||||||
|
/// <param name="descriptorEntry">当前枚举到的描述符条目。</param>
|
||||||
|
/// <param name="assemblyName">当前程序集的稳定名称。</param>
|
||||||
|
/// <param name="logger">日志记录器。</param>
|
||||||
|
/// <returns>当该枚举项可安全写入 dispatcher 缓存时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||||
|
private static bool TryValidateEnumeratedStreamInvokerDescriptor(
|
||||||
|
ICqrsStreamInvokerProvider provider,
|
||||||
|
CqrsStreamInvokerDescriptorEntry descriptorEntry,
|
||||||
|
string assemblyName,
|
||||||
|
ILogger logger)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!provider.TryGetDescriptor(
|
||||||
|
descriptorEntry.RequestType,
|
||||||
|
descriptorEntry.ResponseType,
|
||||||
|
out var resolvedDescriptor) ||
|
||||||
|
resolvedDescriptor is null)
|
||||||
|
{
|
||||||
|
logger.Warn(
|
||||||
|
$"Ignoring generated CQRS stream invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName} because TryGetDescriptor did not return a matching descriptor.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ReferenceEquals(resolvedDescriptor.InvokerMethod, descriptorEntry.Descriptor.InvokerMethod) ||
|
||||||
|
resolvedDescriptor.HandlerType != descriptorEntry.Descriptor.HandlerType)
|
||||||
|
{
|
||||||
|
logger.Warn(
|
||||||
|
$"Ignoring generated CQRS stream invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName} because the enumerated descriptor does not match TryGetDescriptor.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
logger.Warn(
|
||||||
|
$"Ignoring generated CQRS stream invoker descriptor for {descriptorEntry.RequestType.FullName} -> {descriptorEntry.ResponseType.FullName} from provider {provider.GetType().FullName} in assembly {assemblyName} because TryGetDescriptor threw: {exception.Message}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将 generated registry 的 fallback 元数据转换为统一的注册结果,并记录下一阶段是定向补扫还是整程序集扫描。
|
/// 将 generated registry 的 fallback 元数据转换为统一的注册结果,并记录下一阶段是定向补扫还是整程序集扫描。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user