Ingo
Ingo

Reputation: 1815

Can I resolve a collection of services of open typed generics with 'GetServices(typeof(IEmpty<>))' in C#?

Having the following interface and their implementations...

public interface IEmpty<T> { }
public class Empty1 : IEmpty<Empty1>{ }
public class Empty2 : IEmpty<Empty2>{ }
public class EmptyN : IEmpty<EmptyN>{ }

allows me to register them and inject them explicitly into constructors

public class NewClass1 {
private IEmpty<Empty1> Empty;
    public NewClass1(IEmpty<Empty1> empty)
    {
        Empty = empty;
    }
    public string EmptyType => $"{Empty.GetType()}";
}

but when I tried to resolve all implementations of 'IEmpty<>' at once...

var allIEmpties = host.Services.GetServices(typeof(IEmpty<>));
allIEmpties.ToList().ForEach(empty => Console.WriteLine(empty.GetType()));

... execution of the code threw a 'NotSupportedException' (Cannot create arrays of open type), which I kind of understand, but leaves me wondering if it can be done and how it would have to be done to get a handle on all Services implementing IEmpty.

Would anyone have an idea of how to achieve this?


My motivation to get this working is to

  1. only register each service once (DRY)
  2. be able to explicity inject services into constructors without the need of some resolver-pattern or named dependencies
  3. load all implementations after startup to validate specific properties they are requried to implement due to the interfaces without having to search reflectively through all assemblies, which is my temporary solution but crawling through a box of needles to find 2 specific ones sounds wrong, if I could have a sorted box offering me direct access to the needles I need...

Using these additional nuget packages:

I have created this proof of concept snippet in LinqPad, in case you'd want to have a go:

void Main()
{
    var args = new List<string>();

    var host = CreateHostBuilder(args.ToArray()).Build();

    var newClass = host.Services.GetService<NewClass1>();
    Console.WriteLine(newClass.EmptyType);

    var oneEmpty = host.Services.GetService(typeof(IEmpty<Empty2>));
    Console.WriteLine(oneEmpty.GetType());

    var allIEmpties = host.Services.GetServices(typeof(IEmpty<>));
    allIEmpties.ToList().ForEach(empty => Console.WriteLine(empty.GetType()));
}

IHostBuilder CreateHostBuilder(string[] args)
{
    var hostBuilder = 
        Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((context, builder) => {
                builder.SetBasePath(Directory.GetCurrentDirectory());
            })
            .ConfigureServices((context, services) => {
                services.AddTransient<IEmpty<Empty1>, Empty1>();
                services.AddTransient<IEmpty<Empty2>, Empty2>();
                services.AddTransient<IEmpty<EmptyN>, EmptyN>();
                services.AddTransient<NewClass1>();
            });
    return hostBuilder;
}

public class NewClass1 {
    private IEmpty<Empty1> Empty;
    public NewClass1(IEmpty<Empty1> empty)
    {
        Empty = empty;
    }
    public string EmptyType => $"{Empty.GetType()}";
}

public interface IEmpty<T> {}
public class Empty1 : IEmpty<Empty1>{ }
public class Empty2 : IEmpty<Empty2>{ }
public class EmptyN : IEmpty<EmptyN>{ }

Upvotes: 2

Views: 1066

Answers (1)

Steven
Steven

Reputation: 172835

Resolving a list of generic types based on its open-generic definition by calling GetServices(typeof(IEmpty<>)) is not supported by MS.DI. Although technically possible, there is no DI Container that I'm familiar with that actually supports this.

There are many possible ways to solve your issue. You could, for instance, introduce a non-generic IEmpty (marker) interface that IEmpty<T> inherits from.

You can also go through the code base using Reflection, as you already mentioned, or you can go through the registrations in the ServiceCollection to get all registered IEmpty<T> registrations. This list can than be used to get the list. For instance:

var emptyTypes =
    from s in services
    where s.ServiceType.IsGenericType
    where s.ServiceType.GetGenericTypeDefinition() == typeof(IEmpty<>)
    select s.ServiceType;
    
foreach (Type emptyType in emptyTypes)
{
    var empty = host.Services.GetRequiredService(emptyType);
    Console.WriteLine(empty.GetType());
}

Upvotes: 3

Related Questions