From fae50d2f4c2d49d7461a89c92f5d06da87b42732 Mon Sep 17 00:00:00 2001 From: Jaroslav Ruzicka Date: Fri, 16 Jan 2026 11:30:09 +0100 Subject: [PATCH 1/2] keep the last registration being resolved despite conflicting constraints --- .../src/ServiceLookup/CallSiteFactory.cs | 10 +++++++++- .../tests/DI.Tests/ServiceProviderContainerTests.cs | 12 ++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs index b64a8a9b9f7ba2..0a5f1f3a558816 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs @@ -344,11 +344,19 @@ private static bool AreCompatible(DynamicallyAccessedMemberTypes serviceDynamica // For AnyKey, we want to cache based on descriptor identity, not AnyKey that cacheKey has. ServiceIdentifier registrationKey = isAnyKeyLookup ? ServiceIdentifier.FromDescriptor(_descriptors[i]) : cacheKey; slot = GetSlot(registrationKey); - if (CreateOpenGeneric(_descriptors[i], registrationKey, callSiteChain, slot, throwOnConstraintViolation: false) is { } callSite) + + // We skip open generics with incompatible constraints. + if (CreateOpenGeneric(_descriptors[i], registrationKey, callSiteChain, slot, false) is { } callSite) { AddCallSite(callSite, i); UpdateSlot(registrationKey); } + else if (slot == 0) + { + // if the last registration has incompatible constraints, + // we still need to update the slot to allow direct resolution to correctly throw later + UpdateSlot(registrationKey); + } } } } diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderContainerTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderContainerTests.cs index 94802673078634..d61a513086a18c 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderContainerTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderContainerTests.cs @@ -1312,6 +1312,18 @@ public void ResolveKeyedServiceWithKeyedParameter_MissingRegistrationButWithUnke Assert.Contains("Microsoft.Extensions.DependencyInjection.Specification.KeyedDependencyInjectionSpecificationTests+IService", ex.ToString()); } + [Fact] + public async Task InvalidConstrainedOpenGenericThrowsWhenResolvedAsEnumerable() + { + var sc = new ServiceCollection(); + sc.AddSingleton(typeof(IBB<>), typeof(GenericBB<>)); + sc.AddSingleton(typeof(IBB<>), typeof(ConstrainedGenericBB<>)); + var sp = CreateServiceProvider(sc); + + Assert.Single(sp.GetServices>>()); + Assert.Throws(() => sp.GetService>>()); + } + private async Task ResolveUniqueServicesConcurrently() { var types = new Type[] From ee81dd1d98e1de0a03d78a5f68dd815310059524 Mon Sep 17 00:00:00 2001 From: Jaroslav Ruzicka Date: Mon, 19 Jan 2026 14:30:09 +0100 Subject: [PATCH 2/2] resolving automated comments --- .../src/ServiceLookup/CallSiteFactory.cs | 6 ++++-- .../tests/DI.Tests/ServiceProviderContainerTests.cs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs index 0a5f1f3a558816..490337d14b028c 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs @@ -353,8 +353,10 @@ private static bool AreCompatible(DynamicallyAccessedMemberTypes serviceDynamica } else if (slot == 0) { - // if the last registration has incompatible constraints, - // we still need to update the slot to allow direct resolution to correctly throw later + // If the last registration has incompatible constraints, we still need to update the slot. + // This ensures that single service resolution (GetService) will attempt to resolve using the last + // registration and throw an ArgumentException, maintaining "last wins" semantics. During enumerable + // resolution (GetServices), the incompatible registration is simply skipped. UpdateSlot(registrationKey); } } diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderContainerTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderContainerTests.cs index d61a513086a18c..2e837783951f7f 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderContainerTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderContainerTests.cs @@ -1313,7 +1313,7 @@ public void ResolveKeyedServiceWithKeyedParameter_MissingRegistrationButWithUnke } [Fact] - public async Task InvalidConstrainedOpenGenericThrowsWhenResolvedAsEnumerable() + public void InvalidConstrainedOpenGenericIsSkippedInEnumerableButThrowsInSingleResolution() { var sc = new ServiceCollection(); sc.AddSingleton(typeof(IBB<>), typeof(GenericBB<>));