Register multiple complex dependencies with Microsoft DI

Viewed 231

My ASP.NET core application uses a software client consisting of a set of classes ("Helper", "Client", "UserSetting", "ServerSetting") retrieving their correct configurations/instances from dependency injection. A single class can be configured as a singleton within the application and get used. But how can I have multiple instances of this set of classes? How can the instance A of "Client" and "Helper" now receive the corresponding settings for A, while the instance B receives its different corresponding values?

What I have: A single client has a helper, and settings in appsettiongs.json. Both, helper and Client require the settings. All are able to communicate

Targeted case: multiple instances of classes "Client" "Helper", "UserOptions", "ServerOptions". As shown, I need multiple instances of this packages classes to connect to multiple servers and communicate with a selected one. Each Client must be configured for another server/user data combination.

Current initialization of a single client:

class Startup {
  IConfiguration configuration {get;}
  public Startup(){//init configuration from appsettings.json} 

  public void ConfigureServices(IServiceCollection services){
    ... 
    services.Configure<UserSettings>(Configuration.GetSection("userData");
    service.Configure<ServerSettings>(Configuration.GetSection("serverSettings");
    service.AddSingleton<IHelper, MyHelper>();
    service.AddSingleton<IClient, MyClient>();
  }
}

public class MyClient:IClient{
  public void MyClient(ILogger<MyClient> log, IHelper helper){..}
}

public class MyHelper:IHelper{
  public void MyHelper(ILogger<MyHelper> log, IOptions<UserSettings> userSettings, IOptions<ServerSettings> serverSettings){..}
}

class MyWebController{
  public IClient Client;

  public MyWebController(IClient client){ this.Client = client;}

  public ActionResult DoSomeServerStuff(string data){
     Client.Communicate();
  }
}

Idea of a preferred usage:

class Startup {
  IConfiguration configuration {get;}
  public Startup(){//init configuration from appsettings.json} 

  public void ConfigureServices(IServiceCollection services){
    { 
      //configure in some scope for A
      services.Configure<UserSettings>(Configuration.GetSection("ServerA:userData");
      service.Configure<ServerSettings>(Configuration.GetSection("ServerA:serverSettings");
      service.AddSingleton<IHelper, MyHelper>();
      service.AddSingleton<IClient, MyClient>();
    }
    {  
      //configure in some scope for B (How can I kind of scope it within those brackets?)
      services.Configure<UserSettings>(Configuration.GetSection("ServerB:userData");
      service.Configure<ServerSettings>(Configuration.GetSection("ServerB:serverSettings");
      service.AddSingleton<IHelper, MyHelper>();
      service.AddSingleton<IClient, MyClient>();
    }
  }
}

class MyWebController{
  public IClient ClientA, ClientB;

  public MyClass(Magic magic){
    ClientA = magic.GetClient("A");
    ClientB = magic.GetClient("B");
  }

  // GET: MyWebController/
  public ActionResult Index(string data){
     switch(data){
       case "A": 
         ServerA.Communicate();
         break;
       case "B":
         ServerB.Communicate();
         break;
     }
  }

I tried packaging the initialization into a container, to initialize it manually, but failed to create the object IOptions as requested by the constructor. The UserSettings value can not be casted to an IOptons value.

class MyContainer{
  public IClient ClientA, ClientB;
  public MyContainer(IConfiguration config, ILoggerFactory factory){
    userSettingsA = config.GetSection("ServerA:userData").Get<UserSettings>();
    serverSettingsA = config.GetSection("ServerA:serverSettings").Get<ServerSettings();
    var helperA = new MyHelper(factory.CreateLogger<MyHelper>, userSettingsA, serverSettingsA);
    ClientA = new MyClient(factory.CreateLogger<Client>(), helper);
    
    // ClientB configured analog to ClientA
  }
}
class Startup{
  public ConfigureServices(IServiceCollection services){
    service.AddSingleton(MyContainer);
  }
}

However, this fails, as userSettingsA as instance of UserSettings is not castable to IOptions. So I failed even with the unrecommended, explicit construction of the classes, skipping the DI.

Any hints on how to proceed? Any ideas on how to scope the objects during creation with DI or how to create those objects manually by creating the IOptions type?

1 Answers

I was able to fix it with the help of the comments above. Since I use Singletons here, it helps to create a service for each client using this ServiceFactory. One might have more issues in case of transient values, etc. However, this works for me:

New classes: 

public class IClientCollection {
  public IClient GetClient(string val);
}

public class ClientCollection {
  private IClient ClientA, ClientB;
  public ClientCollection (IClient a, IClient b){
    ClientA = a;
    ClientB = b;
  }

  public IClient GetClient(string val){
    switch (val){
      case "A": return ClientA;
      case "B": return ClientB;
  }
}
Initialization in Startup.cs

class Startup {
  IConfiguration configuration {get;}

  public Startup() { /*init configuration from appsettings.json*/ } 

  public void ConfigureServices(IServiceCollection services){
...
    var serviceA = CreateClientService(Configuration.GetSection("ServerA:userData"), Configuration.GetSection("ServerA:serverSettings"));
    var serviceB = CreateClientService(Configuration.GetSection("ServerB:userData"), Configuration.GetSection("ServerB:serverSettings"));

    var clientCollection = new ClientCollection(
        serviceA.GetService<IClient>();
        serviceB.GetService<IClient>();
    );
    serivces.AddSingleton<IClientCollection>(clientCollection);
...
  }

    public ServiceProvider CreateClientService(IServiceCollection services, IConfigurationSection userSettings, IConfigurationSection serverSettings)
    {  
      newService = new ServiceCollection();
      newService.AddSingleton(services);

      newService.Configure<UserSettings>(userSettings);
      newService.Configure<ServerSettings>(serverSettings);
      newService.AddSingleton<IHelper, MyHelper>();
      newService.AddSingleton<IClient, MyClient>();
      
      return newService.BuildServiceProvider();
    }
  }
}
Usage: 

class MyWebController{
  public IClient ClientA, ClientB;

  public MyClass(IClientCollection collection){
    ClientA = collection.GetClient("A");
    ClientB = collection.GetClient("B");
  }

  // GET: MyWebController/
  public ActionResult Index(string data){
     switch(data){
       case "A": 
         ClientA.Communicate();
         break;
       case "B":
         ClientB.Communicate();
         break;
     }
  }

Thank you for your support @Jemery Lakeman.

Related