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 a7d7e9fbaa
Signed by: louis9902
GPG key ID: 4B9DB28F826553BD
7 changed files with 134 additions and 9 deletions

View file

@ -116,7 +116,7 @@ internal sealed class ScalarDispatcherTests
}
[Test]
public async Task I_can_dispatch_a_request_async_with_an_interface_constrained_handler()
public async Task I_can_see_it_throw_on_ambiguous_concrete_and_constrained_scalar_handlers()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
@ -126,13 +126,18 @@ internal sealed class ScalarDispatcherTests
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new InterfaceInheritedScalarRequest { Name = "Constrained" };
var result = await dispatcher.DispatchAsync(request);
// Both InterfaceInheritedScalarHandler and InterfaceConstrainedScalarHandler could match.
// InterfaceInheritedScalarHandler is a concrete match for InterfaceInheritedScalarRequest.
// InterfaceConstrainedScalarHandler is an open generic match.
// Currently Dispatcher.SendAsync checks concrete handlers first.
await Assert.That(result).IsEquivalentTo("Constrained-InterfaceHandled");
// Ambiguity is no longer resolved silently with .First(); it throws instead.
var ex = await Assert.That(async () => await dispatcher.DispatchAsync(request)).Throws<InvalidOperationException>();
using (Assert.Multiple())
{
await Assert.That(ex?.Message).Contains(nameof(InterfaceInheritedScalarHandler));
await Assert.That(ex?.Message).Contains("InterfaceConstrainedScalarHandler");
}
}
[Test]
@ -234,6 +239,26 @@ internal sealed class ScalarDispatcherTests
}
}
[Test]
public async Task I_can_see_it_throw_on_ambiguous_scalar_handlers()
{
var sc = new ServiceCollection();
sc.AddRequestDispatcher(builder => builder
.Add(typeof(DuplicateScalarHandlerA))
.Add(typeof(DuplicateScalarHandlerB)));
var provider = sc.BuildServiceProvider();
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
var request = new DuplicateScalarRequest();
var ex = await Assert.That(async () => await dispatcher.DispatchAsync(request)).Throws<InvalidOperationException>();
using (Assert.Multiple())
{
await Assert.That(ex?.Message).Contains(nameof(DuplicateScalarHandlerA));
await Assert.That(ex?.Message).Contains(nameof(DuplicateScalarHandlerB));
}
}
[Test]
public async Task I_can_see_it_throw_if_dispatcher_options_are_modified_after_build()
{