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.
268 lines
10 KiB
C#
268 lines
10 KiB
C#
// Copyright (c) The Geekeey Authors
|
|
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
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()
|
|
{
|
|
var sc = new ServiceCollection();
|
|
sc.AddRequestDispatcher(builder => builder
|
|
.Add(typeof(OpenStreamHandler<>)));
|
|
var provider = sc.BuildServiceProvider();
|
|
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
|
|
|
|
var request = new OpenStreamRequest { Data = "Hello" };
|
|
var results = await dispatcher.DispatchAsync(request).ToListAsync();
|
|
|
|
await Assert.That(results).IsEquivalentTo(["Hello-Stream-0", "Hello-Stream-1"]);
|
|
}
|
|
|
|
[Test]
|
|
public async Task I_can_dispatch_a_request_async_with_an_open_generic_handler_that_has_constraints()
|
|
{
|
|
var sc = new ServiceCollection();
|
|
sc.AddRequestDispatcher(builder => builder
|
|
.Add(typeof(ConstrainedStreamHandler<>)));
|
|
var provider = sc.BuildServiceProvider();
|
|
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
|
|
|
|
var request = new ConstrainedStreamRequest { Value = 123 };
|
|
var results = await dispatcher.DispatchAsync(request).ToListAsync();
|
|
|
|
await Assert.That(results).IsEquivalentTo(["123-Constrained-0", "123-Constrained-1"]);
|
|
}
|
|
|
|
[Test]
|
|
public async Task I_can_see_it_fail_if_no_handler_is_found_even_with_an_open_generic_available()
|
|
{
|
|
var sc = new ServiceCollection();
|
|
sc.AddRequestDispatcher(builder => builder
|
|
.Add(typeof(OpenStreamHandler<>)));
|
|
var provider = sc.BuildServiceProvider();
|
|
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
|
|
|
|
var request = new UnhandledStreamRequest();
|
|
await Assert.ThrowsAsync<InvalidOperationException>(async () => await dispatcher.DispatchAsync(request).FirstOrDefaultAsync().AsTask());
|
|
}
|
|
|
|
[Test]
|
|
public async Task I_can_dispatch_a_request_async_with_an_inherited_request()
|
|
{
|
|
var sc = new ServiceCollection();
|
|
sc.AddRequestDispatcher(builder => builder
|
|
.Add(typeof(OpenStreamHandler<>)));
|
|
var provider = sc.BuildServiceProvider();
|
|
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
|
|
|
|
var request = new InheritedStreamRequest { Data = "Sub" };
|
|
var results = await dispatcher.DispatchAsync(request).ToListAsync();
|
|
|
|
await Assert.That(results).IsEquivalentTo(["Sub-Stream-0", "Sub-Stream-1"]);
|
|
}
|
|
|
|
[Test]
|
|
public async Task I_can_dispatch_a_request_async_with_an_inherited_handler()
|
|
{
|
|
var sc = new ServiceCollection();
|
|
sc.AddRequestDispatcher(builder => builder
|
|
.Add(typeof(DerivedStreamHandler)));
|
|
var provider = sc.BuildServiceProvider();
|
|
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
|
|
|
|
var request = new DerivedStreamRequest { Value = 42 };
|
|
var results = await dispatcher.DispatchAsync(request).ToListAsync();
|
|
|
|
await Assert.That(results).IsEquivalentTo(["Derived: 42-0", "Derived: 42-1"]);
|
|
}
|
|
|
|
[Test]
|
|
public async Task I_can_dispatch_a_request_async_with_an_interface_inherited_request()
|
|
{
|
|
var sc = new ServiceCollection();
|
|
sc.AddRequestDispatcher(builder => builder
|
|
.Add(typeof(InterfaceInheritedStreamHandler)));
|
|
var provider = sc.BuildServiceProvider();
|
|
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
|
|
|
|
var request = new InterfaceInheritedStreamRequest { Name = "InterfaceTest" };
|
|
var results = await dispatcher.DispatchAsync(request).ToListAsync();
|
|
|
|
await Assert.That(results).IsEquivalentTo(["InterfaceTest-InterfaceHandled-0", "InterfaceTest-InterfaceHandled-1"]);
|
|
}
|
|
|
|
[Test]
|
|
public async Task I_can_dispatch_a_request_async_with_deep_inheritance_in_the_request()
|
|
{
|
|
var sc = new ServiceCollection();
|
|
sc.AddRequestDispatcher(builder => builder
|
|
.Add(typeof(OpenStreamHandler<>)));
|
|
var provider = sc.BuildServiceProvider();
|
|
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
|
|
|
|
var request = new DeepDerivedStreamRequest { Data = "Deep", DeepValue = 99 };
|
|
var results = await dispatcher.DispatchAsync(request).ToListAsync();
|
|
|
|
await Assert.That(results).IsEquivalentTo(["Deep-Stream-0", "Deep-Stream-1"]);
|
|
}
|
|
|
|
[Test]
|
|
public async Task I_can_dispatch_a_request_async_with_an_interface_constrained_handler()
|
|
{
|
|
var sc = new ServiceCollection();
|
|
sc.AddRequestDispatcher(builder => builder
|
|
.Add(typeof(InterfaceInheritedStreamHandler))
|
|
.Add(typeof(InterfaceConstrainedStreamHandler<>)));
|
|
var provider = sc.BuildServiceProvider();
|
|
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
|
|
|
|
var request = new InterfaceInheritedStreamRequest { Name = "Constrained" };
|
|
|
|
// Both InterfaceInheritedStreamHandler and InterfaceConstrainedStreamHandler could match.
|
|
// InterfaceInheritedStreamHandler is a concrete match for InterfaceInheritedStreamRequest.
|
|
// InterfaceConstrainedStreamHandler is an open generic match.
|
|
// 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]
|
|
public async Task I_can_dispatch_a_request_async_with_an_interface_only_match()
|
|
{
|
|
var sc = new ServiceCollection();
|
|
sc.AddRequestDispatcher(builder => builder
|
|
.Add(typeof(InterfaceConstrainedStreamHandler<>)));
|
|
var provider = sc.BuildServiceProvider();
|
|
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
|
|
|
|
var request = new AnotherNamedStreamRequest { Name = "InterfaceOnly" };
|
|
var results = await dispatcher.DispatchAsync(request).ToListAsync();
|
|
|
|
await Assert.That(results).IsEquivalentTo(["InterfaceOnly-ConstrainedByInterface-0", "InterfaceOnly-ConstrainedByInterface-1"]);
|
|
}
|
|
|
|
[Test]
|
|
public async Task I_can_dispatch_a_request_async_with_a_nested_generic_request()
|
|
{
|
|
var sc = new ServiceCollection();
|
|
sc.AddRequestDispatcher(builder => builder
|
|
.Add(typeof(WrapperStreamHandler<>)));
|
|
var provider = sc.BuildServiceProvider();
|
|
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
|
|
|
|
var request = new WrapperStreamRequest<int> { Item = 42 };
|
|
var results = await dispatcher.DispatchAsync(request).ToListAsync();
|
|
|
|
await Assert.That(results).IsEquivalentTo(["Handled-42-0", "Handled-42-1"]);
|
|
}
|
|
|
|
[Test]
|
|
public async Task I_can_handle_multiple_interface_implementations()
|
|
{
|
|
var sc = new ServiceCollection();
|
|
sc.AddRequestDispatcher(builder => builder
|
|
.Add(typeof(MultiInterfaceStreamHandler)));
|
|
var provider = sc.BuildServiceProvider();
|
|
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
|
|
|
|
var request = new MultiInterfaceStreamRequest();
|
|
|
|
var results1 = await dispatcher.DispatchAsync<int>(request).ToListAsync();
|
|
var results2 = await dispatcher.DispatchAsync<string>(request).ToListAsync();
|
|
|
|
await Assert.That(results1).IsEquivalentTo([1, 2]);
|
|
await Assert.That(results2).IsEquivalentTo(["One", "Two"]);
|
|
}
|
|
|
|
[Test]
|
|
public async Task I_can_see_it_fail_if_there_are_ambiguous_handle_methods()
|
|
{
|
|
var sc = new ServiceCollection();
|
|
sc.AddRequestDispatcher(builder => builder
|
|
.Add(typeof(AmbiguousStreamHandler)));
|
|
var provider = sc.BuildServiceProvider();
|
|
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
|
|
|
|
var request = new AmbiguousStreamRequest();
|
|
var results = await dispatcher.DispatchAsync(request).ToListAsync();
|
|
|
|
await Assert.That(results).IsEquivalentTo(["Interface-Handled"]);
|
|
}
|
|
|
|
[Test]
|
|
public async Task I_can_dispatch_a_request_async_with_a_generic_interface_explicit_implementation()
|
|
{
|
|
var sc = new ServiceCollection();
|
|
sc.AddRequestDispatcher(builder => builder
|
|
.Add(typeof(ExplicitGenericStreamHandler<>)));
|
|
var provider = sc.BuildServiceProvider();
|
|
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
|
|
|
|
var request = new ExplicitGenericStreamRequest { Value = "Explicit" };
|
|
var results = await dispatcher.DispatchAsync(request).ToListAsync();
|
|
|
|
await Assert.That(results).IsEquivalentTo(["Explicit-ExplicitHandled"]);
|
|
}
|
|
|
|
[Test]
|
|
public async Task I_can_see_it_throw_the_original_exception()
|
|
{
|
|
var sc = new ServiceCollection();
|
|
sc.AddRequestDispatcher(builder => builder
|
|
.Add(typeof(FailingStreamHandler)));
|
|
var provider = sc.BuildServiceProvider();
|
|
var dispatcher = provider.GetRequiredService<IRequestDispatcher>();
|
|
|
|
var request = new FailingStreamRequest();
|
|
var enumerable = dispatcher.DispatchAsync(request);
|
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(async () => await enumerable.ToListAsync().AsTask());
|
|
|
|
using (Assert.Multiple())
|
|
{
|
|
await Assert.That(ex?.Message).IsEquivalentTo("Handler failed");
|
|
await Assert.That(ex?.StackTrace).Contains(nameof(FailingStreamHandler.HandleAsync));
|
|
}
|
|
}
|
|
|
|
[Test]
|
|
public async Task I_can_see_it_throw_if_dispatcher_options_are_modified_after_build()
|
|
{
|
|
var sc = new ServiceCollection();
|
|
sc.AddRequestDispatcher(builder => builder
|
|
.Add(typeof(FailingStreamHandler)));
|
|
var provider = sc.BuildServiceProvider();
|
|
var options = provider.GetRequiredService<Microsoft.Extensions.Options.IOptions<RequestDispatcherOptions>>().Value;
|
|
options.GetRequestHandlers<IStreamRequestHandler<FailingStreamRequest, string>>(default!);
|
|
|
|
await Assert.That(() => options.Inspect([])).Throws<InvalidOperationException>();
|
|
}
|
|
}
|