I work for a company that has multiple sites across Europe. I recently came across a problem where we had a single application that needed to be installed in each office, but each country’s workflow is slightly different. The solution was to have a separate class for each country and to inject the correct class at runtime.

However, we use the “build once, deploy many” strategy which meant we couldn’t use DI in the traditional manner. I’ll elaborate on this point below.

Traditional ASP.NET Core Dependency Injection

A typical ASP.NET Core app registers your services’ interfaces and corresponding implementations in Program.cs:

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices(services =>
            {
                services.AddScoped<IMyService, MyServiceA>();
            })
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

However, as you can see, the type registration is in code, thus if we wanted to replace MyServiceA with MyServiceB we would need to rebuild the app. This approach would not work at my company because, as I’ve already mentioned, we “build once” via our CI/CD pipeline.

Below we’ll look over a couple of potential solutions.

Using HostBuilderContext

We can use the second overload of ConfigureServices which takes a HostBuilderContext in addition to the IServiceCollection:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices((host, services) =>
        {
            if (host.HostingEnvironment.IsEnvironment("Germany"))
            {
                services.AddScoped<IMyService, MyServiceDE>();
            }
            else
            {
                services.AddScoped<IMyService, MyServiceUK>();
            }
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

The HostingEnvironment class has a number of methods you can use to check your current environment. By default .NET Core has three built-in environments:

  • Development
  • Staging
  • Production

Each of these have corresponding methods: IsDevelopment(), IsStaging() and IsProduction().

In my example above, I’m checking for a custom environment name which is set in the ASPNETCORE_ENVIRONMENT environment variable on the hosting server.

You could also check the EnvironmentName property in an if or switch statement, e.g.

switch (host.HostingEnvironment.EnvironmentName)    
{
    case "Germany":
        services.AddScoped<IMyService, MyServiceDE>();
        break;
    case "UK":
        services.AddScoped<IMyService, MyServiceUK>();
        break;
    default:
        services.AddScoped<IMyService, MyServiceGlobal>();
        break;
}

This method will probably be suitable for my needs, but for the sake of completeness, I wanted to describe another solution.

Choosing an Implementation at Runtime

You may or may not know that it’s possible to register multiple implementations for the same interface, after which, we can use a config setting to select which implementation we want at runtime.

Let’s start by registering our implementations:

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureServices((host, services) =>
        {
            services.AddScoped<IMyService, MyServiceDE>();
            services.AddScoped<IMyService, MyServiceUK>();
            services.AddScoped<IMyService, MyServiceGlobal>();
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

Then in appsettings.json we create a key/value to set which implementation we want:

{
  "AppSettings": {
    "CurrentService": "MyServiceDE"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information"
    }
  }
}

Now, in our MVC controllers there’s two things to note. The first is that we’ll be using the IOptions pattern for injecting config, and the second is that instead of having a parameter of type IMyService we’ll have an IEnumerable<IMyService>, e.g:

public class HomeController : Controller
{
    private readonly ILogger<HomeController> _logger;
    private readonly AppSettings _appSettings { get; set; }
    private readonly IEnumerable<IMyService> _myServices { get; set; }

    public HomeController(ILogger<HomeController> logger, IOptions<AppSettings> settings, IEnumerable<IMyService> myServices)
    {
        _logger = logger;
        _appSettings = settings.Value;
        _myServices = myServices
    }

    public IActionResult Index()
    {
        return View();
    }
}

This allows you to select the implementation by the name that you set in the config1:

public IActionResult Index()
{
    var myService = _myServices.FirstOrDefault(h => h.GetType().Name == _appSettings.CurrentService);
    // use the service
    return View();
}

Using this method, I can build once and then let our CI/CD server amend the value of CurrentService in appsettings.json depending on which country the app is being deployed to.