In this series I'm going to take a look at some of the new features coming in .NET 6. There's already been a lot of content written on .NET 6, including a lot of posts from the .NET and ASP.NET teams themselves. In this series I'm going to be looking at some of the code behind some of those features.
In this first post, I take a look at the ConfigurationManager
class, why it was added, and some of the code used to implement it.
Wait, what's ConfigurationManager
?
If your first response is "what's ConfigurationManager
", then don't worry, you haven't missed a big announcement!
ConfigurationManager
was added to support ASP.NET Core's new WebApplication
model, used for simplifying the ASP.NET Core startup code. However ConfigurationManager
is very much an implementation detail. It was introduced to optimise a specific scenario (which I'll describe shortly), but for the most part, you don't need to (and won't) know you're using it.
Before we get to the ConfigurationManager
itself, we'll look at what it's replacing and why.
Configuration in .NET 5
.NET 5 exposes multiple types around configuration, but the two primary ones you use directly in your apps are:
IConfigurationBuilder
- used to add configuration sources. CallingBuild()
on the builder reads each of the configuration sources, and builds the final configuration.IConfigurationRoot
- represents the final "built" configuration.
The IConfigurationBuilder
interface is mostly a wrapper around a list of configuration sources. Configuration providers typically include extension methods (like AddJsonFile()
and AddAzureKeyVault()
) that add a configuration source to the Sources
list.
public interface IConfigurationBuilder
{
IDictionary<string, object> Properties { get; }
IList<IConfigurationSource> Sources { get; }
IConfigurationBuilder Add(IConfigurationSource source);
IConfigurationRoot Build();
}
The IConfigurationRoot
meanwhile represents the final "layered" configuration values, combining all the values from each of the configuration sources to give a final "flat" view of all the configuration values.

In .NET 5 and earlier, the IConfigurationBuilder
and IConfigurationRoot
interfaces are implemented by ConfigurationBuilder
and ConfigurationRoot
respectively. If you were using the types directly, you might do something like this:
var builder = new ConfigurationBuilder();
// add static values
builder.AddInMemoryCollection(new Dictionary<string, string>
{
{ "MyKey", "MyValue" },
});
// add values from a json file
builder.AddJsonFile("appsettings.json");
// create the IConfigurationRoot instance
IConfigurationRoot config = builder.Build();
string value = config["MyKey"]; // get a value
IConfigurationSection section = config.GetSection("SubSection"); //get a section
In a typical ASP.NET Core app you wouldn't be creating the ConfigurationBuilder
yourself, or calling Build()
, but otherwise this is what's happening behind the scenes. There's a clear separation between the two types, and for the most part, the configuration system works well, so why do we need a new type in .NET 6?
The "partial configuration build" problem in .NET 5
The main problem with this design is when you need to "partially" build configuration. This is a common problem when you store your configuration in a service such as Azure Key Vault, or even in a database.
For example, the following is the suggested way to read secrets from Azure Key Vault inside ConfigureAppConfiguration()
in ASP.NET Core:
.ConfigureAppConfiguration((context, config) =>
{
// "normal" configuration etc
config.AddJsonFile("appsettings.json");
config.AddEnvironmentVariables();
if (context.HostingEnvironment.IsProduction())
{
IConfigurationRoot partialConfig = config.Build(); // build partial config
string keyVaultName = partialConfig["KeyVaultName"]; // read value from configuration
var secretClient = new SecretClient(
new Uri($"https://{keyVaultName}.vault.azure.net/"),
new DefaultAzureCredential());
config.AddAzureKeyVault(secretClient, new KeyVaultSecretManager()); // add an extra configuration source
// The framework calls config.Build() AGAIN to build the final IConfigurationRoot
}
})
Configuring the Azure Key Vault provider requires a configuration value, so you're stuck with a chicken and egg problem—you can't add the configuration source until you have built the configuration!
The solution is to:
- Add the "initial" configuration values
- Build the "partial" configuration result by calling
IConfigurationBuilder.Build()
- Retrieve the required configuration values from the resulting
IConfigurationRoot
- Use these values to add the remaining configuration sources
- The framework calls
IConfigurationBuilder.Build()
implicitly, generating the finalIConfigurationRoot
and using that for the final app configuration.
This whole dance is a little messy, but there's nothing wrong with it per-se, so what's the downside?
The downside is that we have to call Build()
twice: once to build the IConfigurationRoot
using only the first sources, and then again to build the IConfiguartionRoot
using all the sources, including the Azure Key Vault source.
In the default ConfigurationBuilder
implementation, calling Build()
iterates over all of the sources, loading the providers, and passing these to a new instance of the ConfigurationRoot
:
public IConfigurationRoot Build()
{
var providers = new List<IConfigurationProvider>();
foreach (IConfigurationSource source in Sources)
{
IConfigurationProvider provider = source.Build(this);
providers.Add(provider);
}
return new ConfigurationRoot(providers);
}
The ConfigurationRoot
then loops through each of these providers in turn and loads the configuration values.
public class ConfigurationRoot : IConfigurationRoot, IDisposable
{
private readonly IList<IConfigurationProvider> _providers;
private readonly IList<IDisposable> _changeTokenRegistrations;
public ConfigurationRoot(IList<IConfigurationProvider> providers)
{
_providers = providers;
_changeTokenRegistrations = new List<IDisposable>(providers.Count);
foreach (IConfigurationProvider p in providers)
{
p.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
}
}
// ... remainder of implementation
}
If you call Build()
twice during your app startup, then all of this happens twice.
Generally speaking, there's no harm in fetching the data from a configuration source more than once, but it's unnecessary work, and often involves (relatively slow) reading of files etc.
This is such a common pattern, that in .NET 6 a new type was introduced to avoid this "re-building", ConfigurationManager
.
Configuration Manager in .NET 6
As part of the "simplified" application model in .NET 6, the .NET team added a new configuration type, ConfigurationManager
. This type implements both IConfigurationBuilder
and IConfigurationRoot
. By combining both implementations in a single type, .NET 6 can optimise the common pattern show in the previous section.
With ConfigurationManager
, when an IConfigurationSource
is added (when you call AddJsonFile()
for example), the provider is immediately loaded, and the configuration is updated. This can avoid having to load the configuration sources more than once in the partial-build scenario.
Implementing this is a little harder than it sounds due to the IConfigurationBuilder
interface exposing the sources as an IList<IConfigurationSource>
:
public interface IConfigurationBuilder
{
IList<IConfigurationSource> Sources { get; }
// .. other members
}
The problem with this from the ConfigurationManager
point of view, is that IList<>
exposes Add()
and Remove()
functions. If a simple List<>
was used, consumers could add and remove configuration providers without the ConfigurationManager
knowing about it.
To work around this, ConfigurationManager
uses a custom IList<>
implementation. This contains a reference to the ConfigurationManager
instance, so that any changes can be reflected in the configuration:
private class ConfigurationSources : IList<IConfigurationSource>
{
private readonly List<IConfigurationSource> _sources = new();
private readonly ConfigurationManager _config;
public ConfigurationSources(ConfigurationManager config)
{
_config = config;
}
public void Add(IConfigurationSource source)
{
_sources.Add(source);
_config.AddSource(source); // add the source to the ConfigurationManager
}
public bool Remove(IConfigurationSource source)
{
var removed = _sources.Remove(source);
_config.ReloadSources(); // reset sources in the ConfigurationManager
return removed;
}
// ... additional implementation
}
By using a custom IList<>
implementation, ConfigurationManager
ensures AddSource()
is called whenever a new source is added. This is what gives ConfigurationManager
its advantage: calling AddSource()
immediately loads the source:
public class ConfigurationManager
{
private void AddSource(IConfigurationSource source)
{
lock (_providerLock)
{
IConfigurationProvider provider = source.Build(this);
_providers.Add(provider);
provider.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => provider.GetReloadToken(), () => RaiseChanged()));
}
RaiseChanged();
}
}
This method immediately calls Build
on the IConfigurationSource
to create the IConfigurationProvider
, and adds it to the provider list.
Next, the method calls IConfigurationProvider.Load()
. This loads the data into the provider, (e.g. from environment variables, a JSON file, or Azure Key Vault), and is the "expensive" step that this was all for! In the "normal" case, where you just add sources to the IConfigurationBuilder
, and may need to build it multiple times, this gives the "optimal" approach; sources are loaded once, and only once.
The implementation of Build()
in ConfigurationManager
is now a noop, simply returning itself.
IConfigurationRoot IConfigurationBuilder.Build() => this;
Of course software development is all about trade-offs. Incrementally building sources when they're added works well if you only ever add sources. However, if you call any of the other IList<>
functions like Clear()
, Remove()
or the indexer, the ConfigurationManager has to call ReloadSources()
private void ReloadSources()
{
lock (_providerLock)
{
DisposeRegistrationsAndProvidersUnsynchronized();
_changeTokenRegistrations.Clear();
_providers.Clear();
foreach (var source in _sources)
{
_providers.Add(source.Build(this));
}
foreach (var p in _providers)
{
p.Load();
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => p.GetReloadToken(), () => RaiseChanged()));
}
}
RaiseChanged();
}
As you can see, if any of the sources change, the ConfigurationManager
has to remove everything and start again, iterating through each of the sources, reloading them. This could quickly get expensive if you're doing a lot of manipulation of configuration sources, and would completely negate the original advantage of ConfigurationManager
.
Of course, removing sources would be very unusual—there's generally no reason to do anything other than add providers—so ConfigurationManager
is very much optimised for the most common case. Who would have guessed it? 😉
The following table gives a final summary of the relative cost of various operations using both ConfigurationBuilder
and ConfigurationManager
.
Operation | ConfigurationBuilder | ConfigurationManager |
---|---|---|
Add source | Cheap | Moderately Expensive |
Partially Build IConfigurationRoot | Expensive | Very cheap (noop) |
Fully Build IConfigurationRoot | Expensive | Very cheap (noop) |
Remove source | Cheap | Expensive |
Change source | Cheap | Expensive |
So, should I care about ConfigurationManager?
So having read all this way, should you care about whether you're using ConfigurationManager
or ConfigurationBuilder
?
Probably not.
The new WebApplicationBuilder
introduced in .NET 6 uses ConfigurationManager
, which optimises for the use case I described above where you need to partially build your configuration.
However, the WebHostBuilder
or HostBuilder
introduced in earlier versions of ASP.NET Core are still very much supported in .NET 6, and they continue to use the ConfigurationBuilder
and ConfigurationRoot
types behind the scenes.
The only situation I can think where you need to be careful is if you are somewhere relying on the IConfigurationBuilder
or IConfigurationRoot
being the concrete types ConfigurationBuilder
or ConfigurationRoot
. That seems very unlikely to me, and if you are relying on that, I'd be interested to know why!
But other than that niche exception, no the "old" types aren't going away, so there's no need to worry. Just be happy in the knowledge that if you need to do a "partial build", and you're using the new WebApplicationBuilder
, your app will be a tiny bit more performant!
Summary
In this post I described the new ConfigurationManager
type introduced in .NET 6 and used by the new WebApplicationBuilder
used in minimal API examples. ConfigurationManager
was introduced to optimise a common situation where you need to "partially build" configuration. This is typically because a configuration provider requires some configuration itself, for example loading secrets from Azure Key Vault requires configuration indicating which vault to use.
ConfigurationManager
optimises this scenario by immediately loading sources as they're added, instead of waiting till you call Build()
. This avoids the need for "rebuilding" the configuration in the "partial build" scenario. The trade-off is that other operations (such as removing a source) are expensive.