From e96f6f7d30392f8aab6633f8bc655ee19df16b85 Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Fri, 26 Jan 2024 16:55:25 +0100 Subject: [PATCH 1/9] Fix consistency issue in GetKeyedServices with AnyKey --- ...edDependencyInjectionSpecificationTests.cs | 35 +++++++++++++++++++ ...crosoft.Extensions.DependencyInjection.sln | 30 ++++++++++------ .../src/ServiceLookup/CallSiteFactory.cs | 18 +++++++--- 3 files changed, 67 insertions(+), 16 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs index 3664a3b81eafd4..95cb884b733e0c 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs @@ -6,6 +6,7 @@ using System.Security.Cryptography; using Microsoft.Extensions.DependencyInjection.Specification.Fakes; using Xunit; +using static Microsoft.Extensions.DependencyInjection.Specification.KeyedDependencyInjectionSpecificationTests; namespace Microsoft.Extensions.DependencyInjection.Specification { @@ -158,6 +159,40 @@ public void ResolveKeyedServicesAnyKeyWithAnyKeyRegistration() Assert.Equal(new[] { service1, service2, service3, service4 }, allServices.Skip(1)); } + [Fact] + public void ResolveKeyedServicesAnyKeyConsistency() + { + var serviceCollection = new ServiceCollection(); + var service = new Service("first-service"); + serviceCollection.AddKeyedSingleton("first-service", service); + + var provider1 = CreateServiceProvider(serviceCollection); + Assert.Null(provider1.GetKeyedService(KeyedService.AnyKey)); + Assert.Equal(new[] { service }, provider1.GetKeyedServices(KeyedService.AnyKey)); + + var provider2 = CreateServiceProvider(serviceCollection); + Assert.Equal(new[] { service }, provider2.GetKeyedServices(KeyedService.AnyKey)); + Assert.Null(provider2.GetKeyedService(KeyedService.AnyKey)); + } + + [Fact] + public void ResolveKeyedServicesAnyKeyConsistencyWithAnyKeyRegistration() + { + var serviceCollection = new ServiceCollection(); + var service = new Service("first-service"); + var any = new Service("any"); + serviceCollection.AddKeyedSingleton("first-service", service); + serviceCollection.AddKeyedSingleton(KeyedService.AnyKey, (sp, key) => any); + + var provider1 = CreateServiceProvider(serviceCollection); + Assert.Same(any, provider1.GetKeyedService(KeyedService.AnyKey)); + Assert.Equal(new[] { service, any }, provider1.GetKeyedServices(KeyedService.AnyKey)); + + var provider2 = CreateServiceProvider(serviceCollection); + Assert.Equal(new[] { service, any }, provider2.GetKeyedServices(KeyedService.AnyKey)); + Assert.Same(any, provider2.GetKeyedService(KeyedService.AnyKey)); + } + [Fact] public void ResolveKeyedGenericServices() { diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/Microsoft.Extensions.DependencyInjection.sln b/src/libraries/Microsoft.Extensions.DependencyInjection/Microsoft.Extensions.DependencyInjection.sln index 9345a6ded34b0d..fb3518cecab81d 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/Microsoft.Extensions.DependencyInjection.sln +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/Microsoft.Extensions.DependencyInjection.sln @@ -1,4 +1,8 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34414.90 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{30201F60-A891-4C3F-A1A6-DDDD1C8525E3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\ref\Microsoft.Bcl.AsyncInterfaces.csproj", "{047FD3F2-B3A0-4639-B4F0-40D29E61725D}" @@ -43,11 +47,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{74C4FAFF-491 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{28065F40-B930-4A5D-95D8-A3BD5F86CE11}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "tools\gen", "{0B56ECAF-7B4A-4135-A343-1577ACE09920}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{0B56ECAF-7B4A-4135-A343-1577ACE09920}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "tools\src", "{5DD887A4-ED78-4782-B372-34176C27F0C1}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5DD887A4-ED78-4782-B372-34176C27F0C1}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "tools\ref", "{6FA0C03B-0901-4FD8-AF44-D4C9E5516C2F}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{6FA0C03B-0901-4FD8-AF44-D4C9E5516C2F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{D6F10CF9-982E-4D09-A112-0CBFD2D46AFF}" EndProject @@ -135,28 +139,32 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {30201F60-A891-4C3F-A1A6-DDDD1C8525E3} = {5F2D3A6A-76D3-4C22-A7F7-8D73D67A98F8} - {2C6983BB-3CB4-4EB7-9AF1-2F24FE1ECEAB} = {5F2D3A6A-76D3-4C22-A7F7-8D73D67A98F8} - {F11DD75C-122C-4B98-9EED-F71551F9562A} = {5F2D3A6A-76D3-4C22-A7F7-8D73D67A98F8} - {1EE6CA66-6585-459D-8889-666D4C2D4C27} = {5F2D3A6A-76D3-4C22-A7F7-8D73D67A98F8} {047FD3F2-B3A0-4639-B4F0-40D29E61725D} = {E168C5B8-F2EB-4BDE-942A-59C1EB130D59} - {6D90C067-5CCD-4443-81A5-B9C385011F68} = {E168C5B8-F2EB-4BDE-942A-59C1EB130D59} - {66E6ADF5-200F-41F3-9CA4-858EF69D2A61} = {E168C5B8-F2EB-4BDE-942A-59C1EB130D59} {3068B34E-D975-4C11-B2F2-F10790051F2E} = {74C4FAFF-491D-448C-8CA0-F8E5FC838CC5} + {6D90C067-5CCD-4443-81A5-B9C385011F68} = {E168C5B8-F2EB-4BDE-942A-59C1EB130D59} {9CD9F9EB-379C-44C1-9016-33DFEC821C76} = {74C4FAFF-491D-448C-8CA0-F8E5FC838CC5} {4532D9F9-1E0D-4A62-8038-D3454B255E86} = {74C4FAFF-491D-448C-8CA0-F8E5FC838CC5} + {66E6ADF5-200F-41F3-9CA4-858EF69D2A61} = {E168C5B8-F2EB-4BDE-942A-59C1EB130D59} {C5ECD02C-FA5A-4B56-9CA2-47AD8989714A} = {74C4FAFF-491D-448C-8CA0-F8E5FC838CC5} + {2C6983BB-3CB4-4EB7-9AF1-2F24FE1ECEAB} = {5F2D3A6A-76D3-4C22-A7F7-8D73D67A98F8} + {F11DD75C-122C-4B98-9EED-F71551F9562A} = {5F2D3A6A-76D3-4C22-A7F7-8D73D67A98F8} + {1EE6CA66-6585-459D-8889-666D4C2D4C27} = {5F2D3A6A-76D3-4C22-A7F7-8D73D67A98F8} {FD3C8D4F-0EAA-4575-A685-F77C6D340E60} = {28065F40-B930-4A5D-95D8-A3BD5F86CE11} {9E5124E4-BEDA-4B2D-9699-60E2A7B1881D} = {28065F40-B930-4A5D-95D8-A3BD5F86CE11} {C85FD264-C77B-44F3-926C-D61C5DAD369E} = {0B56ECAF-7B4A-4135-A343-1577ACE09920} {A2CD66D3-74F2-4608-A56E-F866CC494620} = {0B56ECAF-7B4A-4135-A343-1577ACE09920} - {0B56ECAF-7B4A-4135-A343-1577ACE09920} = {D6F10CF9-982E-4D09-A112-0CBFD2D46AFF} {45502850-3207-49B0-9F5D-5DE95091DB5A} = {5DD887A4-ED78-4782-B372-34176C27F0C1} {B1E4A89A-6451-4484-B42B-4D1DF21B6961} = {5DD887A4-ED78-4782-B372-34176C27F0C1} - {5DD887A4-ED78-4782-B372-34176C27F0C1} = {D6F10CF9-982E-4D09-A112-0CBFD2D46AFF} {7E8189DE-6BBA-4CD5-9BA6-DF5ADBD027D3} = {6FA0C03B-0901-4FD8-AF44-D4C9E5516C2F} + {0B56ECAF-7B4A-4135-A343-1577ACE09920} = {D6F10CF9-982E-4D09-A112-0CBFD2D46AFF} + {5DD887A4-ED78-4782-B372-34176C27F0C1} = {D6F10CF9-982E-4D09-A112-0CBFD2D46AFF} {6FA0C03B-0901-4FD8-AF44-D4C9E5516C2F} = {D6F10CF9-982E-4D09-A112-0CBFD2D46AFF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {68A7BDA7-8093-433C-BF7A-8A6A7560BD02} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{a2cd66d3-74f2-4608-a56e-f866cc494620}*SharedItemsImports = 5 + ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{b1e4a89a-6451-4484-b42b-4d1df21b6961}*SharedItemsImports = 5 + EndGlobalSection EndGlobal diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs index f9b902dde19f3e..81268ba1b925eb 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs @@ -282,11 +282,13 @@ private static bool AreCompatible(DynamicallyAccessedMemberTypes serviceDynamica CallSiteResultCacheLocation cacheLocation = CallSiteResultCacheLocation.Root; ServiceCallSite[] callSites; + var isAnyKeyLookup = serviceIdentifier.ServiceKey == KeyedService.AnyKey; + // If item type is not generic we can safely use descriptor cache // Special case for KeyedService.AnyKey, we don't want to check the cache because a KeyedService.AnyKey registration // will "hide" all the other service registration if (!itemType.IsConstructedGenericType && - !KeyedService.AnyKey.Equals(cacheKey.ServiceKey) && + !isAnyKeyLookup && _descriptorLookup.TryGetValue(cacheKey, out ServiceDescriptorCacheItem descriptors)) { callSites = new ServiceCallSite[descriptors.Count]; @@ -317,9 +319,12 @@ private static bool AreCompatible(DynamicallyAccessedMemberTypes serviceDynamica int slot = 0; for (int i = _descriptors.Length - 1; i >= 0; i--) { - if (KeysMatch(_descriptors[i].ServiceKey, cacheKey.ServiceKey)) + if (KeysMatch(cacheKey.ServiceKey, _descriptors[i].ServiceKey)) { - if (TryCreateExact(_descriptors[i], cacheKey, callSiteChain, slot) is { } callSite) + // Special case for AnyKey: we don't want to add in cache a mapping AnyKey -> specific type, + // so we need to ask creation with the original identity of the descriptor + var registrationKey = isAnyKeyLookup ? ServiceIdentifier.FromDescriptor(_descriptors[i]) : cacheKey; + if (TryCreateExact(_descriptors[i], registrationKey, callSiteChain, slot) is { } callSite) { AddCallSite(callSite, i); } @@ -327,9 +332,12 @@ private static bool AreCompatible(DynamicallyAccessedMemberTypes serviceDynamica } for (int i = _descriptors.Length - 1; i >= 0; i--) { - if (KeysMatch(_descriptors[i].ServiceKey, cacheKey.ServiceKey)) + if (KeysMatch(cacheKey.ServiceKey, _descriptors[i].ServiceKey)) { - if (TryCreateOpenGeneric(_descriptors[i], cacheKey, callSiteChain, slot, throwOnConstraintViolation: false) is { } callSite) + // Special case for AnyKey: we don't want to add in cache a mapping AnyKey -> specific type, + // so we need to ask creation with the original identity of the descriptor + var registrationKey = isAnyKeyLookup ? ServiceIdentifier.FromDescriptor(_descriptors[i]) : cacheKey; + if (TryCreateOpenGeneric(_descriptors[i], registrationKey, callSiteChain, slot, throwOnConstraintViolation: false) is { } callSite) { AddCallSite(callSite, i); } From d6bacae2dd03a85d0aa059806bcfa8d4cc59bbdb Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Fri, 2 Feb 2024 15:39:19 +0100 Subject: [PATCH 2/9] Do not return AnyKey registration when enumerating services with AnyKey as the lookup key --- ...edDependencyInjectionSpecificationTests.cs | 16 ++++--- .../src/ServiceLookup/CallSiteFactory.cs | 44 +++++++++++-------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs index 95cb884b733e0c..336c3e14b479cf 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs @@ -153,10 +153,11 @@ public void ResolveKeyedServicesAnyKeyWithAnyKeyRegistration() _ = provider.GetKeyedService("something-else"); _ = provider.GetKeyedService("something-else-again"); - // Return all services registered with a non null key, but not the one "created" with KeyedService.AnyKey + // Return all services registered with a non null key, but not the one "created" with KeyedService.AnyKey, + // nor the KeyedService.AnyKey registration var allServices = provider.GetKeyedServices(KeyedService.AnyKey).ToList(); - Assert.Equal(5, allServices.Count); - Assert.Equal(new[] { service1, service2, service3, service4 }, allServices.Skip(1)); + Assert.Equal(4, allServices.Count); + Assert.Equal(new[] { service1, service2, service3, service4 }, allServices); } [Fact] @@ -168,10 +169,12 @@ public void ResolveKeyedServicesAnyKeyConsistency() var provider1 = CreateServiceProvider(serviceCollection); Assert.Null(provider1.GetKeyedService(KeyedService.AnyKey)); + // We don't return KeyedService.AnyKey registration when listing services Assert.Equal(new[] { service }, provider1.GetKeyedServices(KeyedService.AnyKey)); var provider2 = CreateServiceProvider(serviceCollection); Assert.Equal(new[] { service }, provider2.GetKeyedServices(KeyedService.AnyKey)); + // But we should be able to directly do a lookup on it Assert.Null(provider2.GetKeyedService(KeyedService.AnyKey)); } @@ -186,10 +189,11 @@ public void ResolveKeyedServicesAnyKeyConsistencyWithAnyKeyRegistration() var provider1 = CreateServiceProvider(serviceCollection); Assert.Same(any, provider1.GetKeyedService(KeyedService.AnyKey)); - Assert.Equal(new[] { service, any }, provider1.GetKeyedServices(KeyedService.AnyKey)); + Assert.Equal(new[] { service }, provider1.GetKeyedServices(KeyedService.AnyKey)); + // Check twice in different order to check caching var provider2 = CreateServiceProvider(serviceCollection); - Assert.Equal(new[] { service, any }, provider2.GetKeyedServices(KeyedService.AnyKey)); + Assert.Equal(new[] { service }, provider2.GetKeyedServices(KeyedService.AnyKey)); Assert.Same(any, provider2.GetKeyedService(KeyedService.AnyKey)); } @@ -278,7 +282,7 @@ public void ResolveKeyedServicesSingletonInstanceWithAnyKey() var provider = CreateServiceProvider(serviceCollection); var services = provider.GetKeyedServices>("some-key").ToList(); - Assert.Equal(new[] { service1, service2 }, services); + Assert.Equal(new[] { service2 }, services); } [Fact] diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs index 81268ba1b925eb..32245626bf59b0 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs @@ -368,6 +368,32 @@ void AddCallSite(ServiceCallSite callSite, int index) { callSiteChain.Remove(serviceIdentifier); } + + static bool KeysMatch(object? lookupKey, object? descriptorKey) + { + if (lookupKey == null && descriptorKey == null) + { + // Both are non keyed services + return true; + } + + if (lookupKey != null && descriptorKey != null) + { + // Both are keyed services + + // We don't want to return AnyKey registration, so ignore it + if (descriptorKey.Equals(KeyedService.AnyKey)) + return false; + + // Check if both keys are equal, or if the lookup key + // should matches all keys (except AnyKey) + return lookupKey.Equals(descriptorKey) + || lookupKey.Equals(KeyedService.AnyKey); + } + + // One is a keyed service, one is not + return false; + } } private static CallSiteResultCacheLocation GetCommonCacheLocation(CallSiteResultCacheLocation locationA, CallSiteResultCacheLocation locationB) @@ -696,24 +722,6 @@ internal bool IsService(ServiceIdentifier serviceIdentifier) serviceType == typeof(IServiceProviderIsKeyedService); } - /// - /// Returns true if both keys are null or equals, or if key1 is KeyedService.AnyKey and key2 is not null - /// - private static bool KeysMatch(object? key1, object? key2) - { - if (key1 == null && key2 == null) - return true; - - if (key1 != null && key2 != null) - { - return key1.Equals(key2) - || key1.Equals(KeyedService.AnyKey) - || key2.Equals(KeyedService.AnyKey); - } - - return false; - } - private struct ServiceDescriptorCacheItem { [DisallowNull] From 7d2277199592220f8cb08bd8662119fab4dd6b4f Mon Sep 17 00:00:00 2001 From: Benjamin Petit Date: Fri, 2 Feb 2024 15:41:34 +0100 Subject: [PATCH 3/9] Revert solution change --- ...crosoft.Extensions.DependencyInjection.sln | 32 +++++++------------ 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/Microsoft.Extensions.DependencyInjection.sln b/src/libraries/Microsoft.Extensions.DependencyInjection/Microsoft.Extensions.DependencyInjection.sln index fb3518cecab81d..008b98db7ff0da 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/Microsoft.Extensions.DependencyInjection.sln +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/Microsoft.Extensions.DependencyInjection.sln @@ -1,8 +1,4 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.9.34414.90 -MinimumVisualStudioVersion = 10.0.40219.1 +Microsoft Visual Studio Solution File, Format Version 12.00 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{30201F60-A891-4C3F-A1A6-DDDD1C8525E3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bcl.AsyncInterfaces", "..\Microsoft.Bcl.AsyncInterfaces\ref\Microsoft.Bcl.AsyncInterfaces.csproj", "{047FD3F2-B3A0-4639-B4F0-40D29E61725D}" @@ -47,11 +43,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{74C4FAFF-491 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{28065F40-B930-4A5D-95D8-A3BD5F86CE11}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{0B56ECAF-7B4A-4135-A343-1577ACE09920}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "tools\gen", "{0B56ECAF-7B4A-4135-A343-1577ACE09920}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5DD887A4-ED78-4782-B372-34176C27F0C1}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "tools\src", "{5DD887A4-ED78-4782-B372-34176C27F0C1}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{6FA0C03B-0901-4FD8-AF44-D4C9E5516C2F}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "tools\ref", "{6FA0C03B-0901-4FD8-AF44-D4C9E5516C2F}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{D6F10CF9-982E-4D09-A112-0CBFD2D46AFF}" EndProject @@ -139,32 +135,28 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {30201F60-A891-4C3F-A1A6-DDDD1C8525E3} = {5F2D3A6A-76D3-4C22-A7F7-8D73D67A98F8} + {2C6983BB-3CB4-4EB7-9AF1-2F24FE1ECEAB} = {5F2D3A6A-76D3-4C22-A7F7-8D73D67A98F8} + {F11DD75C-122C-4B98-9EED-F71551F9562A} = {5F2D3A6A-76D3-4C22-A7F7-8D73D67A98F8} + {1EE6CA66-6585-459D-8889-666D4C2D4C27} = {5F2D3A6A-76D3-4C22-A7F7-8D73D67A98F8} {047FD3F2-B3A0-4639-B4F0-40D29E61725D} = {E168C5B8-F2EB-4BDE-942A-59C1EB130D59} - {3068B34E-D975-4C11-B2F2-F10790051F2E} = {74C4FAFF-491D-448C-8CA0-F8E5FC838CC5} {6D90C067-5CCD-4443-81A5-B9C385011F68} = {E168C5B8-F2EB-4BDE-942A-59C1EB130D59} + {66E6ADF5-200F-41F3-9CA4-858EF69D2A61} = {E168C5B8-F2EB-4BDE-942A-59C1EB130D59} + {3068B34E-D975-4C11-B2F2-F10790051F2E} = {74C4FAFF-491D-448C-8CA0-F8E5FC838CC5} {9CD9F9EB-379C-44C1-9016-33DFEC821C76} = {74C4FAFF-491D-448C-8CA0-F8E5FC838CC5} {4532D9F9-1E0D-4A62-8038-D3454B255E86} = {74C4FAFF-491D-448C-8CA0-F8E5FC838CC5} - {66E6ADF5-200F-41F3-9CA4-858EF69D2A61} = {E168C5B8-F2EB-4BDE-942A-59C1EB130D59} {C5ECD02C-FA5A-4B56-9CA2-47AD8989714A} = {74C4FAFF-491D-448C-8CA0-F8E5FC838CC5} - {2C6983BB-3CB4-4EB7-9AF1-2F24FE1ECEAB} = {5F2D3A6A-76D3-4C22-A7F7-8D73D67A98F8} - {F11DD75C-122C-4B98-9EED-F71551F9562A} = {5F2D3A6A-76D3-4C22-A7F7-8D73D67A98F8} - {1EE6CA66-6585-459D-8889-666D4C2D4C27} = {5F2D3A6A-76D3-4C22-A7F7-8D73D67A98F8} {FD3C8D4F-0EAA-4575-A685-F77C6D340E60} = {28065F40-B930-4A5D-95D8-A3BD5F86CE11} {9E5124E4-BEDA-4B2D-9699-60E2A7B1881D} = {28065F40-B930-4A5D-95D8-A3BD5F86CE11} {C85FD264-C77B-44F3-926C-D61C5DAD369E} = {0B56ECAF-7B4A-4135-A343-1577ACE09920} {A2CD66D3-74F2-4608-A56E-F866CC494620} = {0B56ECAF-7B4A-4135-A343-1577ACE09920} + {0B56ECAF-7B4A-4135-A343-1577ACE09920} = {D6F10CF9-982E-4D09-A112-0CBFD2D46AFF} {45502850-3207-49B0-9F5D-5DE95091DB5A} = {5DD887A4-ED78-4782-B372-34176C27F0C1} {B1E4A89A-6451-4484-B42B-4D1DF21B6961} = {5DD887A4-ED78-4782-B372-34176C27F0C1} - {7E8189DE-6BBA-4CD5-9BA6-DF5ADBD027D3} = {6FA0C03B-0901-4FD8-AF44-D4C9E5516C2F} - {0B56ECAF-7B4A-4135-A343-1577ACE09920} = {D6F10CF9-982E-4D09-A112-0CBFD2D46AFF} {5DD887A4-ED78-4782-B372-34176C27F0C1} = {D6F10CF9-982E-4D09-A112-0CBFD2D46AFF} + {7E8189DE-6BBA-4CD5-9BA6-DF5ADBD027D3} = {6FA0C03B-0901-4FD8-AF44-D4C9E5516C2F} {6FA0C03B-0901-4FD8-AF44-D4C9E5516C2F} = {D6F10CF9-982E-4D09-A112-0CBFD2D46AFF} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {68A7BDA7-8093-433C-BF7A-8A6A7560BD02} EndGlobalSection - GlobalSection(SharedMSBuildProjectFiles) = preSolution - ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{a2cd66d3-74f2-4608-a56e-f866cc494620}*SharedItemsImports = 5 - ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{b1e4a89a-6451-4484-b42b-4d1df21b6961}*SharedItemsImports = 5 - EndGlobalSection -EndGlobal +EndGlobal \ No newline at end of file From 458cd2b823685f6aec361af71f36e850c70d50c4 Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Tue, 4 Mar 2025 14:50:58 -0600 Subject: [PATCH 4/9] Throw when AnyKey is used to resolve a single service --- ...edDependencyInjectionSpecificationTests.cs | 22 +++++++++++++---- ...crosoft.Extensions.DependencyInjection.sln | 2 +- .../src/Resources/Strings.resx | 3 +++ .../src/ServiceLookup/ThrowHelper.cs | 7 ++++++ .../src/ServiceProvider.cs | 24 +++++++++++++++++-- 5 files changed, 50 insertions(+), 8 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs index 4c3ade8cfca134..abf5684c2a9a5c 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs @@ -164,6 +164,12 @@ public void ResolveKeyedServicesAnyKeyWithAnyKeyRegistration() var allServices = provider.GetKeyedServices(KeyedService.AnyKey).ToList(); Assert.Equal(4, allServices.Count); Assert.Equal(new[] { service1, service2, service3, service4 }, allServices); + + var someKeyedServices = provider.GetKeyedServices("service").ToList(); + Assert.Equal(new[] { service2, service3, service4 }, someKeyedServices); + + var unkeyedServices = provider.GetServices().ToList(); + Assert.Equal(new[] { service5, service6 }, unkeyedServices); } [Fact] @@ -174,14 +180,13 @@ public void ResolveKeyedServicesAnyKeyConsistency() serviceCollection.AddKeyedSingleton("first-service", service); var provider1 = CreateServiceProvider(serviceCollection); - Assert.Null(provider1.GetKeyedService(KeyedService.AnyKey)); + Assert.Throws(() => provider1.GetKeyedService(KeyedService.AnyKey)); // We don't return KeyedService.AnyKey registration when listing services Assert.Equal(new[] { service }, provider1.GetKeyedServices(KeyedService.AnyKey)); var provider2 = CreateServiceProvider(serviceCollection); Assert.Equal(new[] { service }, provider2.GetKeyedServices(KeyedService.AnyKey)); - // But we should be able to directly do a lookup on it - Assert.Null(provider2.GetKeyedService(KeyedService.AnyKey)); + Assert.Throws(() => provider2.GetKeyedService(KeyedService.AnyKey)); } [Fact] @@ -194,13 +199,14 @@ public void ResolveKeyedServicesAnyKeyConsistencyWithAnyKeyRegistration() serviceCollection.AddKeyedSingleton(KeyedService.AnyKey, (sp, key) => any); var provider1 = CreateServiceProvider(serviceCollection); - Assert.Same(any, provider1.GetKeyedService(KeyedService.AnyKey)); Assert.Equal(new[] { service }, provider1.GetKeyedServices(KeyedService.AnyKey)); // Check twice in different order to check caching var provider2 = CreateServiceProvider(serviceCollection); Assert.Equal(new[] { service }, provider2.GetKeyedServices(KeyedService.AnyKey)); - Assert.Same(any, provider2.GetKeyedService(KeyedService.AnyKey)); + Assert.Same(any, provider2.GetKeyedService(new object())); + + Assert.Throws(() => provider2.GetKeyedService(KeyedService.AnyKey)); } [Fact] @@ -543,6 +549,9 @@ public void ResolveKeyedSingletonFromScopeServiceProvider() Assert.Null(scopeA.ServiceProvider.GetService()); Assert.Null(scopeB.ServiceProvider.GetService()); + Assert.Throws(() => scopeA.ServiceProvider.GetKeyedService(KeyedService.AnyKey)); + Assert.Throws(() => scopeB.ServiceProvider.GetKeyedService(KeyedService.AnyKey)); + var serviceA1 = scopeA.ServiceProvider.GetKeyedService("key"); var serviceA2 = scopeA.ServiceProvider.GetKeyedService("key"); @@ -567,6 +576,9 @@ public void ResolveKeyedScopedFromScopeServiceProvider() Assert.Null(scopeA.ServiceProvider.GetService()); Assert.Null(scopeB.ServiceProvider.GetService()); + Assert.Throws(() => scopeA.ServiceProvider.GetKeyedService(KeyedService.AnyKey)); + Assert.Throws(() => scopeB.ServiceProvider.GetKeyedService(KeyedService.AnyKey)); + var serviceA1 = scopeA.ServiceProvider.GetKeyedService("key"); var serviceA2 = scopeA.ServiceProvider.GetKeyedService("key"); diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/Microsoft.Extensions.DependencyInjection.sln b/src/libraries/Microsoft.Extensions.DependencyInjection/Microsoft.Extensions.DependencyInjection.sln index 6bd32107500064..31a6a2c833475a 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/Microsoft.Extensions.DependencyInjection.sln +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/Microsoft.Extensions.DependencyInjection.sln @@ -152,4 +152,4 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {68A7BDA7-8093-433C-BF7A-8A6A7560BD02} EndGlobalSection -EndGlobal \ No newline at end of file +EndGlobal diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.DependencyInjection/src/Resources/Strings.resx index e6f57dcbb5a1ee..2eb2adba7e0060 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/Resources/Strings.resx @@ -192,4 +192,7 @@ The type of the key used for lookup doesn't match the type in the constructor parameter with the ServiceKey attribute. + + KeyedService.AnyKey cannot be used to resolve a single service. + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ThrowHelper.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ThrowHelper.cs index ffacbf9bbc0521..cf9bedf8341d78 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ThrowHelper.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ThrowHelper.cs @@ -15,5 +15,12 @@ internal static void ThrowObjectDisposedException() { throw new ObjectDisposedException(nameof(IServiceProvider)); } + + [DoesNotReturn] + [MethodImpl(MethodImplOptions.NoInlining)] + internal static void ThrowInvalidOperationException_KeyedServiceAnyKeyUsedToResolveService() + { + throw new InvalidOperationException(SR.Format(SR.KeyedServiceAnyKeyUsedToResolveService, nameof(IServiceProvider), nameof(IServiceScopeFactory))); + } } } diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs index d3177c229e31ec..bd40b2c409ec59 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs @@ -112,7 +112,17 @@ internal ServiceProvider(ICollection serviceDescriptors, Serv => GetKeyedService(serviceType, serviceKey, Root); internal object? GetKeyedService(Type serviceType, object? serviceKey, ServiceProviderEngineScope serviceProviderEngineScope) - => GetService(new ServiceIdentifier(serviceKey, serviceType), serviceProviderEngineScope); + { + if (serviceKey == KeyedService.AnyKey) + { + if (!serviceType.IsGenericType || serviceType.GetGenericTypeDefinition() != typeof(IEnumerable<>)) + { + ThrowHelper.ThrowInvalidOperationException_KeyedServiceAnyKeyUsedToResolveService(); + } + } + + return GetService(new ServiceIdentifier(serviceKey, serviceType), serviceProviderEngineScope); + } /// /// Gets the service object of the specified type. @@ -122,7 +132,17 @@ internal ServiceProvider(ICollection serviceDescriptors, Serv /// The keyed service. /// The service wasn't found. public object GetRequiredKeyedService(Type serviceType, object? serviceKey) - => GetRequiredKeyedService(serviceType, serviceKey, Root); + { + if (serviceKey == KeyedService.AnyKey) + { + if (!serviceType.IsGenericType || serviceType.GetGenericTypeDefinition() != typeof(IEnumerable<>)) + { + ThrowHelper.ThrowInvalidOperationException_KeyedServiceAnyKeyUsedToResolveService(); + } + } + + return GetRequiredKeyedService(serviceType, serviceKey, Root); + } internal object GetRequiredKeyedService(Type serviceType, object? serviceKey, ServiceProviderEngineScope serviceProviderEngineScope) { From 8fc3117577b7b50920870b57e438bd3f8859b80f Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Tue, 4 Mar 2025 14:57:32 -0600 Subject: [PATCH 5/9] Remove unnecessary using statement --- .../src/KeyedDependencyInjectionSpecificationTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs index abf5684c2a9a5c..83a06f496e0541 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs @@ -6,7 +6,6 @@ using System.Security.Cryptography; using Microsoft.Extensions.DependencyInjection.Specification.Fakes; using Xunit; -using static Microsoft.Extensions.DependencyInjection.Specification.KeyedDependencyInjectionSpecificationTests; namespace Microsoft.Extensions.DependencyInjection.Specification { From cfad9baa56a9c7f811b7654f66891b6a307aaf74 Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Wed, 5 Mar 2025 12:57:23 -0600 Subject: [PATCH 6/9] Update doc --- .../src/ServiceProvider.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs index bd40b2c409ec59..bb05be370d8303 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs @@ -108,6 +108,9 @@ internal ServiceProvider(ICollection serviceDescriptors, Serv /// The type of the service to get. /// The key of the service to get. /// The keyed service. + /// The value is used for + /// when is not an enumerable based on . + /// public object? GetKeyedService(Type serviceType, object? serviceKey) => GetKeyedService(serviceType, serviceKey, Root); @@ -130,7 +133,9 @@ internal ServiceProvider(ICollection serviceDescriptors, Serv /// The type of the service to get. /// The key of the service to get. /// The keyed service. - /// The service wasn't found. + /// The service wasn't found or the value is used + /// for when is not an enumerable based on . + /// public object GetRequiredKeyedService(Type serviceType, object? serviceKey) { if (serviceKey == KeyedService.AnyKey) From 85b1ff9073c17f46e30f4d3c5889d76944dc77f5 Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Fri, 7 Mar 2025 13:07:02 -0600 Subject: [PATCH 7/9] Add IEnumerable test for AnyKey, nullkey, keyed and unkeyed --- ...edDependencyInjectionSpecificationTests.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs index 83a06f496e0541..b4d55a679f2d81 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs @@ -13,6 +13,63 @@ public abstract partial class KeyedDependencyInjectionSpecificationTests { protected abstract IServiceProvider CreateServiceProvider(IServiceCollection collection); + [Fact] + public void QueryWithIEnumerable() + { + Service service = new(); + Service keyedService = new(); + Service anykeyService = new(); + Service nullkeyService = new(); + + ServiceCollection serviceCollection = new(); + serviceCollection.AddSingleton(service); + serviceCollection.AddSingleton(service); + serviceCollection.AddKeyedSingleton("keyedService", keyedService); + serviceCollection.AddKeyedSingleton("keyedService", keyedService); + serviceCollection.AddKeyedSingleton(KeyedService.AnyKey, anykeyService); + serviceCollection.AddKeyedSingleton(KeyedService.AnyKey, anykeyService); + serviceCollection.AddKeyedSingleton(null, nullkeyService); + serviceCollection.AddKeyedSingleton(null, nullkeyService); + + IServiceProvider provider = CreateServiceProvider(serviceCollection); + + /* + * Table for what results are included in GetServices()\GetKeyedServices(): + * + * Query | Keyed? | Unkeyed? | AnyKey? | nullkey? + * ------------------------------------------------------------------- + * GetServices(Type) | no | yes | no | yes + * GetKeyedServices(null) | no | yes | no | yes + * GetKeyedServices(AnyKey) | yes | no | no | no + * GetKeyedServices(key) | yes | no | no | no + * + * Summary: + * - A null key is the same as unkeyed. This allows the KeyServices APIs to support both keyed and unkeyed. + * - AnyKey is a special case of Keyed. + * - It is not possible to query for AnyKey registrations using IEnumerable; a singleton query resolves. + */ + + // Query with unkeyed (which is really keyed by Type). + Assert.Equal( + new[] { service, service, nullkeyService, nullkeyService }, + provider.GetServices()); + + // Query with null key. + Assert.Equal( + new[] { service, service, nullkeyService, nullkeyService }, + provider.GetKeyedServices(null)); + + // Query with AnyKey. + Assert.Equal( + new[] { keyedService, keyedService }, + provider.GetKeyedServices(KeyedService.AnyKey)); + + // Query with standard keyed. + Assert.Equal( + new[] { keyedService, keyedService }, + provider.GetKeyedServices("keyedService")); + } + [Fact] public void ResolveKeyedService() { From e4736c5aff7def04d16d8b45f6e3b7bc29e807fb Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Thu, 13 Mar 2025 09:17:05 -0500 Subject: [PATCH 8/9] Update the combinational test --- ...edDependencyInjectionSpecificationTests.cs | 69 ++++++++++++------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs index b4d55a679f2d81..966868a67f3713 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs @@ -14,60 +14,81 @@ public abstract partial class KeyedDependencyInjectionSpecificationTests protected abstract IServiceProvider CreateServiceProvider(IServiceCollection collection); [Fact] - public void QueryWithIEnumerable() + public void CombinationalRegistration() { - Service service = new(); - Service keyedService = new(); - Service anykeyService = new(); - Service nullkeyService = new(); + Service service1 = new(); + Service service2 = new(); + Service keyedService1 = new(); + Service keyedService2 = new(); + Service anykeyService1 = new(); + Service anykeyService2 = new(); + Service nullkeyService1 = new(); + Service nullkeyService2 = new(); ServiceCollection serviceCollection = new(); - serviceCollection.AddSingleton(service); - serviceCollection.AddSingleton(service); - serviceCollection.AddKeyedSingleton("keyedService", keyedService); - serviceCollection.AddKeyedSingleton("keyedService", keyedService); - serviceCollection.AddKeyedSingleton(KeyedService.AnyKey, anykeyService); - serviceCollection.AddKeyedSingleton(KeyedService.AnyKey, anykeyService); - serviceCollection.AddKeyedSingleton(null, nullkeyService); - serviceCollection.AddKeyedSingleton(null, nullkeyService); + serviceCollection.AddSingleton(service1); + serviceCollection.AddSingleton(service2); + serviceCollection.AddKeyedSingleton(null, nullkeyService1); + serviceCollection.AddKeyedSingleton(null, nullkeyService2); + serviceCollection.AddKeyedSingleton(KeyedService.AnyKey, anykeyService1); + serviceCollection.AddKeyedSingleton(KeyedService.AnyKey, anykeyService2); + serviceCollection.AddKeyedSingleton("keyedService", keyedService1); + serviceCollection.AddKeyedSingleton("keyedService", keyedService2); IServiceProvider provider = CreateServiceProvider(serviceCollection); /* - * Table for what results are included in GetServices()\GetKeyedServices(): + * Table for what results are included: * - * Query | Keyed? | Unkeyed? | AnyKey? | nullkey? + * Query | Keyed? | Unkeyed? | AnyKey? | null key? * ------------------------------------------------------------------- * GetServices(Type) | no | yes | no | yes + * GetService(Type) | no | yes | no | yes + * * GetKeyedServices(null) | no | yes | no | yes + * GetKeyedService(null) | no | yes | no | yes + * * GetKeyedServices(AnyKey) | yes | no | no | no + * GetKeyedService(AnyKey) | throw | throw | throw | throw + * * GetKeyedServices(key) | yes | no | no | no + * GetKeyedService(key) | yes | no | yes | no * * Summary: * - A null key is the same as unkeyed. This allows the KeyServices APIs to support both keyed and unkeyed. * - AnyKey is a special case of Keyed. - * - It is not possible to query for AnyKey registrations using IEnumerable; a singleton query resolves. + * - AnyKey registrations are not returned with GetKeyedServices(AnyKey) and GetKeyedService(AnyKey) always throws. + * - For IEnumerable, the ordering of the results are in registration order. + * - For a singleton resolve, the last match wins. */ - // Query with unkeyed (which is really keyed by Type). + // Unkeyed (which is really keyed by Type). Assert.Equal( - new[] { service, service, nullkeyService, nullkeyService }, + new[] { service1, service2, nullkeyService1, nullkeyService2 }, provider.GetServices()); - // Query with null key. + Assert.Equal(nullkeyService2, provider.GetService()); + + // Null key. Assert.Equal( - new[] { service, service, nullkeyService, nullkeyService }, + new[] { service1, service2, nullkeyService1, nullkeyService2 }, provider.GetKeyedServices(null)); - // Query with AnyKey. + Assert.Equal(nullkeyService2, provider.GetKeyedService(null)); + + // AnyKey. Assert.Equal( - new[] { keyedService, keyedService }, + new[] { keyedService1, keyedService2 }, provider.GetKeyedServices(KeyedService.AnyKey)); - // Query with standard keyed. + Assert.Throws(() => provider.GetKeyedService(KeyedService.AnyKey)); + + // Keyed. Assert.Equal( - new[] { keyedService, keyedService }, + new[] { keyedService1, keyedService2 }, provider.GetKeyedServices("keyedService")); + + Assert.Equal(keyedService2, provider.GetKeyedService("keyedService")); } [Fact] From 2eb0d18e3eba606b842eb523cb3d2183ac7d274b Mon Sep 17 00:00:00 2001 From: Steve Harter Date: Thu, 13 Mar 2025 15:24:09 -0500 Subject: [PATCH 9/9] Add ordering test for GetKeyedService(AnyKey) --- ...edDependencyInjectionSpecificationTests.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs index 966868a67f3713..7cee6ebd40ad18 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs @@ -286,6 +286,27 @@ public void ResolveKeyedServicesAnyKeyConsistencyWithAnyKeyRegistration() Assert.Throws(() => provider2.GetKeyedService(KeyedService.AnyKey)); } + [Fact] + public void ResolveKeyedServicesAnyKeyOrdering() + { + var serviceCollection = new ServiceCollection(); + var service1 = new Service(); + var service2 = new Service(); + var service3 = new Service(); + + serviceCollection.AddKeyedSingleton("A-service", service1); + serviceCollection.AddKeyedSingleton("B-service", service2); + serviceCollection.AddKeyedSingleton("A-service", service3); + + var provider = CreateServiceProvider(serviceCollection); + + // The order should be in registration order, and not grouped by key for example. + // Although this isn't necessarily a requirement, it is the current behavior. + Assert.Equal( + new[] { service1, service2, service3 }, + provider.GetKeyedServices(KeyedService.AnyKey)); + } + [Fact] public void ResolveKeyedGenericServices() {