.NET Configuration
Configuration abstractions in .NET
In this blog post, we will go through the:
- various configuration sources that we might have
- abstractions that Microsoft provides that help us deal with the configuration
- practical examples where we get to configure and read configuration from various sources
Pour some coffee, or a tea - whatever you prefer and enjoy.
Configuration sources
There are plenty of options available where our application configuration can come from.
It can be from
- a file (different formats supported like JSON, XML, …)
- Environment variables
- command line arguments
- In-Memory
- Azure Key Vault
- Azure App Configuration
- third party services
- many more ….
We can even add our own custom source of configuration. Maybe some of our services, or a database, you name it.
An abstraction that is supposed to get configuration data from any of the sources is named IConfigurationProvider
Therefore, there is a provider for each of the listed sources. And for those that we created, we would create our own custom providers.
Some of the providers are:
- File Configuration Provider
- e.g. JSON
Microsoft.Extensions.Configuration.Json
package
- e.g. JSON
- Environment Variables Configuration Provider
Microsoft.Extensions.Configuration.EnvironmentVariables
package - Azure Key Vault Configuration Provider
Azure.Extensions.AspNetCore.Configuration.Secrets
package
To see a more exhaustive list, visit Configuration Providers
To be able to read configuration, we need to configure the source it comes from.
Let’s see how that’s done in the context of JSON file configuration source.
var configuration = new ConfigurationBuilder()
.AddJsonFile(
"settings.json",
optional: true,
reloadOnChange: true)
If we look behind the scenes, AddJsonFile
just adds JsonConfigurationSource
as a source of our configuration, associates it with the path that we provided “settings.json”
and provides a Build method that exposes its provider JsonConfigurationProvider
public class JsonConfigurationSource : FileConfigurationSource
{
public override IConfigurationProvider Build(
IConfigurationBuilder builder)
{
EnsureDefaults(builder);
return new JsonConfigurationProvider(this);
}
}
If we would want to create our own source we would inherit either a base interface IConfigurationSource
or some the derived ones (e..g. FileConfigurationSource
) and override Build
method to build our custom provider.
For instance, if you are building a Console app, probably you start with the application builder
var host = Host.CreateApplicationBuilder(args).Build();
Underneath, ApplicationBuilder
deals withIConfigurationBuilder
. Similarly with the WebApplicationBuilder
which derivesIApplicationBuilder
In fact, in both cases, list of configuration sources is preconfigured. Let’s have a deeper look:
// framework code
Configuration.AddEnvironmentVariables(prefix: "DOTNET_");
...
appConfigBuilder
.AddJsonFile(
"appsettings.json",
optional: true,
reloadOnChange: reloadOnChange)
.AddJsonFile(
$"appsettings.{env.EnvironmentName}.json",
optional: true,
reloadOnChange: reloadOnChange);
appConfigBuilder.AddEnvironmentVariables();
...
Here’s a way to see all pre-configured config sources:
var hostApplicationBuilder =
Host.CreateApplicationBuilder(args);
foreach(var configSource in hostApplicationBuilder
.Configuration
.Sources)
{
Console.WriteLine(configSource);
}
// Outputs:
// Microsoft.Extensions.Configuration.Memory.MemoryConfigurationSource
// Microsoft.Extensions.Configuration.Memory.MemoryConfigurationSource
// Microsoft.Extensions.Configuration.EnvironmentVariables.EnvironmentVariablesConfigurationSource
// Microsoft.Extensions.Configuration.CommandLine.CommandLineConfigurationSource
// Microsoft.Extensions.Configuration.Json.JsonConfigurationSource
// Microsoft.Extensions.Configuration.Json.JsonConfigurationSource
// Microsoft.Extensions.Configuration.EnvironmentVariables.EnvironmentVariablesConfigurationSource
// Microsoft.Extensions.Configuration.CommandLine.CommandLineConfigurationSource
Don’t get confused by the duplicates. Those are different configuration source instances of the same type.
i.e. in case of JSON configuration source type we might have multiple files where configuration comes from, therefore we configure multiple instances.
Or Environment Variables with different prefixes (a way to group ENV variables, more on that later..).
Configuration
instance that we’re accessing is of type ConfigurationManager
It is both (inheriting) an IConfigurationBuilder
and an IConfigurationRoot
.
IConfigurationBuilder
is an interface that allows us to configure our configuration sources
IConfigurationRoot: IConfiguration
is a single representation of configuration sources.
It’s a readonly
abstraction that allows us to read configuration values represented in key-value format.
Let’s go through an example, where we configure multiple configuration sources and read from them.
Walkthrough
For the sake of an example, let’s assume we have our configuration defined in the JSON file settings.json
// settings.json
{
"Secret": "MySecret",
"Project": {
"Name": "MySecretProject",
"Author": "JohnDoe"
},
"ConnectionStrings": {
"my_connection_string": "my super secret db connection string"
}
}
Besides, we would have Environment variables defined
// environment variables
setx MyOwnEnvVariable__Secret="Environment Variable Secret"
setx MyOwnEnvVariable__Project__Name="EnvProjectName"
setx MyOwnEnvVariable__Project__Author="JohnEnvDoe"
// __ is a way to hierarchically define a variable.
And pass Command Line arguments to the program execution.
dotnet run --Project:Name "ConsoleProjectName" --Project:Author "ConsoleAuthor”
All these three are our configuration sources and we would define them as such:
var configurationBuilder =
new ConfigurationBuilder()
.AddJsonFile(
"settings.json",
optional: false,
reloadOnChange: true)
// include only ENV variables with the given prefix
.AddEnvironmentVariables(prefix: "MyOwnEnvVariable__");
// if there are command line args passed configure CommandLine as a source as well
if (args is { Length: > 0 })
{
configurationBuilder.AddCommandLine(args);
}
When there are multiple providers, values configured in later ones can override previously configured ones.
Once we build our configuration we would get an object of type IConfigurationRoot
(inherits IConfiguration
)
This abstraction exposes list of providers and ways to read our configuration.
There are multiple perspectives we might look from our configuration:
- flatten list of
key-value
pairs - structured, sections and subsections
Let’s see both approaches
var configurationRoot = configurationBuilder.Build();
Console.WriteLine(configurationRoot["Secret"]);
Console.WriteLine(configurationRoot["Project:Name"]);
// yields same results as
Console.WriteLine(configurationRoot.GetSection(key: "Secret").Value);
Console.WriteLine(configurationRoot.GetSection(key: "Project:Name").Value);
// to read all configuration pairs
foreach (var config in configurationRoot.AsEnumerable())
{
Console.WriteLine($"{config.Key}:{config.Value}");
}
Wouldn’t it be nice if we could map the configuration structure into an object and then access values by accessing object properties? - there is a way!
Using Binder (available through Microsoft.Extensions.Configuration.Binder
NuGet package)
internal record ProjectConfiguration(string Name, string Author);
...
ProjectConfiguration? projectConfiguration =
configurationRoot
.GetSection("Project")
.Get<ProjectConfiguration>(); // Microsoft.Extensions.Configuration.Binder nuget required
Console.WriteLine(projectConfiguration?.Name);
Console.WriteLine(projectConfiguration?.Author);
Or through DependencyInjection
via Options<>
pattern
var builder = Host.CreateApplicationBuilder(args);
var services = builder.Services;
services.Configure<ProjectConfiguration>(
builder.Configuration.GetSection("Project")
);
...
private readonly ProjectConfiguration configuration;
SomeConstructor(
IOptions<ProjectConfiguration> options)
{
configuration = options.Value;
}
...
Ok, let’s go back to our initial idea - providing multiple configuration sources and reading from them.
We briefly mentioned that the later sources might override the previously added ones (if they match the key). With the initially defined configuration if we invoke the program and read our configuration, the outputs would be as:
Console.WriteLine(configurationRoot["Project:Name"]); // ConsoleProjectName
Console.WriteLine(configurationRoot["Project:Author"]); // ConsoleAuthor
Console.WriteLine(configurationRoot["Secret"]); // Environment Variable Secret
The order we defined our sources was:
- JSON (File source)
- Environment Variables
- Command Line arguments
Therefore later ones would override the previous. Env variables can override JSON configuration data and Command Line can override both JSON and Environment Variables configuration.
In our example:
- Console did override project name and author
- Command Line did override the secret
Useful extensions
There are extension methods on the IConfigurationRoot
interface worth checking
-
GetDebugView
(returns human-readable view of a configuration and it’s sources)ConnectionStrings: my_connection_string=my super secret db connection string (JsonConfigurationProvider for 'settings.json' (Optional)) Project: Author=ConsoleAuthor (CommandLineConfigurationProvider) Name=ConsoleProjectName (CommandLineConfigurationProvider) Secret=Environment Variable Secret (EnvironmentVariablesConfigurationProvider Prefix: 'MyOwnEnvVariable__')
-
GetConnectionString
(gets a configuration from aConnectionStrings
section with the given name) -
GetRequiredSection
- tries to get a configuration value, if section doesn’t exist it throws anInvalidOperationException
exception
Conclusion
Configuration abstractions that we have give us a lot of options and flexibility. There are so many sources supported out of the box. If we see that our case isn’t we can consider implementing our own Provider. Both ways it’s pretty straightforward.
I hope this shad some light on the internals and usage of the configuration abstractions.
Feel free to grab a source code at my github.
See you in the next one :)