fix(dispatcher): resolve handlers in registration order across closed and open generics
Handler/behavior resolution previously forced closed generics before open generics regardless of registration order, which was surprising and fragile. TypeIndex now tracks each registered type's index and orders matching candidates by registration order, so closed/open ordering is deterministic and follows registration.
This commit is contained in:
parent
a7d7e9fbaa
commit
8099898f3d
3 changed files with 45 additions and 10 deletions
|
|
@ -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<IRequestDispatcher>();
|
||||
|
||||
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<InvalidOperationException>();
|
||||
|
||||
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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -46,11 +46,16 @@ internal sealed class RequestDispatcherOptions
|
|||
|
||||
protected readonly Dictionary<Type, List<Type>> _closedTypeInfo = [];
|
||||
protected readonly List<Type> _openTypeInfo = [];
|
||||
protected readonly Dictionary<Type, int> _order = [];
|
||||
|
||||
protected TypeIndex(IEnumerable<Type> collection, Func<Type, bool> 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<Type> IsAssignableTo(Type @interface)
|
||||
{
|
||||
var result = new List<Type>();
|
||||
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<Type> IsAssignableTo(Type @interface)
|
||||
{
|
||||
var result = new List<Type>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue