Home > Mobile >  Binding both services and parameters with dependency injection
Binding both services and parameters with dependency injection

Time:12-08

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.

  • Related