1

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",
            },
    }
}
JasonC
  • 139
  • 8
  • This is also very similar [question](https://stackoverflow.com/questions/39174989/how-to-register-multiple-implementations-of-the-same-interface-in-asp-net-core) – JasonC Apr 13 '23 at 22:09

2 Answers2

1

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.

Guru Stron
  • 102,774
  • 10
  • 95
  • 132
  • If one uses manual construction or the factory pattern, is there a good way to get access to the IoC container services? Would you recommend something like this `_logger = Host.Services.GetRequiredService>();`? – JasonC Apr 14 '23 at 20:49
  • 1
    @JasonC Updated the answer a bit, check it out. – Guru Stron Apr 15 '23 at 09:08
  • Thanks for updating your answer. You mention adding a class that encapsulates all the required dependencies makes it easier to manage them. What do you mean by easier to manage? Is it just that the class that uses it only has one argument in the constructor? – JasonC Apr 17 '23 at 20:18
  • @JasonC _"Is it just that the class that uses it only has one argument in the constructor?"_ - in short - yes, your class has one parameter besides the "factory" ones. – Guru Stron Apr 17 '23 at 20:57
1

You can change your class constructors to the following:

public AcmeMachine(ITemperatureSensor waterSensor, ITemperatureSensor airSensor)
public AcmeTemperatureSensor(TemperatureSensorOptions options)

With those signatures you can have the following DI configuration:

var temp = Configuration.GetSection("WaterSensor").Get<TemperatureSensorOptions>();
var air = Configuration.GetSection("AirSensor").Get<TemperatureSensorOptions>();

services.AddSingleton(sp => new AcmeMachine(
    waterSensor: new AcmeTemperatureSensor(temp),
    airSensor: new AcmeTemperatureSensor(air));
Steven
  • 166,672
  • 24
  • 332
  • 435
  • Would this still be a good solution if there were N sensors? Say N = 5, 10, or 100? – JasonC Apr 14 '23 at 20:43
  • In that case, please update your question and show how you indent to use such list of 100 sensors and how you want to distinguish between them. Ping me after you updated your question and I'll update my answer. – Steven Apr 15 '23 at 08:45