Examining The ConfigurationManager In .NET 6

Introduction

In this article, let's look at a few of the new features that will be available in.NET 6. Let's examine some of the underlying code for some of those features in this series.

What is ConfigurationManager?

For the purpose of streamlining the ASP.NET Core startup code, ConfigurationManager was introduced to accommodate the new WebApplication paradigm in ASP.NET Core. ConfigurationManager is, however, largely implementation specific. It was created to optimize a certain scenario, will go into more detail about it momentarily, but for the most part, we won't even notice that we're using it.

We'll examine what the ConfigurationManager replaces and why before moving on to the actual ConfigurationManager.

Configuration in.NET version 5

.NET 5 offers several configuration types, but the two main ones we'll see in our projects are,

Configuration sources are added using IConfigurationBuilder. The final configuration is built by reading all of the configuration sources when the builder's Build() method is called.

The final "constructed" configuration is represented by IConfigurationRoot.

A collection of configuration sources are mostly wrapped by the IConfigurationBuilder interface. Extension methods (like AddJsonFile() and AddAzureKeyVault()) that add a configuration source to the Sources list are frequently included by configuration providers.

public interface IConfigurationBuilder
{
    IDictionary<string, object> Properties { get; }
    IList<IConfigurationSource> Sources { get; }
    IConfigurationBuilder Add(IConfigurationSource source);
    IConfigurationRoot Build();
}

The IConfigurationRoot, on the other hand, stands in for the final "layered" configuration values, merging all the values from each configuration source to provide a final "flat" view of all the configuration values.

The IConfigurationBuilder and IConfigurationRoot interfaces are implemented by ConfigurationBuilder and ConfigurationRoot respectively in.NET versions 5 and earlier.

Why do we need a new type in .NET 6?

The .NET 5 "partial configuration build" issue

The primary issue with this method is when we have to "partially" construct settings. This is a typical issue when we save our settings in a service like Azure Key Vault or even in a database.

For instance, the following is the advised method to read secrets from Azure Key Vault inside ConfigureAppConfiguration() in ASP.NET Core:

.ConfigureAppConfiguration((context, config) =>
{
    // "Normal" set up, etc.
    config.AddJsonFile("appsettings.json");
    config.AddEnvironmentVariables();

    if (context.HostingEnvironment.IsProduction())
    {
        // construct a partial configuration
        IConfigurationRoot partialConfig = config.Build(); 
        //read the configuration's value
        string keyVaultName = partialConfig["KeyVaultName"]; 
        var secretClient = new SecretClient(
            new Uri($"https://{keyVaultName}.vault.azure.net/"),
            new DefaultAzureCredential());
	// include a second configuration source
        config.AddAzureKeyVault(secretClient, new KeyVaultSecretManager()); 
        // To create the final IConfigurationRoot, the framework makes ANOTHER call to config.Build().
    }
})

We're in a pickle since we need to build the configuration before we can add the configuration source because configuring the Azure Key Vault provider needs configuring a configuration value.

The answer is,

  • The "initial" configuration values should be included.
  • Call IConfigurationBuilder.Build to create the "partial" configuration outcome ()
  • The generated IConfigurationRoot should have the necessary configuration settings.
  • To add the remaining configuration sources, use these values.
  • In order to create the final IConfigurationRoot and use it for the final app configuration, the framework indirectly uses IConfigurationBuilder.Build().

The drawback is that we must use Build() twice: the first time to build the IConfigurationRoot using only the first sources, and the second time to build the IConfigurationRoot using all sources, including the Azure Key Vault source.

When invoking Build() on the default ConfigurationBuilder implementation, all sources are iterated over, the providers are loaded, and they are then passed to a fresh 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);
}

After loading the configuration settings, the ConfigurationRoot runs over each of these providers in turn.

All of this occurs twice if we run Build() twice when our app first launches. In general, there is no damage in requesting information from a configuration source more than once, although doing so is needless effort that frequently entails reading slow-moving files, etc.

