5

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.

Bruno Zell
  • 7,761
  • 5
  • 38
  • 46
  • Does `serviceProvider.GetServices` return all `IService` instances as well? If so, that's your obvious in (you can use `Type.GetGenericArguments` and pass this type directly or use `.MakeGenericType/.MakeGenericMethod`), if not, the problem seems rather unsolvable. – Jeroen Mostert Nov 27 '18 at 21:31
  • @JeroenMostert In my specific case it actually does (Using Autofac behind the service provider). This is probably the way to go for me, but I want to check if this is possible with using pure generic without reflection. And if it is possible then removing the non-generic interface `IService` altogether. – Bruno Zell Nov 27 '18 at 21:40
  • It's not possible to do without any reflection at all because generics are compile time, and an open-ended method like `GetServices` will only ever produce run-time types. You can apply a bunch of tricks to speed up or cache or pre-compile the reflection, but you can't eliminate it entirely. – Jeroen Mostert Nov 27 '18 at 21:43
  • @JeroenMostert Can you please give me some references about pre-compiling the reflection? Never heard about it and it seems interresting. – Bruno Zell Nov 27 '18 at 21:45
  • 1
    This basically involves using `Reflection.Emit` and/or expression trees to emit the type-specific code at runtime and cache it. This is obviously even slower than reflection if the code only runs once (since you reflect *and* compile code) but when you start running things in tight loops it pays off. Look in the internals of any ORM (like Dapper or Entity Framework or even AutoMapper) for more details. In this case I'm assuming you only ever go through the collection once, so it really wouldn't pay off. – Jeroen Mostert Nov 27 '18 at 21:48
  • @JeroenMostert Thank you for your input. I investigated more on it and I found a [quite good solution](https://stackoverflow.com/a/53916195/5185376) to my problem. – Bruno Zell Dec 24 '18 at 17:22

2 Answers2

5

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.

Bruno Zell
  • 7,761
  • 5
  • 38
  • 46
3

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).

Steven
  • 166,672
  • 24
  • 332
  • 435