Search code examples
c#.netdependency-injection.net-8.0servicecollection

Get all implementations of a given type from IServiceProvider regardless of if they are keyed or not


WHY THIS ISNT A DUPLICATE

NOT TO BE CONFUSED WITH "HOW DO I GET THE KEY AND TYPE FROM KEYED SERVICES", that question only talks about how to get all the keyed types, maybe that's part of the answer but does not talk about non-keyed types and how to get them factored in, as someone who spent ages Googling this already the answers in that question do not answer the question here, and for searchability purposes that question is far more specific than this which is a much higher level "ALL IMPLEMENTATIONS" not just "KEYED IMPLEMENTATIONS" as requested in there.

ACTUAL QUESTION

Since .net 8 it looks like Microsoft has added the ability to have keyed services which seems great, but i've noticed that calling serviceProvider.GetServices(typeof(SomeInterface)); will not return instances that also have key information.

For example:

var collection = new ServiceCollection();
collection.AddKeyedSingleton<ITestInterface, TestClass1>("test1");
collection.AddKeyedSingleton<ITestInterface, TestClass2>("test2");
collection.AddSingleton<ITestInterface, TestClass3>();

var serviceProvider = collection.BuildServiceProvider();
var implementations = serviceProvider.GetServices<ITestInterface>();
Assert.NotNull(implementations);
Assert.NotEmpty(implementations);

// Expecting TestClass1, TestClass2, TestClass3
Assert.True(implementations.Count() == 3); 
// Actually just TestClass3

I am unsure of the expected behaviour of the above test, I would assume it should pass and return all bindings of that given type. So then raises the question of how to get the expected outcome from the serviceProvider.

I thought maybe I would need to use serviceProvider.GetKeyedServices<ITestInterface>() but unfortunately that requires you to provide a key, and in this case we want to ignore the keying and just get all implementations of a given type.

So can anyone advise on how to get the expected behaviour? even if it requires a union between 2 service provider methods.


Solution

  • The behavior you are experiencing is by design, although Microsoft could very well have chosen a behavior where keyed services are part of the collection. But by default, they are not.

    While this answer of mine explains how to resolve all keyed registrations as an IDictionary<TKey, TService>, that doesn't get you the behavior you wish to see.

    This behavior, however, can be achieved using a custom extension method:

    public static void AddBothKeyedAndDefaultSingleton<TService, TImplementation>(
        IServiceCollection services, string key)
        where TService : class
        where TImplementation : class, TService
    {
        services.AddKeyedSingleton<TService, TImplementation>(key);
        services.AddSingleton(sp => sp.GetRequiredKeyedService<TService>(key));
    }
    

    If you run your test with this extension method, you get the expected output:

    var collection = new ServiceCollection();
    collection.AddBothKeyedAndDefaultSingleton<ITestInterface, TestClass1>("test1");
    collection.AddBothKeyedAndDefaultSingleton<ITestInterface, TestClass2>("test2");
    collection.AddSingleton<ITestInterface, TestClass3>();
    
    var serviceProvider = collection.BuildServiceProvider();
    var implementations = serviceProvider.GetServices<ITestInterface>();
    
    Assert.True(implementations.Count() == 3); 
    

    You might be tempted to iterate the entire IServiceCollection and add an extra registration for every keyed registration, making this a global behavior change, but that would be a grave mistake. Other reusable libraries and framework components that you are using (or updates you will be installing in the future) might very well depend on the current behavior.