In my work as a software developer, I have already been involved in numerous existing .NET software projects. Nearly all of these existing code bases had at least one thing in common: application configuration was not obvious. But why? Especially in the .NET ecosystem everything required to make it obvious is there. In this blog post I share my personal best practice to counteract this situation. The focus is on ASP.NET Core web applications with dependency injection.
About configuration in ASP.NET Core
Let’s first start with a few words about application configuration in ASP.NET Core. ASP.NET Core applications configure and launch a host which is responsible for startup and lifetime management of the application. Configuration is divided into two parts, the application configuration and the host configuration. As the title suggests, this post focuses on the application configuration.
ASP.NET Core web applications created from the .NET default templates which are shipped with the .NET SDK, initialize an instance of type WebApplicationBuilder with preconfigured defaults. During initialization of the WebApplicationBuilder, a configuration object is created and different configuration providers are called in a specific order which provide configurations from different sources. If the same configuration (key-value pair) occurs in several sources, the configuration that comes from the source that is provided last wins.
The order is as follows
- Host configuration
appsettings.jsonappsettings.{Environment}.json- User secrets, if environment is
Development - Non-prefixed environment variables
- Command-line arguments
For more details about order and priority, see Configuration in ASP.NET Core section Default application configuration sources.
There are additional configuration providers which can be added if required (i.e. configuration providers for Azure Key Vault or Azure App Configuration).
Use of Application Configuration
For the sake of completeness, I will also briefly discuss how to use the application configuration.
When using dependency injection, application configuration can be injected in different ways. The two most common ways are:
- Inject
IConfiguration - Inject
IOptions<TOptions>orIOptionsMonitor<TOptions>
Let’s have a look at an example. When having the following appsettings.json file, the configuration can be injected as follows using the two ways listed above.
appsettings.json
{
"ArbitrarySection": {
"ArbitraryKey": "Arbitrary value",
"AnotherKey": "Another value"
}
}
Inject IConfiguration
public class ArbitraryService
{
private readonly IConfiguration _configuration;
public ArbitraryService(IConfiguration configuration)
{
_configuration = configuration;
}
public void ArbitraryMethod()
{
var arbitraryValue = _configuration["ArbitrarySection:ArbitraryKey"];
}
}
Inject IOptions<TOptions>
// ideally this class is placed in a separate file
public class ArbitraryOptions
{
public const string SectionName = "ArbitrarySection";
public string ArbitraryKey { get; set; } = String.Empty;
public string AnotherKey { get; set; } = String.Empty;
}
// excerpt of Program.cs
var builder = WebApplication.CreateBuilder(args);
...
builder.Services.Configure<ArbitraryOptions>(
builder.Configuration.GetSection(ArbitraryOptions.SectionName));
var app = builder.Build();
...
public class ArbitraryService
{
private readonly ArbitraryOptions _options;
public ArbitraryService(IOptions<ArbitraryOptions> options)
{
_options = options.Value;
}
public void ArbitraryMethod()
{
var arbitraryValue = _options.ArbitraryKey;
}
}
I personally prefer the options pattern as it provides strongly typed access to groups of related settings. Furthermore the options pattern helps making configuration obvious as the usage of specific options / configuration entries can be found easily by searching for references or usages of the corresponding options class.
Additionally, the options pattern allows validation of the option values using DataAnnotations. The validation can be either run on creation of the options object or when the app starts. To learn more about the options pattern, check out the official documentation.
How to make it obvious
Now we come to the most important part of this post. It’s about how to structure configuration to make it obvious using existing mechanisms. As already stated in the intro, it’s my personal best practice which is based on my experience as a software developer and DevOps engineer.
In existing codebases of ASP.NET Core web applications, I have often encountered the following situations – or even a combination of them.
- Client secrets, passwords, API keys, … for local development in
appsettings.jsonorappsettings.Development.json - All or part of the configuration for local development in
appsettings.json - Duplicated configuration (exact the same values) in
appsettings.jsonandappsettings.Development.json
The first one is an absolute NO-GO. Secrets for local development belong into secrets.json file.
Furthermore, I strongly believe that it is best to place the configuration where it fits best and try to avoid duplicates. This enables new developers to quickly understand the application configuration, as the same key only exists several times if the corresponding value is overwritten.
To illustrate where to put what I visualized my personal best practice.

On the right-hand side you find the sources of configuration considered by the configuration providers which are registered/called by default. However, there is one exception: the top most source (Azure Key Vault) is only considered, if `builder.Configuration.AddAzureKeyVault(…)` is called explicitly.
For more details about Azure Key Vault integration see [HOWTO] Rotate Azure Key Vault secrets used by an ASP.NET Core Web API with Terraform on every deployment.
On the left-hand side you can see which sort of configuration entries should be added to which source.
Hint regarding user secrets: store the most recent version of secrets.json in the companies password manager and give the development team members access to it. Additionally, add a hint to the README.md file of the repository that developers need to download the secrets.json file from the password manager and set/override the user secrets of the startup project accordingly.
The arrow in the middle visualizes the order in which the configuration providers are called and the overlay of the layers illustrates priority.
Besides making configuration more obvious, this approach additionally improves maintainability and facilitates automation. Imagine you are a DevOps engineer and the application configuration follows the approach described here. If you need to automate infrastructure deployment using Infrastructure as Code (IaC), you exactly know what secrets need to be created in the Azure Key Vault (the ones defined in secrets.json – obviously with different values per environment) and what configuration needs to be provided (the ones defined in appsettings.Development – obviously with different values per environment).
That’s it. I’m curious about feedbacks.

Leave a Reply