From ed0f25ba36d0b70787d5928bf1a2074e480a8c11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 09:33:17 +0000 Subject: [PATCH 1/2] Initial plan From 2e5eeb157689ff9b32471a3300e7c83d994b0acb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 10:01:36 +0000 Subject: [PATCH 2/2] Add service key discovery API for keyed DI Co-authored-by: rosebyte <14963300+rosebyte@users.noreply.github.com> --- ...nsions.DependencyInjection.Abstractions.cs | 5 +++ .../src/IServiceKeysProvider.cs | 21 +++++++++++ .../ServiceProviderKeyedServiceExtensions.cs | 18 ++++++++++ ...edDependencyInjectionSpecificationTests.cs | 16 +++++++++ .../src/ServiceLookup/CallSiteFactory.cs | 36 +++++++++++++++++++ .../ServiceProviderEngineScope.cs | 12 ++++++- .../src/ServiceProvider.cs | 14 +++++++- .../ServiceProviderServiceExtensionsTest.cs | 13 +++++++ 8 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/IServiceKeysProvider.cs diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.cs b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.cs index ed9f0323d20685..304088aee08160 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.cs @@ -66,6 +66,10 @@ public partial interface IKeyedServiceProvider : System.IServiceProvider object? GetKeyedService(System.Type serviceType, object? serviceKey); object GetRequiredKeyedService(System.Type serviceType, object? serviceKey); } + public partial interface IServiceKeysProvider + { + System.Collections.Generic.IEnumerable GetServiceKeys(System.Type serviceType); + } public partial interface ISupportRequiredService { object GetRequiredService(System.Type serviceType); @@ -230,6 +234,7 @@ public static partial class ServiceProviderKeyedServiceExtensions public static object? GetKeyedService(this System.IServiceProvider provider, System.Type serviceType, object? serviceKey) { throw null; } public static object GetRequiredKeyedService(this System.IServiceProvider provider, System.Type serviceType, object? serviceKey) { throw null; } public static T GetRequiredKeyedService(this System.IServiceProvider provider, object? serviceKey) where T : notnull { throw null; } + public static System.Collections.Generic.IEnumerable? GetServiceKeys(this System.IServiceProvider provider) { throw null; } } public static partial class ServiceProviderServiceExtensions { diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/IServiceKeysProvider.cs b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/IServiceKeysProvider.cs new file mode 100644 index 00000000000000..a66bc84f6192b9 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/IServiceKeysProvider.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// + /// Provides known service keys for a service type. + /// + public interface IServiceKeysProvider + { + /// + /// Gets known service keys for the specified . + /// + /// The type of service to get known keys for. + /// An enumeration of known service keys for . + IEnumerable GetServiceKeys(Type serviceType); + } +} diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ServiceProviderKeyedServiceExtensions.cs b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ServiceProviderKeyedServiceExtensions.cs index 094fa4551c844f..7170bd3420adbc 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ServiceProviderKeyedServiceExtensions.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ServiceProviderKeyedServiceExtensions.cs @@ -117,5 +117,23 @@ public static IEnumerable GetKeyedServices(this IServiceProvider provider, Type? genericEnumerable = typeof(IEnumerable<>).MakeGenericType(serviceType); return (IEnumerable)provider.GetRequiredKeyedService(genericEnumerable, serviceKey); } + + /// + /// Get all known service keys for service type from the . + /// + /// The type of service object to get known keys for. + /// The to retrieve the service keys from. + /// An enumeration of known service keys for or if not supported. + public static IEnumerable? GetServiceKeys(this IServiceProvider provider) + { + ArgumentNullException.ThrowIfNull(provider); + + if (provider is IServiceKeysProvider serviceKeysProvider) + { + return serviceKeysProvider.GetServiceKeys(typeof(T)); + } + + return null; + } } } 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 20d0a82becc3b3..a1b21c8a674dfe 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Specification.Tests/src/KeyedDependencyInjectionSpecificationTests.cs @@ -250,6 +250,22 @@ public void ResolveKeyedServicesAnyKeyWithAnyKeyRegistration() Assert.Equal(new[] { service5, service6 }, unkeyedServices); } + [Fact] + public void ResolveServiceKeys() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(); + serviceCollection.AddKeyedSingleton("first-service", new Service()); + serviceCollection.AddKeyedSingleton("service", new Service()); + serviceCollection.AddKeyedSingleton("service", new Service()); + serviceCollection.AddKeyedSingleton(null, new Service()); + serviceCollection.AddKeyedTransient(KeyedService.AnyKey, (sp, key) => new Service()); + + var provider = CreateServiceProvider(serviceCollection); + + Assert.Equal(new object?[] { "first-service", "service", null }, provider.GetServiceKeys()); + } + [Fact] public void ResolveKeyedServicesAnyKeyConsistency() { diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs index 3c3d3c3e897cc4..9588aad8a62d6f 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/CallSiteFactory.cs @@ -779,6 +779,42 @@ public void Add(ServiceIdentifier serviceIdentifier, ServiceCallSite serviceCall _callSiteCache[new ServiceCacheKey(serviceIdentifier, DefaultSlot)] = serviceCallSite; } + internal IEnumerable GetServiceKeys(Type serviceType) + { + ArgumentNullException.ThrowIfNull(serviceType); + + List? serviceKeys = null; + HashSet? serviceKeySet = null; + + for (int i = 0; i < _descriptors.Length; i++) + { + ServiceDescriptor descriptor = _descriptors[i]; + if (!descriptor.IsKeyedService || descriptor.ServiceKey == KeyedService.AnyKey) + { + continue; + } + + if (!ShouldCreateExact(descriptor.ServiceType, serviceType) && + !ShouldCreateOpenGeneric(descriptor.ServiceType, serviceType)) + { + continue; + } + + object? serviceKey = descriptor.ServiceKey; + if (serviceKeySet?.Contains(serviceKey) == true) + { + continue; + } + + serviceKeySet ??= new HashSet(); + serviceKeys ??= new List(); + serviceKeySet.Add(serviceKey); + serviceKeys.Add(serviceKey); + } + + return serviceKeys is null ? Array.Empty() : serviceKeys; + } + public bool IsService(Type serviceType) => IsService(new ServiceIdentifier(null, serviceType)); public bool IsKeyedService(Type serviceType, object? key) => IsService(new ServiceIdentifier(key, serviceType)); diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs index edeb6330091ece..8ed617a19cb6aa 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceProviderEngineScope.cs @@ -13,7 +13,7 @@ namespace Microsoft.Extensions.DependencyInjection.ServiceLookup { [DebuggerDisplay("{DebuggerToString(),nq}")] [DebuggerTypeProxy(typeof(ServiceProviderEngineScopeDebugView))] - internal sealed class ServiceProviderEngineScope : IServiceScope, IServiceProvider, IKeyedServiceProvider, IAsyncDisposable, IServiceScopeFactory + internal sealed class ServiceProviderEngineScope : IServiceScope, IServiceProvider, IKeyedServiceProvider, IServiceKeysProvider, IAsyncDisposable, IServiceScopeFactory { // For testing and debugging only internal IList Disposables => _disposables ?? (IList)Array.Empty(); @@ -71,6 +71,16 @@ public object GetRequiredKeyedService(Type serviceType, object? serviceKey) return RootProvider.GetRequiredKeyedService(serviceType, serviceKey, this); } + public IEnumerable GetServiceKeys(Type serviceType) + { + if (_disposed) + { + ThrowHelper.ThrowObjectDisposedException(); + } + + return RootProvider.GetServiceKeys(serviceType); + } + public IServiceProvider ServiceProvider => this; public IServiceScope CreateScope() => RootProvider.CreateScope(); diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs index 21013478a1faf4..184977cd2fba6b 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs @@ -17,7 +17,7 @@ namespace Microsoft.Extensions.DependencyInjection /// [DebuggerDisplay("{DebuggerToString(),nq}")] [DebuggerTypeProxy(typeof(ServiceProviderDebugView))] - public sealed class ServiceProvider : IServiceProvider, IKeyedServiceProvider, IDisposable, IAsyncDisposable + public sealed class ServiceProvider : IServiceProvider, IKeyedServiceProvider, IServiceKeysProvider, IDisposable, IAsyncDisposable { private readonly CallSiteValidator? _callSiteValidator; @@ -167,6 +167,18 @@ internal object GetRequiredKeyedService(Type serviceType, object? serviceKey, Se return service; } + /// + /// Gets all known service keys for the specified type. + /// + /// The type of service to get known keys for. + /// An enumeration of known service keys for the specified . + public IEnumerable GetServiceKeys(Type serviceType) + { + ArgumentNullException.ThrowIfNull(serviceType); + + return CallSiteFactory.GetServiceKeys(serviceType); + } + internal bool IsDisposed() => _disposed; /// diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderServiceExtensionsTest.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderServiceExtensionsTest.cs index d9a2113465d277..3579e90ed8a1cb 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderServiceExtensionsTest.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderServiceExtensionsTest.cs @@ -182,6 +182,19 @@ public void NonGeneric_GetServices_Returns_EmptyArray_WhenNoServicesAvailable() Assert.IsType(services); } + [Fact] + public void GetServiceKeys_ReturnsNull_WhenProviderDoesNotSupportServiceKeys() + { + // Arrange + var serviceProvider = new RequiredServiceSupportingProvider(); + + // Act + var serviceKeys = serviceProvider.GetServiceKeys(); + + // Assert + Assert.Null(serviceKeys); + } + [Fact] public void GetServices_WithBuildServiceProvider_Returns_EmptyList_WhenNoServicesAvailable() {