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.