loading...
Cover image for How to rely on configuration during application bootstrapping?

How to rely on configuration during application bootstrapping?

kritner profile image Russ Hammett Originally published at blog.kritner.com on ・6 min read

Working with the IHostBuilder introduced to .netcore has been an overall pleasant experience… until I needed configuration from appsettings for app bootstrapping. Luckily, there’s still a way to do it!

Introduction

The docs on IHostBuilder are a good place to start if you’ve not worked with it before - though if you’ve found this post I’d imagine you have and are having the same struggle as me!

The idea of the IHostBuilder is the place where you “set up” all the startup bits of the application - the “services” that are a part of it like WebHost, Orleans, Logging, IOC configuration, etc.

Here’s an example of what it can look like:

private static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, builder) =>
        {
            var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
            if (string.IsNullOrWhiteSpace(env))
            {
                throw new Exception("ASPNETCORE_ENVIRONMENT env variable not set.");
            }

            context.HostingEnvironment.EnvironmentName = env;

            builder
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)
                .AddJsonFile($"appsettings.{env}.json", optional: false, reloadOnChange: false);
        })
        .ConfigureLogging(builder => { builder.AddConsole(); })
        .ConfigureServices((hostContext, services) =>
        {
            // Add some services
        })
        .ConfigureWebHostDefaults(builder =>
        {
            builder.UseUrls($"https://*:443");
            builder.UseStartup<Startup>();
        });

The above should be pretty straight forward, though I am setting some things “again” that the CreateDefaultBuilder has already set - like logging and the loading of app settings, for example. What is the issue with the above? Well in my case, I needed a way to be able to change the port the web host was running on, depending on the environment.

There are ways to accomplish this with environment variables, as well as variables passed in the args at application run, but I wanted to do it via configuration files. I needed a way to get configuration files “loaded and accessible” during the ConfigureWebHostDefaults.

First, we need a class that will represent our configuration, I’ve gone over a bit of this before in dotnet core console application IOptions configuration; see that for a refresher if needed.

The POCO:

public class MyWebsiteConfig
{
    public int Port { get; set; }
}

The appsettings.json:

{ "MyWebsiteConfig": { "Port": 443 }}

The appsettings.prod.json:

{ "MyWebsiteConfig": { "Port": 4337 }}

In the above, sure we could have just use a “root” level property in our config, but I like doing it this way for getting a strongly typed IOptions<T>, as well as demonstrating the fact you could do this with multiple properties via the strongly typed configuration object within your application bootstrapping.

The problem

So, why can’t we just do what we usually do and either inject the IOptions<T> or get the service from our IServiceProvider? The problem I was running into is the place where I’d need the MyWebSiteConfig - under ConfigureWebHostDefaults from the intro, has not yet actually “built” the configuration by ingesting the config files via ConfigureAppConfiguration nor set up the services via ConfigureServices. This can be confirmed by placing breakpoints in each section (ConfigureWebHostDefaults, ConfigureAppConfiguration, and ConfigureServices) and observing that ConfigureWebHostDefaults is the first breakpoint to hit, well before the things we need from configuration are actually loaded.

My initial thoughts were to just create two HostBuilders, one to load the settings I need, get an instance of my MyWebSiteConfig and pass it into a new HostBuilder that will do (some) of the work again, but this time I’ll have access to what I need.

This seemed to work, aside from the fact that I got a few warnings that stated something to the effect of “don’t do this cuz singleton scoped things will be weird” - I don’t recall the exact warning (or was it error?), but I immediately went on to find another way to do it.

The solution

Thankfully, I found something promising IConfigurationBuilder.AddConfiguration. This extension method allows for that adding of an IConfiguration onto an IConfigurationBuilder. What does this mean? It means that I can do almost was I was working toward from “the problem” above, but rather than two separate HostBuilders, we’ll use two separate IConfigurations.

So what does our first IConfiguration need to be made up of? We know we at least need the app settings files loaded, and a service provider that can return an instance of our MyWebsiteConfig. That looks like this:

private static (string env, IConfigurationRoot configurationRoot, MyWebsiteConfig myWebsiteConfig) BootstrapConfigurationRoot()
{
    var env = GetEnvironmentName();
    var tempConfigBuilder = new ConfigurationBuilder();

    tempConfigBuilder
        .AddJsonFile($"appsettings.json", optional: false, reloadOnChange: false)
        .AddJsonFile($"appsettings.{env}.json", optional: false, reloadOnChange: false);

    var configurationRoot = tempConfigBuilder.Build();

    var serviceCollection = new ServiceCollection();
    serviceCollection.Configure<MyWebsiteConfig>(configurationRoot.GetSection(nameof(MyWebsiteConfig)));
    var serviceProvider = serviceCollection.BuildServiceProvider();
    var myWebsiteConfig = serviceProvider.GetService<IOptions<MyWebsiteConfig>>().Value;
    return (env, configurationRoot, myWebsiteConfig);
}

