Updated on
ASP.NET Core configuration is a single flat dictionary of string keys and string values, assembled at startup from several sources and read through one interface, IConfiguration.
Nested JSON becomes flat keys joined by colons, so Logging:LogLevel:Default reaches the Default value two levels down in appsettings.json. Every source (files, environment variables, command-line arguments, a key vault) contributes to that same dictionary, and later sources overwrite earlier ones.
That is the whole model. Everything else in this article is a more convenient way to read the same dictionary: GetValue<T>() for one key, GetSection() for a subtree, and Bind() for a class.
Let’s dive in.
What Is IConfiguration?
Configuration is the set of values an application reads at startup instead of having them compiled in. Connection strings, log levels, feature switches, and API endpoints all belong here.
In ASP.NET Core those values live in one flat dictionary of string keys and string values, built by a chain of configuration providers and exposed through the IConfiguration interface.
A provider is just a source. The default chain reads appsettings.json, then appsettings.{Environment}.json, then user secrets in development, then environment variables, then command-line arguments.
Order is the mechanism. Each provider writes into the same dictionary, so a key set by a later provider overwrites the same key set by an earlier one. That is how an environment variable overrides a file without either knowing about the other.
Nothing here requires recompiling. Change a value, restart or reload, and the application behaves differently.
Outside ASP.NET Core, all of this ships as a set of NuGet packages. The abstractions live in Microsoft.Extensions.Configuration, but the strongly typed reads this article uses, GetValue<T>() and Bind(), are extension methods from Microsoft.Extensions.Configuration.Binder, which is a separate package:
PM> Install-Package Microsoft.Extensions.Configuration.Binder in the Package Manager Console in Visual Studio, or
dotnet add package Microsoft.Extensions.Configuration.Binder if you prefer the dotnet CLI.
If you create an ASP.NET Core application, you don’t have to worry about either of them since they are referenced by default. A console application is the case where the packages matter, and we walk through that setup in a .NET console application.
Configuration in .NET is even more powerful with the use of sections, configuration providers, and the Options pattern. We’ll talk about all of these concepts in this article, as well as later on.
How Do We Define Configuration Data?
Configuration data is defined as a set of key-value pairs.
Values can be:
- integers – can be any integer number, used when we need a numerical value, like the maximum number of items, or a default temperature for example
DefaultRoomTemperature = 21 - booleans – can be true or false. Used often to determine if a behavior should be triggered or not within our application
TurnOnDetailedReports = true - strings – if we need a specific string value. Typical examples, but not limited to these are connection strings and URLs
sqlConnection = "Server=.;Database=AccountOwnerDatabase;Trusted_Connection=True;TrustServerCertificate=True"
These are just examples and besides a value, each key contains information about which level of the hierarchy it’s in.
Let’s see what that means.
Hierarchical Data Organization and Data Flattening
The Configuration API reads the hierarchical data by flattening the structure using delimiters.
This means we can write something like this in our configuration file:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
And later on, access it in our code by using the colon delimiter “:”.
Examples:
"Logging:LogLevel:Default" – We can get the Default logging level
"AllowedHosts" – We’ll get “*” (any host) in this case
This means we can have multiple keys named “Default” since the organization is hierarchical.
Let’s extend our example:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"OtherLoggingProvider": {
"LogLevel": {
"Default": "Debug"
}
},
"AllowedHosts": "*"
}
Now we can access the same key within the different hierarchy:
"Logging:LogLevel:Default" – returns “Information”
"OtherLoggingProvider:LogLevel:Default" – returns “Debug”
If the value doesn’t exist, we’ll get a null as a result.
Instead of accessing the values directly, using delimiters, we can use the options pattern, and GetSection() and GetChildren() methods to get sections and children of a section. These mechanisms make the configuration much easier to use. We’ll see how to use them later on.
Environment variables are the exception: a colon does not work as a key separator on every platform, so the configuration system reads a double underscore there and converts it, making Logging__LogLevel__Default the same key as Logging:LogLevel:Default.
Let’s see what we get out of the box when we create an ASP.NET Core application.
What Configuration Does ASP.NET Core Load by Default?
A new ASP.NET Core application already has configuration wired up before we write a line. WebApplication.CreateBuilder(args) builds the chain and hands it back as builder.Configuration.
Five sources go in, and the order decides who wins. appsettings.json first, then appsettings.{Environment}.json, then user secrets when the environment is Development, then environment variables, then command-line arguments.
Read that list backwards for precedence. A command-line argument beats an environment variable, which beats a user secret, which beats the environment-specific file, which beats the base file.
The environment-specific file is the one most teams use daily. appsettings.Development.json only overrides the keys it names, so a base file holding twenty settings and a development file holding one connection string works exactly as expected.
The result is registered in dependency injection as IConfiguration, which is why any class in the application can ask for it without knowing where a single value came from.
That is the whole of Program.cs in a new project. The builder.Configuration property is both the finished configuration and the builder still open for further sources, which is why we can read from it and add to it in the same file:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
var app = builder.Build();
app.MapStaticAssets();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}")
.WithStaticAssets();
app.Run();
These key-value pairs are stored inside the IConfiguration, and it plays a big role in our application later on.
Reading values here, before the application is built, follows its own rules, and we cover them in get configuration during application startup.
Now that we know how the default configuration is populated in our application, let’s see how we can read it at runtime.
How Do We Read Values With IConfiguration?
IConfiguration is registered in dependency injection by the host, so any class can take it as a constructor parameter and read from it.
Three methods cover almost everything. The indexer, configuration["Logging:LogLevel:Default"], returns the raw string or null. GetValue<T>() returns a converted value, so GetValue<int>("MaxRetries") hands back an int. GetSection() returns a subtree that can be read with its own shorter keys.
A missing key is not an error. The indexer and GetValue<string>() both return null; GetValue<int>() returns 0, which is the failure mode worth knowing about, because a mistyped key looks exactly like a configured zero.
GetValue<T>() is an extension method from the configuration binder rather than a member of the interface itself. An ASP.NET Core project already references it; a console project has to add it.
Reading one key at a time stops scaling around the fourth setting. That is where binding a section to a class earns its place.
The registration is what makes this work: dependency injection is turned on by default in our application, so we can ask for IConfiguration in the HomeController class, or anywhere else.
Our controller takes its dependencies through a primary constructor, so adding configuration to it means one more parameter and one more field:
public class HomeController(ILogger<HomeController> logger, IConfiguration configuration) : Controller
{
private readonly ILogger<HomeController> _logger = logger;
private readonly IConfiguration _configuration = configuration;
}
Now we can use the _configuration field to access our values in the entire controller. Let’s create a dummy model first to populate it with our data. Let’s navigate to the Models folder and create a HomeModel class. This will be a simple class with a single property DefaultLogLevel:
public class HomeModel
{
public string? DefaultLogLevel { get; set; }
}
Let’s return to our controller, read our configuration, and send the data to our Home Index view:
public IActionResult Index()
{
var homeModel = new HomeModel
{
DefaultLogLevel = _configuration.GetValue<string>("Logging:LogLevel:Default")
};
return View(homeModel);
}
And change our Home Index view a bit:
@model HomeModel
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<span>Our default logging level is </span><strong>@Model.DefaultLogLevel</strong>
<p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>
Sure enough, our application now displays our configuration value:
As you can see, we’ve used a strongly typed GetValue() method to read our default logging level, using the same principle as before to navigate the hierarchy. If a single value is all we need, we can also read a value out of appsettings.json without a model at all.
Wiring several of these registrations up in a real project gets its own structure, and we build that out in Service Configuration in ASP.NET Core Web API With Extension Methods.
Configuration Sections
The GetSection() method helps us further by isolating separate sections or subsections of our configuration. To put that into perspective let’s look at our example once again:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"OtherLoggingProvider": {
"LogLevel": {
"Default": "Debug"
}
},
"AllowedHosts": "*"
}
In this case, “Logging” would be a section, and “LogLevel” is a subsection. Since we need just the LogLevel data, we can isolate it by returning just the LogLevel subsection in our HomeController Index action:
public IActionResult Index()
{
var logLevelSection = _configuration.GetSection("Logging:LogLevel");
var homeModel = new HomeModel
{
DefaultLogLevel = logLevelSection.GetValue<string>("Default")
};
return View(homeModel);
}
The result is the same as before.
Although this is a small example, imagine more complex settings file like we see every day in a real-world project. It contains a lot of different sections, for various parts of our application. Getting sections and organizing them logically would be a crucial task to make our application more readable, and less dependent on hard coded strings.
A common usage of the GetSection() method can be seen in the extension method GetConnectionString(), which lives on ConfigurationExtensions in Microsoft.Extensions.Configuration.Abstractions.
Its whole implementation is two lines:
public static string? GetConnectionString(this IConfiguration configuration, string name)
{
return configuration?.GetSection("ConnectionStrings")[name];
}
We can see that it’s just the implementation of the GetSection() method. It takes a name of the connection string and then tries to find it within the “ConnectionStrings” section.
That’s why our connection strings should be located within that section inside our appsettings.json file:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionStrings": {
"sqlConnection": "Server=.;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True"
},
"OtherLoggingProvider": {
"LogLevel": {
"Default": "Debug"
}
},
"AllowedHosts": "*"
}
We can get the string as easy as this now:
_configuration.GetConnectionString("sqlConnection");
Awesome!
We recommend that you use a similar pattern to get other sections of your configuration. If you are not familiar with extension methods check out our article on them to learn how they work.
Binding Configuration
We’ve seen how we can extract our configuration data by using IConfiguration. But it does have its flaws.
Having to type sections and keys to get the values can be really repetitive and error-prone. We risk introducing errors to our code, and these kinds of errors can cost us a lot of time until we discover them since someone else can introduce them, and we won’t notice them since a null result is returned when values are missing.
To overcome this problem, we can bind the configuration data to strongly typed objects. To do that, we can use the Bind() method.
We can quickly create a simple container class for the configuration called LoggingLevelConfiguration, inside our Models folder:
public class LoggingLevelConfiguration
{
public string? Default { get; set; }
}
And now, let’s make some changes to our HomeController Index() method:
public IActionResult Index()
{
var logLevelConfiguration = new LoggingLevelConfiguration();
_configuration.Bind("Logging:LogLevel", logLevelConfiguration);
var homeModel = new HomeModel
{
DefaultLogLevel = logLevelConfiguration.Default
};
return View(homeModel);
}
And once again, we can run the project and make sure that result is the same.
As you can see, instead of using GetValue() or GetSection() methods we’ve bound our configuration data to the LoggingLevelConfiguration section directly, and we’re accessing the configuration data by calling the Default property of that class.
Pretty neat, huh?
There are two things to note here though. First is that the names of the configuration data keys and class properties must match. The other is that if you extend the configuration, you need to extend the class as well, which can be a bit cumbersome, but it beats getting values by typing strings.
Binding is not limited to objects either. If a section holds a list rather than a set of named keys, we can bind a JSON array to a collection in the same way.
What Replaces ConfigurationManager.AppSettings in .NET?
ConfigurationManager.AppSettings is not part of the ASP.NET Core configuration system, and the host reads nothing out of Web.config. The replacement is IConfiguration, injected wherever it is needed.
The mechanical translation is short. ConfigurationManager.AppSettings["Key"] becomes configuration["Key"]. A connection string read through ConfigurationManager.ConnectionStrings["Db"].ConnectionString becomes configuration.GetConnectionString("Db"). An <appSettings> block becomes appsettings.json.
Two habits have to change with it. There is no static entry point, so configuration arrives by constructor injection rather than by calling a class in the middle of a method. And there is no Web.config transform per build configuration; environments are files, appsettings.Production.json beside appsettings.json.
Custom configuration sections translate best to the options pattern rather than to a hand-written class, which the next article in this series covers.
One habit is worth keeping. A Web.config that grouped settings by area maps cleanly onto JSON objects, so a migration is usually a re-shaping of the same keys rather than a redesign of what the application is configured with.
| .NET Framework | ASP.NET Core / .NET |
|---|---|
ConfigurationManager.AppSettings["Key"] | configuration["Key"] |
ConfigurationManager.AppSettings["Key"] as a typed value | configuration.GetValue<int>("Key") |
ConfigurationManager.ConnectionStrings["Db"].ConnectionString | configuration.GetConnectionString("Db") |
<appSettings> in Web.config | appsettings.json |
<connectionStrings> in Web.config | the ConnectionStrings section of appsettings.json |
A custom ConfigurationSection class | a section bound to a class with Bind() or the options pattern |
| Web.config transforms per build configuration | appsettings.{Environment}.json per environment |
ConfigurationManager.RefreshSection(), then read again | reloadOnChange: true on the file source, so the value reloads automatically rather than on request |
A published ASP.NET Core application does still ship a web.config, but only when it is hosted in IIS and only to point IIS at the ASP.NET Core Module. No application setting is ever read from it.
Creating an Environment Specific Configuration
Any application that is meant to go to the production has at least two environments – development and production. Besides that, we can have other environments, like staging which is an environment where we can check if the application is working correctly before we deploy it to production.
Check out our article about using multiple environments in ASP.NET Core to learn about it in more detail.
As you might have noticed, our project template has two appsettings files. One is the default appsettings.json file, and another is appsettings.Development.json. In this file, we can override any value from the appsettings.json file and it will be used when we are working in the development environment.
The practical example of the usage would be having different configuration strings for production and development since we don’t want to mess up the production database during development.
For example, this would be a connection string for the development:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"ConnectionStrings": {
"sqlConnection": "Server=.;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True"
},
"OtherLoggingProvider": {
"LogLevel": {
"Default": "Debug"
}
},
"AllowedHosts": "*"
}
And now we can create an appsettings.Production.json file in which we will change the connection string to something else:
{
"ConnectionStrings": {
"sqlConnection": "Server=ProductionServerName;Database=CodeMazeCommerce;Trusted_Connection=True;TrustServerCertificate=True"
}
}
That’s it. Once the application is running in the production environment (indicated by the ASPNETCORE_ENVIRONMENT environment variable), the production string from the appsettings.Production.json will be used instead of the default one.
Getting environments right is where configuration stops being a syntax question and starts being an architecture one: how the same application talks to a local database, a staging one, and a production one without a code change. The Ultimate ASP.NET Core Web API course builds that out end to end on the project this series starts.
Conclusion
In this article, we’ve looked at how IConfiguration works: where its values come from, how a hierarchy flattens into keys, how to read those keys with the indexer, GetValue() and GetSection(), how to bind a whole section to a class, and what to write instead of ConfigurationManager.AppSettings when moving an application from .NET Framework.
We still haven’t touched upon the options pattern or configuration providers, which are our next topics of this series.
Tested with .NET 10.0.10.


Why this tutorial does not follow the previous tutorial “Advanced ASP.NET Core Web API Concepts”
Hey Roy, this is not the next article in the series, although these concepts are easily applicable to that project or any other.