How to get a dictionary of keyed services in ASP.NET Core 8

1k Views Asked by At

In ASP.NET Core 8, we have the option to register services using a key. Typically, injecting IEnumerable<ICustom> in a constructor will return all services that are registered of the ICustom.

But, how can I resolve a dictionary of the service along with their keys? For example, IDictionary<string, ICustom> so I can access both the service and its key?

3

There are 3 best solutions below

0
On BEST ANSWER

There's nothing included OOTB to get a dictionary of keyed services, but you can add this behavior using the following extension method:

// License: This code is published under the MIT license.
// Source: https://stackoverflow.com/questions/77559201/
public static class KeyedServiceExtensions
{
    public static void AllowResolvingKeyedServicesAsDictionary(
        this IServiceCollection sc)
    {
        // KeyedServiceCache caches all the keys of a given type for a
        // specific service type. By making it a singleton we only have
        // determine the keys once, which makes resolving the dict very fast.
        sc.AddSingleton(typeof(KeyedServiceCache<,>));
        
        // KeyedServiceCache depends on the IServiceCollection to get
        // the list of keys. That's why we register that here as well, as it
        // is not registered by default in MS.DI.
        sc.AddSingleton(sc);
        
        // Last we make the registration for the dictionary itself, which maps
        // to our custom type below. This registration must be  transient, as
        // the containing services could have any lifetime and this registration
        // should by itself not cause Captive Dependencies.
        sc.AddTransient(typeof(IDictionary<,>), typeof(KeyedServiceDictionary<,>));
        
        // For completeness, let's also allow IReadOnlyDictionary to be resolved.
        sc.AddTransient(
            typeof(IReadOnlyDictionary<,>), typeof(KeyedServiceDictionary<,>));
    }

    // We inherit from ReadOnlyDictionary, to disallow consumers from changing
    // the wrapped dependencies while reusing all its functionality. This way
    // we don't have to implement IDictionary<T,V> ourselves; too much work.
    private sealed class KeyedServiceDictionary<TKey, TService>(
        KeyedServiceCache<TKey, TService> keys, IServiceProvider provider)
        : ReadOnlyDictionary<TKey, TService>(Create(keys, provider))
        where TKey : notnull
        where TService : notnull
    {
        private static Dictionary<TKey, TService> Create(
            KeyedServiceCache<TKey, TService> keys, IServiceProvider provider)
        {
            var dict = new Dictionary<TKey, TService>(capacity: keys.Keys.Length);

            foreach (TKey key in keys.Keys)
            {
                dict[key] = provider.GetRequiredKeyedService<TService>(key);
            }

            return dict;
        }
    }

    private sealed class KeyedServiceCache<TKey, TService>(IServiceCollection sc)
        where TKey : notnull
        where TService : notnull
    {
        // Once this class is resolved, all registrations are guaranteed to be
        // made, so we can, at that point, safely iterate the collection to get
        // the keys for the service type.
        public TKey[] Keys { get; } = (
            from service in sc
            where service.ServiceKey != null
            where service.ServiceKey!.GetType() == typeof(TKey)
            where service.ServiceType == typeof(TService)
            select (TKey)service.ServiceKey!)
            .ToArray();
    }
}

After adding this code to your project, you use it as follows:

services.AllowResolvingKeyedServicesAsDictionary();

This allows you to resolve keyed registrations as an IDictionary<TKey, TService>. For instance, take a look at this fully working console application example:

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();

services.AllowResolvingKeyedServicesAsDictionary();

services.AddKeyedTransient<IService, Service1>("a");
services.AddKeyedTransient<IService, Service2>("b");
services.AddKeyedTransient<IService, Service3>("c");
services.AddKeyedTransient<IService, Service4>("d");
services.AddKeyedTransient<IService, Service5>(5);

var provider = services.BuildServiceProvider(validateScopes: true);