Due to the prevalence of this pattern, ConfigurationManager, a new class, was created in.NET 6 to prevent this "re-building".

Configuration Manager in.NET version 6

The ConfigurationManager configuration type was introduced by the.NET team as part of the "simplified" application model in.NET 6. IConfigurationBuilder and IConfigurationRoot are both implemented by this type. The typical pattern shown in the preceding section may be optimized for.NET 6 by consolidating both implementations into a single type.

With ConfigurationManager, the provider is instantly loaded and the configuration is changed when an IConfigurationSource is added (by calling AddJsonFile(), for instance). In the partial-build situation, this can prevent the need to load the configuration sources more than once.

Because the IConfigurationBuilder interface exposes the sources as an IListIConfigurationSource>, implementing this is a little more difficult than it seems,

public interface IConfigurationBuilder{
    IList<IConfigurationSource> Sources { get; }
    // .. further included
}

From the perspective of the ConfigurationManager, this is problematic since IList> exposes the Add() and Remove() operations. Consumers might add and delete configuration providers without the ConfigurationManager being aware of it if a simple List> was used.

ConfigurationManager makes advantage of a unique IList> implementation to get around this. In order for any changes to be reflected in the configuration, this holds a reference to the ConfigurationManager instance,

private sealed class ConfigurationSources : IList<IConfigurationSource>, ICollection<IConfigurationSource>, IEnumerable<IConfigurationSource>, IEnumerable
{
    private readonly List<IConfigurationSource> _sources = new List<IConfigurationSource>();

    private readonly ConfigurationManager _config;

    public IConfigurationSource this[int index]
    {
        get
        {
            return _sources[index];
        }
        set
        {
            _sources[index] = value;
            _config.ReloadSources();
        }
    }
    public int Count => _sources.Count;
    public bool IsReadOnly => false;
    public ConfigurationSources(ConfigurationManager config)
    {
        _config = config;
    }

    public void Add(IConfigurationSource source)
    {
        _sources.Add(source);
        _config.AddSource(source); // add the source to the ConfigurationManager
    }
    public void Clear()
    {
        _sources.Clear();
        _config.ReloadSources();// reset sources in the ConfigurationManager
    }
    public bool Contains(IConfigurationSource source)
    {
        return _sources.Contains(source);
    }
    public void CopyTo(IConfigurationSource[] array, int arrayIndex)
    {
        _sources.CopyTo(array, arrayIndex);
    }
    public IEnumerator<IConfigurationSource> GetEnumerator()
    {
        return _sources.GetEnumerator();
    }
    public int IndexOf(IConfigurationSource source)
    {
        return _sources.IndexOf(source);
    }
    public void Insert(int index, IConfigurationSource source)
    {
        _sources.Insert(index, source);
        _config.ReloadSources();
    }
    public bool Remove(IConfigurationSource source)
    {
        bool result = _sources.Remove(source);
        _config.ReloadSources();
        return result;
    }
    public void RemoveAt(int index)
    {
        _sources.RemoveAt(index);
        _config.ReloadSources();
    }
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

ConfigurationManager guarantees that AddSource() is executed each time a new source is added by employing a unique IList> implementation. The benefit of ConfigurationManager is due to the following: the source is loaded immediately after using AddSource().

In the use case mentioned, when we need to partially create our setup, the new WebApplicationBuilder introduced in.NET 6 makes use of ConfigurationManager, which is optimized for it.

The WebHostBuilder or HostBuilder introduced in earlier versions of ASP.NET Core, however, are still very much supported in.NET 6, and they continue to use the ConfigurationBuilder and ConfigurationRoot classes invisibly.

Conclusion

In this article, we covered the new ConfigurationManager class that was added to.NET 6 and utilized by the new WebApplicationBuilder in the examples of the minimum API. When we need to "partially construct" a configuration, it is frequently necessary. ConfigurationManager was created to optimize this situation. Typically, this is because a configuration provider has to be configured in some way. For instance, importing secrets from Azure Key Vault requires settings stating which vault to use.