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 HostBuilder
s, 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 IConfiguration
s.
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 thetempConfigBuilder
- Create a new service collection
- Configure the service for
MyWebsiteConfig
- Build the
IServiceProvider
from theIServiceCollection
- Get the service from the
IServiceProvider
- Return the
env
,configurationRoot
, andmyWebsiteConfig
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:
Code for this post can be found: https://github.com/Kritner-Blogs/Kritner.ConfigDuringBootstrapNetCore
Top comments (0)