injecting/resolving generic and none generic services which inherit from same base interface

Viewed 34

I have some service classes which inherit all from the same non generic interface.
But some services will not directly inherit it but through some intermediate generic interface.
Using .NET 6

public interface IService { }   
public interface IServiceDirect {}
public class ServiceDirect: IServiceDirect {}

public interface IGenericInterface<T>: IService{}
public abstract class AGenericClass<T>: IGenericInterface<T> {}
public class GenericClass: IGenericInterface<MyEntity> {}

I now have the need to get all services which inherit from IService during runtime.
Unfortunately calling _serviceProvider.GetServices<IService>() will only return the services which inherit directly (meaning only through non generic interfaces like ServiceDirect) from IService

Here is how I register the services (assembly is just an Assembly provided through a parameter) and I left out some code to select only the classes/interfaces I want.

assembly.ExportedType.ToList().ForEach(x => {
  //services is of type IServiceCollection
  services.AddScoped(x.Parent.IsGenericType 
       ? x.Parent.GenericTypeDefinition()
             .MakeGenericType(x.Parent.GetGenericArguments())  
       : x.Parent, x.Class);
}

So far so good, they get injected and can be resolved when constructor injecting them by their most direct interface or abstract parent (exactly what I usually need).

To resolve them with _serviceProvider.GetServices<IService>() I have to add a second registration like this in my ForEach

assembly.ExportedType.ToList().ForEach(x => {
  if(x.Parent.IsGenericType)
  {
     services.AddScoped(x.Parent.GenericTypeDefinition()
         .MakeGenericType(x.Parent.GetGenericArguments())
             .GetInterfaces().FirstOrDefault(x => x == typeof(IService)),  
       x.Class);
  }

  //services is of type IServiceCollection
  services.AddScoped(x.Parent.IsGenericType 
       ? x.Parent.GenericTypeDefinition().MakeGenericType(
             x.Parent.GetGenericArguments())  
       : x.Parent, x.Class);
}

Now I can resolve all the services like I need.

My question is: is there a better way instead of registering every interface I need explicitly?

1 Answers

is there a better way instead of registering every interface I need explicitly?

Although your use of Reflection might possibly be simplified a bit, in general, all DI Containers require you to register all types up front.

What you are doing is something that is known as Auto-Registration (a.k.a Batch Registration). Auto-Registration is:

"the ability to automatically register components based on a certain convention in a DI Container by scanning one or more assemblies for implementations of desired abstractions." [Section 12.2.3 of DIPP&P]

Unfortunately, MS.DI does not contain any out-of-the-box functionality for Auto-Registration, which is probably why you are doing this by hand. There's nothing wrong implementing Auto-Registration by hand.

What is also required is making multiple registrations for a single component in case that component needs to be injected/used using multiple interfaces that it implements, as some of your components implement both IService and IGenericInterface<T>. As a general example, consider a class X that implements two interfaces, and application components that need to get injected with both interfaces. That means you'd make the following registration.

class X : IFoo, IBar { }

services.AddTransient<IFoo, X>();
services.AddTransient<IBar, X>();

The only thing you have to be careful about—especially in the context of MS.DI—is to prevent components to get a Torn Lifestyle. The following example, for instance, shows the previous class X getting a Torn Lifestyle:

services.AddSingleton<IFoo, X>();
services.AddSingleton<IBar, X>();

In this case, even though X is registered a singleton, eventually two instances of X will exist in the application; this is typically not the desired behavior and can lead to bugs. This holds for registering components with both the scoped as singleton lifestyles. Transient registration won't have this problem, because new instances are created anyway.

You can prevent this problem by following the advice given here, or in short, when applied to class X:

services.AddSingleton<X>();
services.AddSingleton<IFoo>(sp => sp.GetRequiredService<X>());
services.AddSingleton<IBar>(sp => sp.GetRequiredService<X>());

As a consequence, this does quite complicate your registration process, especially in your case; unfortunately, there's little you can do about it.

Related