By now, most of us are familiar with the basics of bootstrapping a .NET 8 application. If you create a new .NET 8 API project, you will get boilerplate code that looks like this in your Program.cs
file:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
The WebApplication.CreateBuilder(args)
method hides a lot wiring things up under the covers, including loading configuration into the DI container for use by services. And, honestly, this is great, and a vast improvement over the old method(s) of loading configuration we had to use in the .NET Framework days. Microsoft has even documented the load order, which matches what you would expect:
appsettings.json
appsettings.<environment>.json
file, using the ASPNETCORE_ENVIRONMENT
environment variable defined on the hostDevelopment
environment)__
for level nesting in JSONEach subsequent configuration source in the load order can override settings that were loaded before it. For example, you might configuration your logging globally in the appsettings.json
file and then override the default level in your appsettings.<environment>.json
file to make sure you are only logging errors in production, but debug messages in development. Finally, you might change the environment variable on one deployment in production (and restart the service) to troubleshoot some temporary issue.
We are not limited to only the configuration loaded by default. We might want to separate our configuration into logical chunks, rather than one large file. We might want to load secrets from files mounted in volumes to our containers to avoid exposing them as Environment variables.
The good news is that we can add any number of custom configurations. We just have to add some code to do that, like the following:
var config = new ConfigurationBuilder().AddJsonFile("appsettings.Override.json").Build();
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddConfiguration(config);
// Rest of builder registrations skipped
var app = builder.Build();
// Rest of Program.cs skipped
In addition to JSON files, we can also add XML, key per file, and many others (as outlined here).
Now, as you might have gathered from the code sample above, there is something odd going on with the load order.
Our appsettings.Override.json
has the following content:
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.AspNetCore": "Error"
}
},
"AllowedHosts": "*",
"ASPNETCORE_ENVIRONMENT": "Test"
}
When we start the application, knowing the loading order and that the value of the ASPNETCORE_ENVIRONMENT
environment variable is Development
, what would you expect the value of ASPNETCORE_ENVIRONMENT
to be after we finish loading all the configuration?
If you said Test
, you would be right, and you win a cookie. If you said anything else, well, you are making an assumption about how all this works, and you know what they say about assumptions.
This behavior might seem a bit strange, but it is important to remember a key part of configuring pipelines in modern dotnet - order matters.
So, in this case, you have an already established pipeline, as defined by the WebApplication.CreateBuilder(args)
method call. Thus, when you add a new configuration pipeline, in this case defined on the first line, it adds it to the existing pipeline, rather than replacing the pipeline you had already.
This is probably not the behavior you expected, and it can lead to unexpected configuration values being used in your running application.
So, how do you control this?
There are two options:
The right choice for you is the one that works best with your workflow and company culture!