Environment Specific Configuration in DotNet Core

It's common to have different application configurations per environment (e.g: Dev, QA, Production) allowing us to switch between them at each stage.

In older Web Applications this was usually done with Web Config Transformations or Razor files, however the new ConfigurationBuilder available in DotNet Core makes it relatively straight forward to use multiple files, building up & overridding a set of config keys.

Below is a quick guide on how to do this. All the code in this article can be found here: https://github.com/lukemerrett/EnvironmentConfigSample

The Config Files

Let's stary with 3 configuration files, simulating our environment. Firstly, appsettings.json:

{
  "Hosts": {
    "TestsEnabled": true,
    "Tenant":  "CA",
    "FirstApi": "http://ca.development.api1.localhost",
    "SecondApi": "http://ca.development.api2.localhost"
  }
}

Then we have appsettings.Staging.CA.json:

{
  "Hosts": {
    "Tenant": "CA",
    "FirstApi": "http://ca.staging.api1.localhost",
    "SecondApi": "http://ca.staging.api2.localhost"
  }
}

And finally we have appsettings.Staging.UK.json:

{
  "Hosts": {
    "Tenant": "UK",
    "FirstApi": "http://uk.staging.api1.localhost",
    "SecondApi": "http://uk.staging.api2.localhost"
  }
}

What's important to note here is that we're not overridding every property in the app settings for Staging CA and UK. The TestsEnabled property is left the same, so will not be overridden.

This allows us to build a set of base config files, and only override the behaviour we need to be environment specific later on.

Model

Next we have a basic DTO class representing the configuration:

namespace EnvironmentConfig.Model
{
    public class HostsConfiguration
    {
        public bool TestsEnabled { get; set; }

        public string Tenant { get; set; }

        public string FirstApi { get; set; }

        public string SecondApi { get; set; }
    }
}

NuGet References

To use the ConfigurationBuilder in this context we need to reference 2 NuGet packages:

Configuration Loader

Now we have that set up, the last piece of the puzzle is the loader itself. This controls which settings files to load into the configuration builder:

using EnvironmentConfig.Model;
using Microsoft.Extensions.Configuration;
using System.Globalization;
using System.IO;

namespace EnvironmentConfig
{
    public class ConfigurationLoader
    {
        private const string DefaultSettingsFile = "./Config/appsettings.json";

        /// <summary>
        /// Loads the configuration for the given environment and tenant.
        /// If no environment or tenant is provided, then the default local config is loaded.
        /// </summary>
        /// <param name="environment">The environment to point to.</param>
        /// <param name="tenant">The country to target.</param>
        /// <returns>The relevant configuration for .</returns>
        public HostsConfiguration Load(string environment = "", string tenant = "")
        {
            string targetJsonFile = DefaultSettingsFile;

            if (!string.IsNullOrWhiteSpace(environment) && !string.IsNullOrWhiteSpace(tenant))
            {
                // Override default config with environment specific config
                environment = FormatEnvironment(environment);
                tenant = FormatTenant(tenant);
                targetJsonFile = $"./Config/appsettings.{environment}.{tenant}.json";
            }
            
            // Loads the default config, then overrides matching keys 
            // with the environment specific version
            var configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile(DefaultSettingsFile)
                .AddJsonFile(targetJsonFile)
                .Build();

            var gitHubConfiguration = new HostsConfiguration();
            
            // Deserialises the config into the HostsConfiguration object
            configuration.GetSection("Hosts").Bind(gitHubConfiguration);

            return gitHubConfiguration;
        }

        private string FormatEnvironment(string environment)
        {
            return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(environment);
        }

        private string FormatTenant(string tenant)
        {
            return tenant.ToUpper();
        }
    }
}

Using The Loader

Now that's setup we can use the loader to pull in the correct configuration for our environment:

var configLoader = new ConfigurationLoader();
var configuration = configLoader.Load("Staging", "UK");

Console.WriteLine($"Tests Enabled: {configuration.TestsEnabled}");
Console.WriteLine($"Tenant: {configuration.Tenant}");
Console.WriteLine($"First API: {configuration.FirstApi}");
Console.WriteLine($"Second API: {configuration.SecondApi}");

The output from this will be:

Tests Enabled: true
Tenant: UK
First API: http://uk.staging.api1.localhost
Second API: http://uk.staging.api2.localhost

And that's how to load environment specific configuration in DotNet Core.

Image credit: Tooth & Tail - Wikimedia