fix(dispatcher): throw on ambiguous handlers instead of silent .First()

Multiple handlers for the same request were previously resolved via .First(),
with ordering depending on Type.GetInterfaces() order (non-deterministic).
Now ScalarRequestInvoker/StreamRequestInvoker throw InvalidOperationException
listing the candidate handlers when more than one matches.
This commit is contained in:
Louis Seubert 2026-07-12 19:18:21 +02:00
commit f79e8780f7
Signed by: louis9902
GPG key ID: 4B9DB28F826553BD
7 changed files with 134 additions and 9 deletions

View file

@ -7,6 +7,26 @@ namespace Geekeey.Request.Dispatcher.Tests;
public class StreamDispatcherTests
{
[Test]
public async Task I_can_see_it_throw_on_ambiguous_stream_handlers()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(DuplicateStreamHandlerA))
.Add(typeof(DuplicateStreamHandlerB)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new DuplicateStreamRequest();
var ex = await Assert.That(async () => await dispatcher.DispatchAsync(request).ToListAsync()).Throws<InvalidOperationException>();
using (Assert.Multiple())
{
await Assert.That(ex?.Message).Contains(nameof(DuplicateStreamHandlerA));
await Assert.That(ex?.Message).Contains(nameof(DuplicateStreamHandlerB));
}
}
[Test]
public async Task I_can_dispatch_a_request_async_with_an_open_generic_handler()
{
@ -121,13 +141,18 @@ public class StreamDispatcherTests
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new InterfaceInheritedStreamRequest { Name = "Constrained" };
var results = await dispatcher.DispatchAsync(request).ToListAsync();
// Both InterfaceInheritedStreamHandler and InterfaceConstrainedStreamHandler could match.
// InterfaceInheritedStreamHandler is a concrete match for InterfaceInheritedStreamRequest.
// InterfaceConstrainedStreamHandler is an open generic match.
// Currently Dispatcher checks concrete handlers first.
await Assert.That(results).IsEquivalentTo(["Constrained-InterfaceHandled-0", "Constrained-InterfaceHandled-1"]);
// Ambiguity is no longer resolved silently with .First(); it throws instead.
var ex = await Assert.That(async () => await dispatcher.DispatchAsync(request).ToListAsync()).Throws<InvalidOperationException>();
using (Assert.Multiple())
{
await Assert.That(ex?.Message).Contains(nameof(InterfaceInheritedStreamHandler));
await Assert.That(ex?.Message).Contains("InterfaceConstrainedStreamHandler");
}
}
[Test]