Skip to content

Commit bba50db

Browse files
committed
Fix open-generic interface ordering bug when multiple interfaces match service pattern (#1464)
When an open-generic implementation type implements multiple interfaces that match the service's name/namespace pattern (e.g. IHandler<IRequest> and IHandler<IRequest<TParam>>), TryMapImplementationGenericArguments previously fell back to availableArguments[0] when no exact match existed. If the first candidate produced an incomplete mapping (null entries because its generic args could not be resolved to the implementation's type parameters), the caller's null check would fail and the registration was silently dropped. Fix: after the exact-match check, prefer the first candidate that produces a complete mapping (no null entries). Only fall back to availableArguments[0] when no candidate maps fully, preserving existing behavior for genuinely un-bindable registrations.
1 parent f8d4095 commit bba50db

2 files changed

Lines changed: 141 additions & 1 deletion

File tree

src/Autofac/Features/OpenGenerics/OpenGenericServiceBinder.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,18 @@ private static Service[] BuildImplementedServices(IEnumerable<Service> configure
240240
.ToArray();
241241

242242
var exactMatch = availableArguments.FirstOrDefault(a => a.SequenceEqual(serviceGenericArguments));
243-
return exactMatch ?? availableArguments[0];
243+
if (exactMatch is not null)
244+
{
245+
return exactMatch;
246+
}
247+
248+
// When no exact match exists, prefer the first candidate that produces a complete
249+
// mapping (no null entries), so that a non-mappable interface appearing earlier in
250+
// the interface list does not shadow a valid one that appears later. Fall back to
251+
// availableArguments[0] when no candidate fully maps (preserves prior behavior for
252+
// genuinely un-bindable registrations, where the caller's null check handles it).
253+
var completeMatch = availableArguments.FirstOrDefault(a => a.All(arg => arg is not null));
254+
return completeMatch ?? availableArguments[0];
244255
}
245256

246257
private static Type?[] TryFindServiceArgumentsForImplementation(Type implementationType, IEnumerable<Type> serviceGenericArguments, IEnumerable<Type> serviceArgumentDefinitions)
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Copyright (c) Autofac Project. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
namespace Autofac.Test.Features.OpenGenerics;
5+
6+
/// <summary>
7+
/// Regression tests for issue #1464: OpenGenericServiceBinder.TryBindOpenGenericTypedService
8+
/// should not let a non-mappable interface appearing first in the interface list suppress a
9+
/// valid later interface that CAN be mapped.
10+
/// </summary>
11+
public class OpenGenericMultipleInterfaceOrderTests
12+
{
13+
private interface IHandler<in T>
14+
{
15+
}
16+
17+
private interface IRequest
18+
{
19+
}
20+
21+
private interface IRequest<T>
22+
{
23+
}
24+
25+
/// <summary>
26+
/// Handler whose first interface IS the mappable one (IHandler&lt;IRequest&lt;TParam&gt;&gt;).
27+
/// This worked before the fix and must continue to work.
28+
/// </summary>
29+
private class HandlerMappableFirst<TParam>
30+
: IHandler<IRequest<TParam>>, IHandler<IRequest>
31+
{
32+
}
33+
34+
/// <summary>
35+
/// Handler whose first interface is NOT mappable for the requested service
36+
/// (IHandler&lt;IRequest&gt; — closed, no TParam) and the mappable one comes second.
37+
/// This is the ordering bug reported in #1464.
38+
/// </summary>
39+
private class HandlerNonMappableFirst<TParam>
40+
: IHandler<IRequest>, IHandler<IRequest<TParam>>
41+
{
42+
}
43+
44+
/// <summary>
45+
/// Handler whose class generic parameter is NOT used in the interface it implements.
46+
/// Assignability rules say this CANNOT be bound as an open-generic IHandler&lt;&gt; because
47+
/// TParam cannot be inferred from the service arguments. Autofac must not bind this
48+
/// and must not throw; it must cleanly return no registration.
49+
/// </summary>
50+
private class HandlerWithUnrelatedTypeParam<TParam>
51+
: IHandler<int>
52+
{
53+
}
54+
55+
[Fact]
56+
public void MappableFirstHandlerIsResolvable()
57+
{
58+
var builder = new ContainerBuilder();
59+
builder
60+
.RegisterGeneric(typeof(HandlerMappableFirst<>))
61+
.As(typeof(IHandler<>).MakeGenericType(typeof(IRequest<>)));
62+
63+
var container = builder.Build();
64+
65+
var handlers = container.Resolve<IEnumerable<IHandler<IRequest<int>>>>();
66+
Assert.Single(handlers);
67+
Assert.IsType<HandlerMappableFirst<int>>(handlers.Single());
68+
}
69+
70+
[Fact]
71+
public void NonMappableFirstHandlerIsResolvable()
72+
{
73+
// Regression for #1464: when the first interface (IHandler<IRequest>) cannot
74+
// be mapped to the type parameter TParam, the binder must not give up — it
75+
// must continue to the second interface (IHandler<IRequest<TParam>>) which CAN
76+
// be mapped.
77+
var builder = new ContainerBuilder();
78+
builder
79+
.RegisterGeneric(typeof(HandlerNonMappableFirst<>))
80+
.As(typeof(IHandler<>).MakeGenericType(typeof(IRequest<>)));
81+
82+
var container = builder.Build();
83+
84+
var handlers = container.Resolve<IEnumerable<IHandler<IRequest<int>>>>();
85+
Assert.Single(handlers);
86+
Assert.IsType<HandlerNonMappableFirst<int>>(handlers.Single());
87+
}
88+
89+
[Fact]
90+
public void BothHandlerOrderVariantsAreResolvedFromEnumerable()
91+
{
92+
// The full scenario from the issue: both handlers registered for the same open
93+
// generic service — both must appear in the resolved enumerable regardless of
94+
// which one has the non-mappable interface first.
95+
var builder = new ContainerBuilder();
96+
builder
97+
.RegisterGeneric(typeof(HandlerMappableFirst<>))
98+
.As(typeof(IHandler<>).MakeGenericType(typeof(IRequest<>)));
99+
builder
100+
.RegisterGeneric(typeof(HandlerNonMappableFirst<>))
101+
.As(typeof(IHandler<>).MakeGenericType(typeof(IRequest<>)));
102+
103+
var container = builder.Build();
104+
105+
var handlers = container.Resolve<IEnumerable<IHandler<IRequest<int>>>>().ToList();
106+
Assert.Equal(2, handlers.Count);
107+
Assert.Contains(handlers, h => h is HandlerMappableFirst<int>);
108+
Assert.Contains(handlers, h => h is HandlerNonMappableFirst<int>);
109+
}
110+
111+
[Fact]
112+
public void HandlerWithUnrelatedTypeParamIsNotResolved()
113+
{
114+
// Second case from #1464: GenericMismatchWithInterface<TParam> : IHandler<int>
115+
// registered as IHandler<>. The type parameter TParam cannot be inferred from the
116+
// service arguments (int), so Autofac must not bind this and the enumerable must
117+
// be empty. No exception should be thrown.
118+
var builder = new ContainerBuilder();
119+
builder
120+
.RegisterGeneric(typeof(HandlerWithUnrelatedTypeParam<>))
121+
.As(typeof(IHandler<>));
122+
123+
var container = builder.Build();
124+
125+
// Must not throw; must return no registrations.
126+
var handlers = container.Resolve<IEnumerable<IHandler<int>>>();
127+
Assert.Empty(handlers);
128+
}
129+
}

0 commit comments

Comments
 (0)