using (var scope = provider.CreateAsyncScope())
{
    var stringDict = scope.ServiceProvider
        .GetRequiredService<IDictionary<string, IService>>();

    Console.WriteLine("IService registrations with string keys:");
    foreach (var pair in stringDict)
    {
        Console.WriteLine($"{pair.Key}: {pair.Value.GetType().Name}");
    }

    var intDict = scope.ServiceProvider
        .GetRequiredService<IReadOnlyDictionary<int, IService>>();

    Console.WriteLine("IService registrations with int keys:");
    foreach (var pair in intDict)
    {
        Console.WriteLine($"{pair.Key}: {pair.Value.GetType().Name}");
    }
}

public class IService { }

public class Service1 : IService { }
public class Service2 : IService { }
public class Service3 : IService { }
public class Service4 : IService { }
public class Service5 : IService { }

When running, the application will produce the following output:

IService registrations with string keys:
a: Service1
b: Service2
c: Service3
d: Service4
IService registrations with int keys:
5: Service5
3
On

I have not found an option to do this out of the box but you can try to make something yourself by manipulating service descriptors. Something to get you started:

var services = new ServiceCollection();
services.AddKeyedSingleton("A", new MyTest("A"));
services.AddKeyedSingleton("B", new MyTest("B"));

var descriptors = services
    .Where(sd => sd.IsKeyedService && sd.ServiceType == typeof(MyTest));

services.AddSingleton<IDictionary<object, MyTest>>(p => descriptors
    .Select(d => d.ServiceKey)
    .Distinct()
    .ToDictionary(k => k, k => p.GetKeyedService<MyTest>(k)));

var sp = services.BuildServiceProvider();
var myTestClasses = sp.GetService<IDictionary<object, MyTest>>();

record MyTest(string Name);

This code can be made more generic so the key and service types are provided as generic type parameters:

RegisterKeyedDictionary<string, MyTest>(services);

void RegisterKeyedDictionary<TKey, T>(ServiceCollection serviceCollection)
{
    var keys = serviceCollection
        .Where(sd => sd.IsKeyedService && sd.ServiceType == typeof(T))
        .Select(d => d.ServiceKey)
        .Distinct()
        .Select(k => (TKey)k)
        .ToList();

    serviceCollection.AddTransient<IDictionary<TKey, T>>(p => keys
        .ToDictionary(k => k, k => p.GetKeyedService<T>(k)));
}

var sp = services.BuildServiceProvider();
var myTestClasses = sp.GetService<IDictionary<string, MyTest>>();
0
On

I have a somewhat simpler solution:

//register
services.AddSingleton(new KeyedServicesKeyCache(services));
...
//implementation
internal class KeyedServicesKeyCache(IServiceCollection services)
{
   private readonly Dictionary<Type, IEnumerable<object>> _cache = [];
   public IEnumerable<object> GetAllServiceKeys<T>() => GetAllServiceKeys(typeof(T));
   public IEnumerable<object> GetAllServiceKeys(Type t) => _cache.TryGetValue(t, out var c) ? c : _cache[t] = services.Where(x=>x.IsKeyedService && x.ServiceType == t).Select(x=>x.ServiceKey).ToList();
}
...
//usage
sp.GetRequiredService<KeyedServicesKeyCache>().GetAllServiceKeys<T>().ToDictionary(x=>x, x => sp.GetRequiredKeyedService<T>(x));
//or as a simple IEnumerable
sp.GetRequiredService<KeyedServicesKeyCache>().GetAllServiceKeys<T>().Select(x => sp.GetRequiredKeyedService<T>(x));

and optionally add a helper class to create instances:

//register
services.AddTransient(typeof(IResolveAllKeyedServices<>), typeof(ResolveAllKeyedServices<>));
//implementation
public interface IResolveAllKeyedServices<out T> : IEnumerable<T> where T : notnull;

internal class ResolveAllKeyedServices<T>(KeyedServicesKeyCache cache, IServiceProvider sp) : IResolveAllKeyedServices<T> where T : notnull
{
   public IEnumerator<T>   GetEnumerator() => cache.GetAllServiceKeys<T>().Select(sp.GetRequiredKeyedService<T>).GetEnumerator();
   IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
//usage
IEnumerable<T> instances = sp.GetRequiredService<IResolveAllKeyedServices<T>>();