dotnet

How to work with lists in .NET dependency injection

Published at 2022-02-22

After posting a tweet about passing multiple interfaces into the .NET service container and resolving the services as an IEnumerable in the constructor, I thought it would be a good idea to write more about it in detail. I will also share some other neat tricks when working with lists in .NET dependency injection.

Register multiple instances of an interface and resolve as an IEnumerable

We'll be doing the examples as a FizzBuzz implementation and creating an interface for a rule.

public interface IRule
{
    string Output { get; }
    bool IsValid(int input);
}

This is what a Fizz implementation of that interface would look like.

public class FizzRule : IRule
{
    public string Output => "Fizz";
    public bool IsValid(int input) => input % 3 == 0;
}

When registering the services we can simply add all the implementations of IRule. We create implementations for Buzz, Fuzz and Jazz as well.

var serviceProvider = new ServiceCollection()
    .AddTransient<IRule, FizzRule>()
    .AddTransient<IRule, BuzzRule>()
    .AddTransient<IRule, JazzRule>()
    .AddTransient<IRule, FuzzRule>()
    .AddTransient<Game>()
    .BuildServiceProvider();

We can now inject the rules as an IEnumerable in the Game class.

public class Game
{
    private readonly IEnumerable<IRule> _rules;
    public Game(IEnumerable<IRule> rules) =>
        _rules = rules;

    public IEnumerable<string> GetResults(int from, int to)
    {
        foreach (var i in Enumerable.Range(from, to - ( from -1 )))
        {
            var output = _rules
                .Where(rule => rule.IsValid(i))
                .Aggregate(string.Empty, (current, rule) => current + rule.Output);

            if (string.IsNullOrEmpty(output))
                output = i.ToString();

            yield return output;
        }
    }
}

Now we can access IEnumerable<IRule> without any custom code in the DI. See the whole example on Github.

Using reflection to resolve as an IEnumerable

@LaylaCodeslt also pointed out that you use reflection to avoid having to manually add each service. Let's look at how that would be solved.

var rules = Assembly.GetCallingAssembly().GetTypes()
    .Where(type => typeof(IRule).IsAssignableFrom(type) && !type.IsInterface);

var serviceCollection = new ServiceCollection()
    .AddTransient<Game>();

foreach (var rule in rules)
    serviceCollection.AddTransient(typeof(IRule), rule);

var serviceProvider = serviceCollection.BuildServiceProvider();

Now we don't have to manually register IRule and we can create rules without modifying the service collection file. See the whole example on Github.

Using Func<> to access individual rules

Sometimes we need to access a single rule from the list of rules. I want to fetch the rule by an enum. I just want Fizz and Buzz rules to count. Let's extend our interface with a rule key:

public interface IRule
{
    RuleKey Key { get; }
    string Output { get; }
    bool IsValid(int input);
}

Our Fizz rule now looks like this:

public class FizzRule : IRule
{
    public RuleKey Key => RuleKey.Fizz;
    public string Output => "Fizz";
    public bool IsValid(int input) => input % 3 == 0;
}

After registering the rules we register a Func for accessing rules in the game.

services.AddTransient(
        factory => (Func<RuleKey, IRule>)
            (key => factory.GetServices<IRule>().FirstOrDefault(m => m.Key == key))
    )

I've made some changes in the Game to load the rules that I want before getting the results.

public class Game : IGame
{
    private readonly Func<RuleKey, IRule> _ruleAccessor;
    private readonly IList<IRule> _rules = new List<IRule>();
    public GameService(Func<RuleKey, IRule> ruleAccessor)
    {
        _ruleAccessor = ruleAccessor;
    }

    public void LoadRules(params RuleKey[] keys)
    {
        foreach (RuleKey ruleKey in keys)
            _rules.Add(_ruleAccessor(ruleKey));
    }

    public void DisposeRules()
    {
        _rules.Clear();
    }

    public IEnumerable<string> GetResults(int from, int to)
    {
        foreach (int i in Enumerable.Range(from, to - ( from -1 )))
        {
            string output = _rules
                .Where(rule => rule.IsValid(i))
                .Aggregate(string.Empty, (current, rule) => current + rule.Output);

            if (string.IsNullOrEmpty(output))
                output = i.ToString();

            yield return output;
        }
    }
}

Now we can call the Game.GetResults like this to run it for just Fizz and Buzz.

var game = serviceProvider.GetService<Game>();

game.LoadRules(RuleKey.Fizz, RuleKey.Buzz);

foreach (var result in game.GetResults(1, 100))
    System.Console.WriteLine(result);

Again see the whole example on Github.

Let me know if you know and other alternatives working with lists in the .NET service container.

Avatar of Author

Karl SolgÄrd

Norwegian software developer. Eager to learn and to share knowledge. Sharing is caring! Follow on social: Twitter and LinkedIn. Email me: [email protected]