Search code examples
c#dependency-injectionioc-container

How to create and configure multiple instances of the same class with dependency injection?


How can one create and configure multiple instances of the same class with dependency injection?

I have seen similar questions / tutorials but I am not understanding it yet.

Here is an example:

Container

public class App()
{
    public IHost Host { get; }

    public App()
    {
        Host = Microsoft.Extensions.Hosting.Host.
            CreateDefaultBuilder().
            ConfigureServices((context, services) =>
            {
                services.AddSingleton<ITemperatureSensor, AcmeTemperatureSensor>();
                services.AddSingleton<IAcmeMachine, AcmeMachine>();
                services.Configure<TemperatureSensorOptions>(context.Configuration.GetSection(nameof(TemperatureSensorOptions)));
            }).
            Build();
    }
}

Acme Machine

This is where the temperature sensors should be injected.

public class AcmeMachine()
{
    public AcmeMachine( Something? )
    {
        // How inject the temperature sensors?
    }
    
    ITemperatureSensor WaterSensor
    ITemperatureSensor AirSensor
}

Temperature Sensors

public interface ITemperatureSensor
{
    string SerialNumber;
    double GetTemperature();
}

public class AcmeTemperatureSensor()
{
    public string SerialNumber { get; }
    
    public AcmeTemperatureSensor(IOptions<TemperatureSensorOptions> options)
    {
        SerialNumber = options.Value.SerialNumber;
    }
    
    public double GetTemperature()
    {
        return 25.0;
    }
}

Settings

appsettings.json
{
    "WaterSensor": {
        "TemperatureSensorOptions": {
            "SerialNumber": "123",
            },
    },
    "AirSensor": {
        "TemperatureSensorOptions": {
            "SerialNumber": "456",
            },
    }
}

Solution

  • I would suggest to change the appsettings to something like :

    {
      // ...
      "Sensors": {
        "WaterSensor": {
          "TemperatureSensorOptions": {
            "SerialNumber": "123"
          }
        },
        "AirSensor": {
          "TemperatureSensorOptions": {
            "SerialNumber": "456"
          }
        }
      }
    }
    

    Then you will be able to read the config as:

    builder.Services.Configure<Dictionary<string, Sensor>>(builder.Configuration.GetSection("Sensors"));
    
    class Sensor
    {
        public TemperatureSensorOptions TemperatureSensorOptions { get; set; }
    }
    
    class TemperatureSensorOptions
    {
        public string SerialNumber { get; set; }
    }
    

    And inject it into some service as IOptions<Dictionary<string, Sensor>>.

    Or use a dedicated class instead of Dictionary:

    builder.Services.Configure<Sensors>(builder.Configuration.GetSection("Sensors"));
    
    class Sensors
    {
        public Sensor WaterSensor { get; set; }
        public Sensor AirSensor { get; set; }
    }
    

    Note that if sensor does not have any other settings the TemperatureSensorOptions can be flattened/removed to simplify the config.

    If you need to construct the AcmeTemperatureSensor by injecting there these settings then you will need either construct them manually or follow the factory pattern.

    From the comments:

    If one uses manual construction or the factory pattern, is there a good way to get access to the IoC container services?

    Personally I would go with factory pattern which can be quite easily be implemented with Func<>'s (though it can have some downsides too, like adding factory parameters will become not that easy):

    enpointServices.AddTransient<Func<TemperatureSensorOptions, AcmeTemperatureSensor>>(sp => 
        opts => new AcmeTemperatureSensor(opts, sp.GetRequiredService<ILogger<AcmeTemperatureSensor>>()));
    

    And then inject the Func<TemperatureSensorOptions, AcmeTemperatureSensor> as dependency and call it.

    Also usually in such cases I create and register in DI class like AcmeTemperatureSensorDeps to encapsulate all the AcmeTemperatureSensor dependencies form DI to make it easier to manage them.