I have a service consuming a set of services implementing an interface IX
:
public class MyService()
{
MyService(IEnumerable<IX> xs)
{
// Store xs in some field and later use them repeatedly.
}
}
I want a number of instances of a class MyX
implementing IX
:
public class MyX : IX
{
public string Field { get; }
public MyX(string field)
{
Field = field;
}
}
I add a number of these either as singletons:
builder.Services.AddSingleton<IX>(new MyX("field value 1"));
builder.Services.AddSingleton<IX>(new MyX("field value 2"));
[UPDATE] ... or from configuration:
builder.Services.AddSingleton(configuration.GetSection("xs").Get<IEnumerable<MyX>>());
This implementation works as expected: My service now has an IEnumerable comprising the two distinct instances of MyX
.
However, I need to add a logger to the MyX
class. I try this:
public class MyX : IX
{
ILogger<MyX> _logger;
public string Field { get; }
public X(ILogger<MyX> logger, string field)
{
_logger = logger;
Field = field;
}
}
But now I cannot construct MyX
during service setup, because I do not yet have a logger:
builder.Services.AddSingleton<IX>(new MyX(/* But where would I get a logger? */, "field value 1"));
I've run into variants of this problem a number of times. In general, it feels like DI wants me to separate my classes into some form of multi-stage construction (first field, then later, at service resolution time, add the logger). That would entail separating MyX
into a MyXFactory
and a MyX
, but that seems a bit awkward.
What's the right way to construct some number of instances of MyX
for use with dependency injection?
CodePudding user response:
You can use an override of AddSingleton<T>
method which accepts a lambda builder where you can access the ServiceProvider
.
So you can utilize it like below :
builder.Services.AddSingleton<X>(buillder=>
new MyX(builder.GetRequiredService<ILogger<MyX>>(),"field 1");
});
Alternatively, you can build a singleton factory IXFactory
that instantiates and returns the related X
implementations.
As the question evolves :
Assuming you have access to the IConfiguration
provided. You can have a configuration class something like below:
public class XConfig
{
public string[] FieldValues { get; set; }
}
You can read your config into XConfig
instance :
var config = Configuration.Get<XConfig>();
You may need a using using Microsoft.Extensions.Configuration;
for this.
Now you can use basic foreach to add a singleton instance for each FieldValue
foreach(var fieldValue in config.FieldValues){
builder.Services.AddSingleton<X>(buillder=>
new MyX(builder.GetRequiredService<ILogger<MyX>>(),fieldValue);
});
}
An alternate approach with a Factory approach and with the possibility of FieldValues
changing (to give you an idea of edge cases, you can of course just use IOptions<T>
to initiate the instances once)
public class XFactory
{
private readonly object Sync = new();
private readonly Dictionary<string, MyX> instances = new();
private readonly IServiceProvider serviceProvider;
public XFactory(IOptionsMonitor<XConfig> optionsMonitor, IServiceProvider serviceProvider)
{
optionsMonitor.OnChange(this.OnConfigChange);
this.serviceProvider = serviceProvider;
}
private void OnConfigChange(XConfig config)
{
lock (Sync)
{
var itemsToRemove = instances.Keys.Where(r => !config.FieldValues.Contains(r)).ToList();
itemsToRemove.ForEach(r => instances.Remove(r));
foreach (var fieldValue in config.FieldValues)
{
if (instances.ContainsKey(fieldValue))
{
continue;
}
var logger = this.serviceProvider.GetRequiredService<ILogger<MyX>>();
instances.Add(fieldValue, new MyX(logger, fieldValue));
}
}
}
public IEnumerable<MyX> GetInstances()
{
return instances.Values;
}
}
And register this factory as a singleton :
builder.Services.AddSingleton<IXFactory,XFactory>();
CodePudding user response:
Another option is to use ActivatorUtilities.CreateInstance
, which allows the resolution of parameters not registered with the DI container:
builder.Services.AddSingleton<IX>(
sericeProvider => ActivatorUtilities.CreateInstance<MyX>(
serviceProvider,
new object[] { "field value 1" }));
This will allow MyX
to be instantiated with any number of dependencies, as long as there is only one of type string
.