2

Assuming this use case:

You've got two classes X and Y that depends on a configuration of type Config

public class X
{
    public X(IOptions<Config> config)
    {
    }
}

public class Y
{
    public Y(IOptions<Config> config)
    {
    }
}

Now, you want to create each an instance of X and Y, but with different configurations. What would be the right way to register this?

From everything I read, the only way to solve this would be by adding some sort of "naming" for the different configuration instances and resolve them via a custom resolver:

public delegate Config ServiceResolver(string key);

services.AddTransient<ServiceResolver>(serviceProvider => key =>
{
    switch (key)
    {
        case "A":
            return ... (whatever to get the first config instance);
        case "B":
            return ... (whatever to get the second config instance);
        default:
            throw new KeyNotFoundException();
    }
});

However, this means that the implementation of each X and Y must know about details about how to get the configurations:

  • They must know the correct name (A or B) and
  • they must know the ConfigResolver type, which is only an implementation detail/helper class for the sake of dependency injection.

This problem hits even harder if you need to go through several stages of dependencies, like

Config (A)         Config (B)
   |                  |
   v                  v
Service            Service
   |                  |
   v                  v
   X                  Y

My feeling is, there should be a better way to solve this. Like some form of receipent dependent service factory:

Host.CreateDefaultBuilder(args).ConfigureServices((context, services) => {
    services.Configure<Config>(context.Configuration.GetSection("ConfigA")).For<X>();
    services.Configure<Config>(context.Configuration.GetSection("ConfigB")).For<Y>();
});

and maybe

Host.CreateDefaultBuilder(args).ConfigureServices((context, services) => {
   services.AddTransient<Service>((services, receiverType) => {
      if(receiverType == typeof(X)) {
        ... resolve a service instance;
      }
      else {
        ... resolve some other service instance;
      }
   });
});

So, is there just some feature I missed until now? Is my understanding of the situation totaly misguided? Or is this really a feature that should be, but has not been added until now?


EDIT:

To make my point clearer: Just assume that X and Y are classes of a third-party library. Their constructors signature cannot be changed by you, as you don't have access to the source code.

So, how would you set this up in a way that you can get each an instance of X with ConfigA and an instance of Y with ConfigB?


Another EDIT 2023-01-02:

Happy new year everyone :)

Seems I have to describe a bit better what's my problem. This is not constrained to IOptions/configurations, but more a general question about where to decide about which service to inject and how it is configured.

Assume I have two a congress location with 2 stages. I call them "bigStage" and "smallStage", but in the end they've got the same implementation. I also got two speakers invited, called "loadSpeaker" and "quietSpeaker", but at this moment in time I don't know which one will speak on which of the two stages.

So I decide I've got this setup:

class Stage {
  public Stage(string name, ISpeaker speaker) {
    ...
  }
}

class Speaker: ISpeaker {
  public Speaker(string name) {
    ...
  }
}

Now, at the latest time possible, I want to compose my final setup so that I've got 2 Stages (called bigStage and smallStage) and their assigned Speakers (loudSpeaker on bigStage and quietSpeaker on smallStage). This composition/assignment should completely happen in my composition root, so that no code changes have to happen in the rest of my code. How can I do that?

