skip to Main Content

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?

2

Answers


  1. 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>();
    
    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search