private static string GetEnvironmentName()
{
    var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
    if (string.IsNullOrWhiteSpace(env))
    {
        throw new Exception("ASPNETCORE_ENVIRONMENT env variable not set.");
    }

    return env;
}

In the above I’m returning a named tuple with a env, configurationRoot, and myWebsiteConfig. Much of this should look familiar:

  • Get the environment name
  • Create a temporary configuration builder
  • Add the settings files to the builder using the environment
  • get the IConfigurationRoot by building the tempConfigBuilder
  • Create a new service collection
  • Configure the service for MyWebsiteConfig
  • Build the IServiceProvider from the IServiceCollection
  • Get the service from the IServiceProvider
  • Return the env, configurationRoot, and myWebsiteConfig

Now that we actually have an instance of MyWebsiteConfig, we are able to build our configuration dependent IHost:

private static IHostBuilder CreateHostBuilder(string[] args, string env, IConfigurationRoot configurationRoot, MyWebsiteConfig myWebsiteConfig) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, builder) =>
        {
            context.HostingEnvironment.EnvironmentName = env;
            builder.AddConfiguration(configurationRoot);
        })
        .ConfigureLogging(builder => { builder.AddConsole(); })
        .ConfigureServices((hostContext, services) =>
        {
            // Add some services
        })
        .ConfigureWebHostDefaults(builder =>
        {
            builder.UseUrls($"http://*:{myWebsiteConfig.Port}");
            builder.UseStartup<Startup>();
        });

In the above, we’re doing a lot of the same things as before, with just a few additions. First, our method signature is now receiving in addition to args, the three items from our named tuple. We’re able to add our existing configurationRoot onto the IHostBuilder, as well as set the environment. Now, we are able to utilize our myWebsiteConfig without having to worry about the ordering of the builder methods of IHostBuilder, since we already have our instance of MyWebsiteConfig prior to entering the method.

Here’s what it all looks like:

public static Main(string[] args)
{
    var (env, configurationRoot, myWebsiteConfig) = BootstrapConfigurationRoot();

    CreateHostBuilder(args, env, configurationRoot, myWebsiteConfig).Build().Run();
}

private static (string env, IConfigurationRoot configurationRoot, MyWebsiteConfig myWebsiteConfig) BootstrapConfigurationRoot()
{
    var env = GetEnvironmentName();
    var tempConfigBuilder = new ConfigurationBuilder();

    tempConfigBuilder
        .AddJsonFile($"appsettings.json", optional: false, reloadOnChange: false)
        .AddJsonFile($"appsettings.{env}.json", optional: false, reloadOnChange: false);

    var configurationRoot = tempConfigBuilder.Build();

    var serviceCollection = new ServiceCollection();
    serviceCollection.Configure<MyWebsiteConfig>(configurationRoot.GetSection(nameof(MyWebsiteConfig)));
    var serviceProvider = serviceCollection.BuildServiceProvider();
    var myWebsiteConfig = serviceProvider.GetService<IOptions<MyWebsiteConfig>>().Value;
    return (env, configurationRoot, myWebsiteConfig);
}

private static string GetEnvironmentName()
{
    var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
    if (string.IsNullOrWhiteSpace(env))
    {
        throw new Exception("ASPNETCORE_ENVIRONMENT env variable not set.");
    }

    return env;
}

private static IHostBuilder CreateHostBuilder(string[] args, string env, IConfigurationRoot configurationRoot, MyWebsiteConfig myWebsiteConfig) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, builder) =>
        {
            context.HostingEnvironment.EnvironmentName = env;
            builder.AddConfiguration(configurationRoot);
        })
        .ConfigureLogging(builder => { builder.AddConsole(); })
        .ConfigureServices((hostContext, services) =>
        {
            // Add some services
        })
        .ConfigureWebHostDefaults(builder =>
        {
            builder.UseUrls($"http://*:{myWebsiteConfig.Port}");
            builder.UseStartup<Startup>();
        });

Running the app and breaking on the UseUrls line you can see:

Port value from config

Code for this post can be found: https://github.com/Kritner-Blogs/Kritner.ConfigDuringBootstrapNetCore

References

Discussion

pic
Editor guide