Kc_
  • 201
  • 4
  • 11
  • Maybe the idea of named options can help you. See [here](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-7.0#named-options-support-using-iconfigurenamedoptions) – Enrico Massone Dec 05 '22 at 13:55
  • @EnricoMassone: Named options highlight exactly the situation I'm talking about: The receiving implementation is in charge of selecting which instance of options it should get. This creates a coupling to the dependency injection framework where I feel it shouldn't be necessary. I think all the aspects of composition should be defined through DI registration. – Kc_ Dec 05 '22 at 14:09
  • just use different types for different configs – theemee Dec 05 '22 at 14:42
  • @theemee: Yeah, well. That is similar to the "named options" solution. Just now the "name" equals the type... Still the service implementation has to choose which config should be injected (by choosing the correct "type" inside the constructor). And also we now have two types with the same properties, which violates the ["don't repeat yourself"](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) principle. – Kc_ Dec 05 '22 at 15:06
  • @Kc_ just use inheritance to follow DRY principle – theemee Dec 05 '22 at 15:07
  • @theemee: Even if we push this aside, the original situation still stays the same: The receiving class has to choose which instance of the configuration (or any service) it should get. IMHO, this decision should be made during DI registration and not in the dependency itself. – Kc_ Dec 05 '22 at 15:15
  • @Kc_ why is it so important for the service to choose? just create two classes ConfigA and ConfigB that inherit from Config and inject them. it's really that simple. – theemee Dec 05 '22 at 15:20
  • Whether it is "MyConfigThing" .. or (in my link here) an "IShipper" , it seems it is about the same situaiton. "I need a runtime decision maker for multiple registered IMyThing(s)". See my answer here: https://stackoverflow.com/a/52435195/214977 – granadaCoder Dec 05 '22 at 15:28
  • @theemee: Because this solution does not scale. Look at the second example I brought up (with configurations A,B and 2 instances of Service that get injected into X and Y). So with your solution, I would first create a class ConfigA and a drived class ConfigB. Then I would create a class ServiceA with Constructor ServiceA(ConfigA) and the same implementation as class ServiceB(ConfigB). Then I'd create classes X with ctor X(ServiceA) and Y with Y(ServiceB). That is a hell load of overhead for such a small example. And real-world code can be a lot more difficult. – Kc_ Dec 05 '22 at 15:29
  • @Kc_ ServiceA and ServiceB are identical apart from config? – theemee Dec 05 '22 at 15:34
  • @theemee: Yes. Anyhow, just using another constructor on the same class wouldn't work, because when injecting into X and Y it must be decided if you want the Service instance with ConfigA or the one with ConfigB. So again, we're left with 2 separate classes. – Kc_ Dec 05 '22 at 15:45
  • @granadaCoder: Just skimmed your code and if I understood it coorectly, you're adding some sort of "naming property" to your service (IShipper), so that you can later identify it inside the receiving implementation (OrderProcessor). Still that's the other way round than I want it to be. My receiving class should not be aware that there are more than 1 service, it should just get the one that I somehow defined when creating my DI container. – Kc_ Dec 05 '22 at 16:22
  • 1
    Ok.. I see. You want compile-time declarations of multiple ISomethings. I need to think about that one in dotnet-core world. – granadaCoder Dec 05 '22 at 16:52

3 Answers3

1

I suggest to use a factory for your Service:

class X {
    private readonly Service _service;
    public X(ServiceFactory serviceFactory) {
        _service = serviceFactory.Create<X>();
    }
}

class Service {
    private readonly Config _config;
    public Service(Config config) { _config = config; }
}

class ServiceFactory {
    private readonly IConfiguration _configuration;
    /* other Service dependencies would also be injected here */
    public ServiceFactory(IConfiguration configuration, /* Service dependencies */) {
        _configuration = configuration;
        ...
    }

    public Service Create<T>() {
        return Create(typeof(T));
    }

    public Service Create(Type type) {
        var configName = switch typeof(T) {
            X => "ConfigX",
            Y => "ConfigY",
            default => throw new Exception()
        };
        var config = _configuration.GetSection(configName).Get<Config>();
        return new Service(config, /* other dependencies */);
    }
}

The switch statement can be replaced with a Dictionary<Type, string> or Dictionary<string, string> if you would want to export this dictionary to IConfiguration.

Getting the Config can be also cached for performance (don't forget the thread safety)

theemee
  • 769
  • 2
  • 10
  • Yeah, that's about what I use today :) So instead of naming the configuration/service to get, this will communicate back to the service factory "for which receiver" this resolves (through the generic argument of ServiceFactory<>). Still I'm wondering, why this type of registration is not already integrated in Microsoft.Extensions.DependencyInjection and why it's not possible to get some information about the receiving end inside our factory method. – Kc_ Dec 05 '22 at 16:30
1

So the "trick" to all of this is... you have to piggy back onto ~something to make a decision on which one IMySomething . when you register multiple IMySomething(s).

The factory above where you switch/case on the object.TYPE....is one way. But it is "fragile", IMHO. Or at the very last, violates the Open/Closed principle of SOLID, as you have to keep editing the code to add a new case-statement.

So I also think you want a Factory.......BUT I do not like "hard coding" the values of the switch/case statements.

So if you follow my IShipper example:

Using a Strategy and Factory Pattern with Dependency Injection

I think you want to create a

IShipperFactory

and inject the IEnumerable of "IShipper"(s).

..

Then you will use your IShipperFactory... when registering your things that need an IShipper.

This does cause a small "ripple" because you need access to the IShipperFactory....to do (later) IoC registrations.

But it would be "clean" and have good separations of concerns.

Let me pseudo code it.

public interface IShipper (from other article)

3 concretes (Usps, FedEx, Ups)

public interface IShipperFactory()
  public IShipper GetAnIShipper(string key)

..

public class ShipperFactoryConcrete
    (from other article, inject multiple IShippers here)

    public IShipper GetAnIShipper(string key)
    // look through the injected IShippers to find a match, or else throw exception.

.....

public interface IOrderProcessor

..

public class WestCoastOrderProcessor : IOrderProcessor
    /* c-stor */
    public WestCoastOrderProcessor(IShipper aSingleShipper)

public class EastCoastOrderProcessor : IOrderProcessor
    /* c-stor */
    public WestCoastOrderProcessor(IShipper aSingleShipper)

........

Ok, so we decide at compile-time, we want to define the "best" IShipper for the EastCoastOrderProcessor and WestCoastOrderProcessor. (making up some kind of example here)

So need need to IoC register.

from the other article:

cont.RegisterType<IShipper, FedExShipper>(FedExShipper.FriendlyName);
cont.RegisterType<IShipper, UspsShipper>(UspsShipper.FriendlyName);
cont.RegisterType<IShipper, UpsShipper>(UpsShipper.FriendlyName);

now it gets a little "off beaten path".

See:

https://stackoverflow.com/a/53885374/214977

and

// so this is a cart-horse situation, where we need something from the IoC container.... to complete the IoC registrations.

  IShipperFactory sf = services.GetRequiredService<IShipperFactory>(); // see https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-7.0#resolve-a-service-at-app-start-up

.. and now we IoC register...but we specify specific values for the constructor. please see the SOF (214977), for syntax-sugar hints. the below is definately pseduo code.....

_serviceCollection.AddSingleton<IOrderProcesor>(x => 
    ActivatorUtilities.CreateInstance<EastCoastOrderProcessor>(x, sf.GetAnIShipper(FedExShipper.ShipperName));
);

_serviceCollection.AddSingleton<IOrderProcesor>(x => 
    ActivatorUtilities.CreateInstance<WestCoastOrderProcessor>(x, sf.GetAnIShipper(UspsShipper.ShipperName));
);

APPEND:ONE:

Another "trick" .. if you have a code base that you cannot change is.

The "proxy design pattern":

The Proxy design pattern provides a surrogate or placeholder for another object to control access to it.

https://www.dofactory.com/net/proxy-design-pattern

public EastCoastOrderProcessorProxy

  private readonly ThirdPartyOrderProcessor innerThirdPartyOrderProcessor;

    public EastCoastOrderProcessor(ThirdPartyOrderProcessor innerThirdPartyOrderProcessor)
    {
        this.innerThirdPartyOrderProcessor = innerThirdPartyOrderProcessor;
    }

..

public WestCoastOrderProcessorProxy

  private readonly ThirdPartyOrderProcessor innerThirdPartyOrderProcessor;

    public EastCoastOrderProcessor(ThirdPartyOrderProcessor innerThirdPartyOrderProcessor)
    {
        this.innerThirdPartyOrderProcessor = innerThirdPartyOrderProcessor;
    }

So while you cannot change the ThirdPartyOrderProcessor, you can write 1:N wrapper-proxies around it.

granadaCoder
  • 26,328
  • 10
  • 113
  • 146
  • Still, I don't see how this would change the situation. Just to make my issue more clear: Assume that you have a class OrderProcessor that comes from a third-party library, so you don't have access to it's source and cannot change the way it works. Its constructor needs an instance of IShipper. And now you need two instances of OrderProcessor in your code, one that uses an implementation of UpsShipper and one that uses a FedExShipper. How would you go on to register this without creating the instances "by hand" (i.e. without leveraging dependency injection)? – Kc_ Dec 05 '22 at 20:05
  • Ok, seems the example from my last comment CAN be solved by using the ActivatorUtilities... although it's awfully unreadable :D This screams for some fluent interface that hides the details. – Kc_ Dec 05 '22 at 22:24
  • I added another "trick" under "APPEND:ONE" in my answer. (as another option if you have code from a third party that you cannot change). – granadaCoder Dec 06 '22 at 14:19
  • Yeah. I agree with the un-read-ability part. My "much more simple" need sometimes, is I have a class that needs a simple INTEGER value for its constructor. Let's use "BlindCacheAsideSecondsValue" as example. I don't want to write a IBlindCacheAsidesSecondsLookerUpper interface and concrete to do this. I just want to constructor inject a integer-value into my class.......defined during the IoC registrations. (I write alot of "framework'ish" code). This is harder to do in dotnet-core-ioc than I prefer. But the default ioc-registrations in dotnet-core is overall "very nice"..... – granadaCoder Dec 06 '22 at 14:24
0

The simplest solution I can think of, without using named options inside of your service classes, is moving the selection of the configuration object from the class constructor to the composition root of the application.

This way, your service class simply receives a configuration object as a constructor parameter and it is not aware of the underlying configuration infrastructure.

The composition root, which is in charge of composing the objects which make your application, do know about the configuration infrastructure and picks the right configuration object for your services.

In order to implement this pattern, you need to define an option class as the first step. This option class is needed in order to leverage the options pattern support offered by ASP.NET core. You will only use this class at the composition root level.

public sealed class LayoutOptions
{
    public const string Layout = "Layout";
    public const string Basic = "Basic";
    public const string Complex = "Complex";

    public string Name { get; set; } = default!;
    public string Color { get; set; } = default!;
    public int NumberOfColumns { get; set; }
}

Then you need to define a class which represents the configuration object for your services. This is basically a strongly typed configuration object used to configure your services. This object is built strating from the options class, notice that you don't need to make it identical to the options class itself.

public sealed class LayoutConfiguration
{
    public string Name { get; }
    public string Color { get; }

    public LayoutConfiguration(string name, string color)
    {
      Name = name;
      Color = color;
    }
}

Now you need to define your service classes. These types are configured by using the LayoutConfiguration configuration class. Each service class will be properly configured by the composition root of the application, by using the proper named options.

public interface ILayoutService
{
    string GetLayoutDescription();
}

public sealed class BasicLayoutService : ILayoutService
{
    private readonly LayoutConfiguration _config;

    public BasicLayoutService(LayoutConfiguration config)
    {
      _config = config ?? throw new ArgumentNullException(nameof(config));
    }

    public string GetLayoutDescription() =>
      $"Basic layout description. Name: '{_config.Name}' Color: '{_config.Color}'";
}

public sealed class ComplexLayoutService : ILayoutService
{
    private readonly LayoutConfiguration _config;

    public ComplexLayoutService(LayoutConfiguration config)
    {
      _config = config ?? throw new ArgumentNullException(nameof(config));
    }

    public string GetLayoutDescription() =>
      $"Complex layout description. Name: '{_config.Name}' Color: '{_config.Color}'";
}

You can also defined a couple of controllers, that you can use to test this implementation and be user that your services are wired-up correctly by the composition root of the application:

  [ApiController]
  [Route("[controller]")]
  public sealed class BasicLayoutController : ControllerBase
  {
    private readonly BasicLayoutService _basicLayoutService;

    public BasicLayoutController(BasicLayoutService basicLayoutService)
    {
      _basicLayoutService = basicLayoutService ?? throw new ArgumentNullException(nameof(basicLayoutService));
    }

    [HttpGet("description")]
    public string GetDescription() => _basicLayoutService.GetLayoutDescription();
  }

  [ApiController]
  [Route("[controller]")]
  public sealed class ComplexLayoutController : ControllerBase
  {
    private readonly ComplexLayoutService _complexLayoutService;

    public ComplexLayoutController(ComplexLayoutService complexLayoutService)
    {
      _complexLayoutService = complexLayoutService ?? throw new ArgumentNullException(nameof(complexLayoutService));
    }

    [HttpGet("description")]
    public string GetDescription() => _complexLayoutService.GetLayoutDescription();
  }

This is the most important part. Put this registration code inside the Program.cs class (which is the composition root for an ASP.NET core 6 application):

      // Configure named options 
      builder.Services.Configure<LayoutOptions>(
        LayoutOptions.Basic,
        builder.Configuration.GetSection($"{LayoutOptions.Layout}:{LayoutOptions.Basic}")
      );

      builder.Services.Configure<LayoutOptions>(
        LayoutOptions.Complex,
        builder.Configuration.GetSection($"{LayoutOptions.Layout}:{LayoutOptions.Complex}")
      );
      
      // Register the BasicLayoutService by picking the right configuration
      builder
        .Services
        .AddScoped(serviceProvider =>
        {
          // Get named options 
          var layoutOptions = serviceProvider.GetRequiredService<IOptionsSnapshot<LayoutOptions>>();
          var basicLayoutOptions = layoutOptions.Get(LayoutOptions.Basic);

          // Create strongly typed configuration object from named options
          var configuration = new LayoutConfiguration(
            basicLayoutOptions.Name,
            basicLayoutOptions.Color);

          // Creates new instance of BasicLayoutService using the service provider and the configuration object
          return ActivatorUtilities.CreateInstance<BasicLayoutService>(
            serviceProvider,
            configuration);
        });

      // Register the ComplexLayoutService by picking the right configuration
      builder
        .Services
        .AddScoped(serviceProvider =>
        {
          // Get named options 
          var layoutOptions = serviceProvider.GetRequiredService<IOptionsSnapshot<LayoutOptions>>();
          var complexLayoutOptions = layoutOptions.Get(LayoutOptions.Complex);

          // Create strongly typed configuration object from named options
          var configuration = new LayoutConfiguration(
            complexLayoutOptions.Name,
            complexLayoutOptions.Color);

          // Creates new instance of ComplexLayoutService using the service provider and the configuration object
          return ActivatorUtilities.CreateInstance<ComplexLayoutService>(
            serviceProvider,
            configuration);
        });

You can now test this implementation. As an example, you can set the following configuration in appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "Layout": {
    "Basic": {
      "Name": "Basic Layout",
      "Color": "red",
      "NumberOfColumns": 2
    },
    "Complex": {
      "Name": "Complex Layout",
      "Color": "blue",
      "NumberOfColumns": 3
    }
  }
}

If you run this application and you issue a GET request to /BasicLayout/description, you ge the following response:

Basic layout description. Name: 'Basic Layout' Color: 'red'

If you issue a GET request to /ComplexLayout/description the response you get is:

Complex layout description. Name: 'Complex Layout' Color: 'blue'

A final note on the service lifetime for BasicLayoutService and ComplexLayoutService. In my example I decided to register them as scoped services, because you may want to recompute the configuration object for them (LayoutConfiguration) for each incoming request. This is useful if your configuration may change over time. If this is not the case, you can safely register them as singleton services. That's up to you and depends on your requirements.

Enrico Massone
  • 6,464
  • 1
  • 28
  • 56
  • Hi Enrico, thanks for your answer. I'm not sure if I understood your example completely. Isn't the service selection still made inside the consumers constructors? I.e. the selection is made by using either BasicLayoutService or ComplexLayoutService inside the controllers constructor. I couldn't replace the service in BasicLayoutController by a ComplexLayoutService without editing the controllers code. – Kc_ Jan 01 '23 at 20:25
  • The purpose of my sample is to show how you can decide which named configuration to use by avoiding to do the selection itself inside the constructor of the class using the configuration itself. Basically my sample moves the selection inside the application composition root and removes this responsibility from the class using the configuration itself. Based on my understanding of your question, this was the thing you were trying to understand. – Enrico Massone Jan 02 '23 at 12:29
  • From a coding perspective my sample is far from ideal. As you noticed, both the controller classes depend on concrete implementations. This introduces coupling and it is a bad thing. To make things better, you can inject the interface ILayoutService inside the controller classes instead of its concrete implementations. But again, the core of my sample is not the overall design, but the fact that you can select the named configuration inside the composition root instead of doing the selection inside the class using the configuration itself. – Enrico Massone Jan 02 '23 at 12:32
  • Maybe I misunderstood your question. If this is the case and none of the provided answers solves your problem, I would suggest to edit your question. If you find out that your question spans over multiple different problems at the same time, I would suggest to post different questions on the site, each one focusing on a single problem only. – Enrico Massone Jan 02 '23 at 12:36
  • Hi Enrico, maybe my question really is the problem ;) I added another example to make it clearer what I want to achieve. – Kc_ Jan 02 '23 at 15:24