Skip to content

Commit 208c40b

Browse files
authored
Do not allow a non-keyed service to be injected to a keyed parameter (#105839)
1 parent 51582ae commit 208c40b

File tree

3 files changed

+126
-5
lines changed

3 files changed

+126
-5
lines changed

src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Microsoft.Extensions.DependencyInjection.Specification
1111
{
1212
public abstract partial class KeyedDependencyInjectionSpecificationTests
1313
{
14-
protected abstract IServiceProvider CreateServiceProvider(IServiceCollection collection);
14+
protected abstract IServiceProvider CreateServiceProvider(IServiceCollection collection);
1515

1616
[Fact]
1717
public void ResolveKeyedService()
@@ -263,6 +263,70 @@ public void ResolveKeyedServiceSingletonInstanceWithKeyedParameter()
263263
Assert.Equal("service2", svc.Service2.ToString());
264264
}
265265

266+
[Fact]
267+
public void ResolveKeyedServiceWithKeyedParameter_MissingRegistration_SecondParameter()
268+
{
269+
var serviceCollection = new ServiceCollection();
270+
271+
serviceCollection.AddKeyedSingleton<IService, Service>("service1");
272+
// We are missing the registration for "service2" here and OtherService requires it.
273+
274+
serviceCollection.AddSingleton<OtherService>();
275+
276+
var provider = CreateServiceProvider(serviceCollection);
277+
278+
Assert.Null(provider.GetService<IService>());
279+
Assert.Throws<InvalidOperationException>(() => provider.GetService<OtherService>());
280+
}
281+
282+
[Fact]
283+
public void ResolveKeyedServiceWithKeyedParameter_MissingRegistration_FirstParameter()
284+
{
285+
var serviceCollection = new ServiceCollection();
286+
287+
// We are not registering "service1" and "service1" keyed IService services and OtherService requires them.
288+
289+
serviceCollection.AddSingleton<OtherService>();
290+
291+
var provider = CreateServiceProvider(serviceCollection);
292+
293+
Assert.Null(provider.GetService<IService>());
294+
Assert.Throws<InvalidOperationException>(() => provider.GetService<OtherService>());
295+
}
296+
297+
[Fact]
298+
public void ResolveKeyedServiceWithKeyedParameter_MissingRegistrationButWithDefaults()
299+
{
300+
var serviceCollection = new ServiceCollection();
301+
302+
// We are not registering "service1" and "service1" keyed IService services and OtherServiceWithDefaultCtorArgs
303+
// specifies them but has argument defaults if missing.
304+
305+
serviceCollection.AddSingleton<OtherServiceWithDefaultCtorArgs>();
306+
307+
var provider = CreateServiceProvider(serviceCollection);
308+
309+
Assert.Null(provider.GetService<IService>());
310+
Assert.NotNull(provider.GetService<OtherServiceWithDefaultCtorArgs>());
311+
}
312+
313+
[Fact]
314+
public void ResolveKeyedServiceWithKeyedParameter_MissingRegistrationButWithUnkeyedService()
315+
{
316+
var serviceCollection = new ServiceCollection();
317+
318+
// We are not registering "service1" and "service1" keyed IService services and OtherService requires them,
319+
// but we are registering an unkeyed IService service which should not be injected into OtherService.
320+
serviceCollection.AddSingleton<IService, Service>();
321+
322+
serviceCollection.AddSingleton<OtherService>();
323+
324+
var provider = CreateServiceProvider(serviceCollection);
325+
326+
Assert.NotNull(provider.GetService<IService>());
327+
Assert.Throws<InvalidOperationException>(() => provider.GetService<OtherService>());
328+
}
329+
266330
[Fact]
267331
public void CreateServiceWithKeyedParameter()
268332
{
@@ -490,9 +554,9 @@ public void ResolveKeyedTransientFromScopeServiceProvider()
490554
Assert.NotSame(serviceA1, serviceB1);
491555
}
492556

493-
internal interface IService { }
557+
public interface IService { }
494558

495-
internal class Service : IService
559+
public class Service : IService
496560
{
497561
private readonly string _id;
498562

@@ -503,7 +567,7 @@ internal class Service : IService
503567
public override string? ToString() => _id;
504568
}
505569

506-
internal class OtherService
570+
public class OtherService
507571
{
508572
public OtherService(
509573
[FromKeyedServices("service1")] IService service1,
@@ -518,6 +582,36 @@ public OtherService(
518582
public IService Service2 { get; }
519583
}
520584

585+
internal class OtherServiceWithDefaultCtorArgs
586+
{
587+
public OtherServiceWithDefaultCtorArgs(
588+
[FromKeyedServices("service1")] IService service1 = null,
589+
[FromKeyedServices("service2")] IService service2 = null)
590+
{
591+
Service1 = service1;
592+
Service2 = service2;
593+
}
594+
595+
public IService Service1 { get; }
596+
597+
public IService Service2 { get; }
598+
}
599+
600+
internal class ServiceWithOtherService
601+
{
602+
public ServiceWithOtherService(
603+
[FromKeyedServices("service1")] IService service1,
604+
[FromKeyedServices("service2")] IService service2)
605+
{
606+
Service1 = service1;
607+
Service2 = service2;
608+
}
609+
610+
public IService Service1 { get; }
611+
612+
public IService Service2 { get; }
613+
}
614+
521615
internal class ServiceWithIntKey : IService
522616
{
523617
private readonly int _id;

src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,7 @@ private ConstructorCallSite CreateConstructorCallSite(
574574
for (int index = 0; index < parameters.Length; index++)
575575
{
576576
ServiceCallSite? callSite = null;
577+
bool isKeyedParameter = false;
577578
Type parameterType = parameters[index].ParameterType;
578579
foreach (var attribute in parameters[index].GetCustomAttributes(true))
579580
{
@@ -591,11 +592,15 @@ private ConstructorCallSite CreateConstructorCallSite(
591592
{
592593
var parameterSvcId = new ServiceIdentifier(keyed.Key, parameterType);
593594
callSite = GetCallSite(parameterSvcId, callSiteChain);
595+
isKeyedParameter = true;
594596
break;
595597
}
596598
}
597599

598-
callSite ??= GetCallSite(ServiceIdentifier.FromServiceType(parameterType), callSiteChain);
600+
if (!isKeyedParameter)
601+
{
602+
callSite ??= GetCallSite(ServiceIdentifier.FromServiceType(parameterType), callSiteChain);
603+
}
599604

600605
if (callSite == null && ParameterDefaultValue.TryGetDefaultValue(parameters[index], out object? defaultValue))
601606
{

src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderContainerTests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1290,6 +1290,28 @@ public void ScopedServiceResolvedFromSingletonAfterCompilation3()
12901290
Assert.Same(sp.GetRequiredService<IFakeOpenGenericService<Aa>>().Value.PropertyA, sp.GetRequiredService<A>());
12911291
}
12921292

1293+
[Fact]
1294+
public void ResolveKeyedServiceWithKeyedParameter_MissingRegistrationButWithUnkeyedService()
1295+
{
1296+
var serviceCollection = new ServiceCollection();
1297+
1298+
// We are not registering "service1" and "service1" keyed IService services and OtherService requires them,
1299+
// but we are registering an unkeyed IService service which should not be injected into OtherService.
1300+
serviceCollection.AddSingleton<KeyedDependencyInjectionSpecificationTests.IService, KeyedDependencyInjectionSpecificationTests.Service>();
1301+
1302+
serviceCollection.AddSingleton<KeyedDependencyInjectionSpecificationTests.OtherService>();
1303+
1304+
AggregateException ex = Assert.Throws<AggregateException>(() => serviceCollection.BuildServiceProvider(new ServiceProviderOptions
1305+
{
1306+
ValidateOnBuild = true
1307+
}));
1308+
1309+
Assert.Equal(1, ex.InnerExceptions.Count);
1310+
Assert.StartsWith("Some services are not able to be constructed", ex.Message);
1311+
Assert.Contains("ServiceType: Microsoft.Extensions.DependencyInjection.Specification.KeyedDependencyInjectionSpecificationTests+OtherService", ex.ToString());
1312+
Assert.Contains("Microsoft.Extensions.DependencyInjection.Specification.KeyedDependencyInjectionSpecificationTests+IService", ex.ToString());
1313+
}
1314+
12931315
private async Task<bool> ResolveUniqueServicesConcurrently()
12941316
{
12951317
var types = new Type[]

0 commit comments

Comments
 (0)