65

I have an interface called IRule and multiple classes that implement this interface. I want to use the .NET Core dependency injection Container to load all implementations of IRule, so all implemented rules.

Unfortunately I can't make this work. I know I can inject an IEnumerable<IRule> into my ctor of the controller, but I don't know how to register this setup in the Startup.cs

0

4 Answers 4

95

It's just a matter of registering all IRule implementations one by one; the Microsoft.Extensions.DependencyInjection (MS.DI) library can resolve it as an IEnumerable<T>. For instance:

services.AddTransient<IRule, Rule1>();
services.AddTransient<IRule, Rule2>();
services.AddTransient<IRule, Rule3>();
services.AddTransient<IRule, Rule4>();

Consumer:

public sealed class Consumer
{
    private readonly IEnumerable<IRule> rules;

    public Consumer(IEnumerable<IRule> rules)
    {
        this.rules = rules;
    }
}

NOTE: The only collection type that MS.DI supports is IEnumerable<T>.

Sign up to request clarification or add additional context in comments.

8 Comments

Are there any performance concern resolving IEnumerable<T>? Like a dozen of them, comparing to inject an abstract factory then resolve individual service by name. Thanks
@whoever: this is an impossible to answer question. You will have to measure this for yourself to determine whether or not the performance characteristics are okay in your particular environment. Measure, measure, measure.
@Steven Thanks. I just took another look. Indeed, even the two cases I have are different. In one, I loop through and call all implementations, so inject IEnumerable makes perfect sense; In the other, each caller only needs one particular implementation, but there are only two of them and init cost is negligible, kind of a wash. So I decide the save the abstraction for another day ^_^
@steven How would you go about ordering the rules? Say i have rule1, rule2 and rule3. If you want the order as rule3, rule1, rule2. Perhaps in another section you want rule2 followed by rule1. One way i was thinking was to have a comma delimited list them loop the array and match against the rule name but it seems hacky. Any suggestions?
Note that this works fine with AddSingleton as well.
|
26

For anyone else looking for an answer. You could also go through your assembly and register all classes that implement a specific interface:

// Get all implementations of IRule and add them to the DI
var rules = typeof(Program).Assembly.GetTypes()
    .Where(x => !x.IsAbstract && x.IsClass && x.GetInterface(nameof(IRule)) == typeof(IRule));

foreach (var rule in rules)
{
    services.Add(new ServiceDescriptor(typeof(IRule), rule, ServiceLifetime.Transient));
    // Replace Transient with whatever lifetime you need
}

This will also provide an IEnumerable<IRule> to every class that is part of your dependency injection. The advantage of this solution is that you do not need to add every single rule to your dependency injection. You can simply add a new implementation of IRule to your project and it will be registered automatically.

4 Comments

Thanks for this idea! You might want to consider using IsAssignableFrom instead of GetInterface.
Thanks for the hint. That would be even nicer. Unfortunately, it does not work. I guess the reason is that IsAssignableFrom refers to the reference, which cannot be resolved here. Therefore, the workaround via the name has to be chosen.
@Sebastian Not sure what you mean there. This works fine: .Where(x => typeof(IRule).IsAssignableFrom(x));
I generally find the phrasing .Where(x=>x.IsAssignableTo(typeof(IRule)) to be more readable, since it is easier to wrap my head around assigning to than assigning from
10

Sebastian and Dennisch answers combined:

var rules = Assembly.GetExecutingAssembly().GetTypes()
             .Where(x => !x.IsAbstract && x.IsClass && typeof(IRule).IsAssignableFrom(x));

foreach (var rule in rules)
{
    services.Add(new ServiceDescriptor(typeof(IRule), rule, ServiceLifetime.Transient));
}

Also, note, you can use executing assembly to search for types.

Comments

0

Here's something that might work, with a few caveats. Uniquely, this will handle different service lifetimes, so you won't saturate your db connections or lose shared data from a cache if your rule class is implemented by callers of differing lifetimes.

This code will also share instances of services limited by existing service lifetime rules. It does this by resolving the rule according to the original service descriptor, then casing that result to your validation class. If you have a singleton class that implements your rule, you will get the same instance using this that your other services are injecting.

You should make sure you call this last in your DI scope - it inspects the services registered in your service collection as of the time this code is run, and re-adds services that implement the one you want with the appropriate lifetime.

The use case for this is a validation routine on a user-configurable settings object. Various service classes of differing lifetimes and potentially custom factory methods made looking via assembly a nonstarter, and it would be too easy to miss an implementor if I did it manually.

drawbacks:

  • uses factory methods to resolve back to the original service, then casts the result to the desired type, so not clear for tree shakers and can complicate the dependency graph
  • doesn't get every implementor in the assembly, only those that have been registered as of when this method is called.
  • will miss any service descriptor added after it is run, since it gets the service collection as of whenever you call it. In other words, call this last.
public static IServiceCollection AddSettingsValidation(this IServiceCollection services)
{
    // omit unrelated code

    var validatorInterface = typeof(IValidateSettings);

    // find all registered services
    var implementors = services
        .Where(x => (x.ImplementationType ?? x.ServiceType).IsAssignableTo(validatorInterface))
        .ToList();

    foreach (var implementor in implementors)
    {
        var descriptor = implementor.IsKeyedService
            ? new ServiceDescriptor(
                validatorInterface,
                implementor.ServiceKey,
                (provider, key) =>
                    provider.GetRequiredKeyedService(implementor.ServiceType, key),
                implementor.Lifetime
            )
            : new ServiceDescriptor(
                validatorInterface,
                provider => provider.GetRequiredService(implementor.ServiceType),
                implementor.Lifetime
            );
        services.TryAdd(descriptor);
    }

    return services;
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.