diff --git a/CHANGELOG.md b/CHANGELOG.md index b05e20b..0541f06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,7 @@ To have a consistent experience across all packages, some public interfaces have - **request.validation:** Return no problems when validating a `null` instance of a reference type instead of dereferencing it (`NullReferenceException`) - **request.validation:** Make `Severity` meaningful — `Validation.IsValid` now only fails on `Error` problems; `Warning`/`Info` no longer invalidate. Added `Validation.IsValidFor(Severity)` to set the failing-severity threshold - **request.dispatcher:** Throw `InvalidOperationException` listing candidates when multiple handlers match a request instead of silently resolving via `.First()`. Also throws when no handler is registered +- **request.dispatcher:** Resolve handlers/behaviors in registration order across closed and open generics instead of the fixed closed-before-open reflection order ### Removed diff --git a/src/request.dispatcher.tests/ScalarDispatcherTests.cs b/src/request.dispatcher.tests/ScalarDispatcherTests.cs index 8c5ffb1..f274da9 100644 --- a/src/request.dispatcher.tests/ScalarDispatcherTests.cs +++ b/src/request.dispatcher.tests/ScalarDispatcherTests.cs @@ -259,6 +259,29 @@ internal sealed class ScalarDispatcherTests } } + [Test] + public async Task I_can_see_candidates_ordered_by_registration_not_closed_first() + { + var sc = new ServiceCollection(); + sc.AddRequestDispatcher(builder => builder + .Add(typeof(InterfaceConstrainedScalarHandler<>)) + .Add(typeof(InterfaceInheritedScalarHandler))); + var provider = sc.BuildServiceProvider(); + var dispatcher = provider.GetRequiredService(); + + var request = new InterfaceInheritedScalarRequest { Name = "Constrained" }; + + // The open generic handler is registered before the closed handler, so candidates must be + // listed in registration order (open first), not the old fixed closed-before-open order. + var ex = await Assert.That(async () => await dispatcher.DispatchAsync(request)).Throws(); + + var message = ex?.Message ?? string.Empty; + var openIndex = message.IndexOf("InterfaceConstrainedScalarHandler", StringComparison.Ordinal); + var closedIndex = message.IndexOf("InterfaceInheritedScalarHandler", StringComparison.Ordinal); + + await Assert.That(openIndex >= 0 && closedIndex >= 0 && openIndex < closedIndex).IsTrue(); + } + [Test] public async Task I_can_see_it_throw_if_dispatcher_options_are_modified_after_build() { diff --git a/src/request.dispatcher/RequestDispatcherOptions.cs b/src/request.dispatcher/RequestDispatcherOptions.cs index d18def9..6560f46 100644 --- a/src/request.dispatcher/RequestDispatcherOptions.cs +++ b/src/request.dispatcher/RequestDispatcherOptions.cs @@ -46,11 +46,16 @@ internal sealed class RequestDispatcherOptions protected readonly Dictionary> _closedTypeInfo = []; protected readonly List _openTypeInfo = []; + protected readonly Dictionary _order = []; protected TypeIndex(IEnumerable collection, Func predicate) { + var index = 0; + foreach (var type in collection) { + _order[type] = index++; + if (type.IsGenericTypeDefinition) { if (type.GetInterfaces().Any(predicate)) @@ -102,11 +107,14 @@ internal sealed class RequestDispatcherOptions { protected override IReadOnlyList IsAssignableTo(Type @interface) { - var result = new List(); + var result = new List<(int Order, Type Type)>(); if (_closedTypeInfo.TryGetValue(@interface, out var list)) { - result.AddRange(list); + foreach (var type in list) + { + result.Add((_order[type], type)); + } } var requestType = @interface.GetGenericArguments()[0]; @@ -121,7 +129,7 @@ internal sealed class RequestDispatcherOptions var impl = type.MakeGenericType(requestType); if (impl.IsAssignableTo(@interface)) { - result.Add(impl); + result.Add((_order[type], impl)); } } catch (ArgumentException) @@ -137,7 +145,7 @@ internal sealed class RequestDispatcherOptions var impl = type.MakeGenericType(requestType.GetGenericArguments()); if (impl.IsAssignableTo(@interface)) { - result.Add(impl); + result.Add((_order[type], impl)); } } } @@ -146,7 +154,7 @@ internal sealed class RequestDispatcherOptions } } - return result; + return result.OrderBy(x => x.Order).Select(x => x.Type).ToList(); } } @@ -162,11 +170,14 @@ internal sealed class RequestDispatcherOptions { protected override IReadOnlyList IsAssignableTo(Type @interface) { - var result = new List(); + var result = new List<(int Order, Type Type)>(); if (_closedTypeInfo.TryGetValue(@interface, out var list)) { - result.AddRange(list); + foreach (var type in list) + { + result.Add((_order[type], type)); + } } var requestType = @interface.GetGenericArguments()[0]; @@ -180,7 +191,7 @@ internal sealed class RequestDispatcherOptions var impl = behaviour.MakeGenericType(requestType, responseType); if (impl.IsAssignableTo(@interface)) { - result.Add(impl); + result.Add((_order[behaviour], impl)); } } catch (ArgumentException) @@ -193,7 +204,7 @@ internal sealed class RequestDispatcherOptions var impl = behaviour.MakeGenericType(requestType.GetGenericArguments()); if (impl.IsAssignableTo(@interface)) { - result.Add(impl); + result.Add((_order[behaviour], impl)); } } catch (ArgumentException) @@ -201,7 +212,7 @@ internal sealed class RequestDispatcherOptions } } - return result; + return result.OrderBy(x => x.Order).Select(x => x.Type).ToList(); } } }