Bruno Zell
Bruno Zell

Reputation: 8541

Get open ended generic service in Microsofts dependency injection

Suppose we have the following services:

interface IService { }
interface IService<T> : IService {
    T Get();
}

In ASP.Net-Core, after we have registered some implementations with different T we can get all registered services like this:

IEnumerable<IService> services = serviceProvider.GetServices<IService>();

Now, because I need access to the generic type parameter from the other interface that is not an option. How can I retrieve all implementations of IService<T> without losing the generic type? Something like:

IEnumerable<IService<T>> services = serviceProvider.GetServices<IService<T>>();
foreach (var s in services) {
    Method(s);
}

// Here we have a generic method I don't have control over.
// I want to call the method for each `T` registered in DI
void Method<T>(IService<T> service) {
    Type t = typeof(T); // This here will resolve to the actual type, different in each call. Not object or whatever less derived.
}

And all of this should have a somewhat decent performance.

Upvotes: 5

Views: 5486

Answers (2)

Bruno Zell
Bruno Zell

Reputation: 8541

Following up on the helpful comments from @JeroenMostert I discovered a way to do exactly what I want. As he pointed out since we don't know the generic parameter type at compile time, we can't statically bind that method call. What we need is late binding.

Reflection is a type of late binding, but there is a better solution to it: dynamic The example from the answer would become:

IEnumerable<IService> services = serviceProvider.GetServices<IService>();
foreach (var s in services) {
    Method((dynamic)s);
}

void Method<T>(IService<T> service) {
    // This here will resolve to the actual type, different in each call. Not object or whatever less derived.
    Type t = typeof(T);
}

The cast to dynamic will postpone method binding until runtime when the actual type of s is known. Then it will look for the best fitting overload (if there is none an exception would be thrown). This approach has some advantages to using reflection:

  • We don't have to care about caching. The DLR (Dynamic Language Runtime) will handle it for us.
  • We bind late, but get as much static analysis as possible. E.g. all other argument types get checked and could result in an compile time error when invalid.
  • It is shorter and easier to write.

You can read an excellent in-depth post about this approach and how it compares to reflection here.

Upvotes: 6

Steven
Steven

Reputation: 172646

There are 2 options that I can think of:

  1. Inject an IService and filter out the incompatible types:

    serviceProvider.GetServices<IService>().OfType<IService<T>>();
    
  2. Make duplicate registrations:

    services.AddScoped<IService, FooService>();
    services.AddScoped<IService, BarService1>();
    services.AddScoped<IService, BarService2>();
    services.AddScoped<IService<Foo>, FooService>();
    services.AddScoped<IService<Bar>, BarService1>();
    services.AddScoped<IService<Bar>, BarService2>();
    
    serviceProvider.GetServices<IService<Bar>>(); // returns 2 services
    serviceProvider.GetServices<IService>(); // returns 3 services
    

    Do note, however, that you need to be careful with these duplicate registrations to not fall into the Torn Lifestyles trap. This can happen when a service is registered as Scoped or Singleton. To combat this, you need to change the above registrations to the following:

    services.AddScoped<FooService>();
    services.AddScoped<BarService1>();
    services.AddScoped<BarService2>();
    
    services.AddScoped<IService>(c => c.GetRequiredService<FooService>());
    services.AddScoped<IService>(c => c.GetRequiredService<BarService1>());
    services.AddScoped<IService>(c => c.GetRequiredService<BarService2>());
    services.AddScoped<IService<Foo>>(c => c.GetRequiredService<FooService>());
    services.AddScoped<IService<Bar>>(c => c.GetRequiredService<BarService1>());
    services.AddScoped<IService<Bar>>(c => c.GetRequiredService<BarService2>());
    

Additionally, as you seem to be using a different container under the covers, you might be able to reduce the boilerplate using Auto-Registration (a.k.a. assembly scanning).

Upvotes: 3

Related Questions