Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ ms.date: 08/02/2023
---
# Binding config to dictionary extends values

When binding a configuration using a <xref:System.Collections.Generic.Dictionary%602> object where the value is a mutable collection type, binding to the same key more than once now extends the values collection instead of replacing the whole collection with the new value.
When binding a configuration using a <xref:System.Collections.Generic.Dictionary`2> object where the value is a mutable collection type, binding to the same key more than once now extends the values collection instead of replacing the whole collection with the new value.

## Version introduced

Expand Down
99 changes: 77 additions & 22 deletions docs/core/extensions/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: Configuration
description: Learn how to use the Configuration API to configure .NET applications. Explore various inbuilt configuration providers.
ms.date: 10/09/2024
ms.topic: overview
ai-usage: ai-assisted
---

# Configuration in .NET
Expand All @@ -14,7 +15,7 @@ Configuration in .NET is performed using one or more [configuration providers](#
- [Azure Key Vault](/azure/key-vault/general/overview)
- [Azure App Configuration](/azure/azure-app-configuration/overview)
- Command-line arguments
- Custom providers, installed or created
- Custom providers (installed or created)
- Directory files
- In-memory .NET objects
- Third-party providers
Expand All @@ -30,7 +31,7 @@ Given one or more configuration sources, the <xref:Microsoft.Extensions.Configur

## Configure console apps

.NET console applications created using the [dotnet new](../tools/dotnet-new.md) command template or Visual Studio by default *don't* expose configuration capabilities. To add configuration in a new .NET console application, [add a package reference](../tools/dotnet-package-add.md) to [Microsoft.Extensions.Configuration](https://www.nuget.org/packages/Microsoft.Extensions.Configuration). This package is the foundation for configuration in .NET apps. It provides the <xref:Microsoft.Extensions.Configuration.ConfigurationBuilder> and related types.
.NET console apps created using the [dotnet new](../tools/dotnet-new.md) command template or Visual Studio by default *don't* expose configuration capabilities. To add configuration in a new .NET console application, [add a package reference](../tools/dotnet-package-add.md) to [📦 Microsoft.Extensions.Configuration](https://www.nuget.org/packages/Microsoft.Extensions.Configuration). This package is the foundation for configuration in .NET apps. It provides the <xref:Microsoft.Extensions.Configuration.ConfigurationBuilder> and related types.

:::code source="snippets/configuration/console-basic-builder/Program.cs":::

Expand All @@ -41,11 +42,11 @@ The preceding code:
- Calls the <xref:Microsoft.Extensions.Configuration.ConfigurationBuilder.Build> method to create an <xref:Microsoft.Extensions.Configuration.IConfiguration> instance.
- Writes the value of the `SomeKey` key to the console.

While this example uses an in-memory configuration, there are many configuration providers available, exposing functionality for file-based, environment variables, command line arguments, and other configuration sources. For more information, see [Configuration providers in .NET](configuration-providers.md).
While this example uses an in-memory configuration, there are many configuration providers available, exposing functionality for file-based, environment variables, command-line arguments, and other configuration sources. For more information, see [Configuration providers in .NET](configuration-providers.md).

### Alternative hosting approach
## Alternative hosting approach

Commonly, your apps will do more than just read configuration. They'll likely use dependency injection, logging, and other services. The [.NET Generic Host](generic-host.md) approach is recommended for apps that use these services. Instead, consider [adding a package reference](../tools/dotnet-package-add.md) to [Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting). Modify the *Program.cs* file to match the following code:
Commonly, your apps will do more than just read configuration. They'll likely use dependency injection, logging, and other services. The [.NET Generic Host](generic-host.md) approach is recommended for apps that use these services. Instead, consider [adding a package reference](../tools/dotnet-package-add.md) to [📦 Microsoft.Extensions.Hosting](https://www.nuget.org/packages/Microsoft.Extensions.Hosting). Modify the *Program.cs* file to match the following code:

:::code source="snippets/configuration/console/Program.cs" highlight="3":::

Expand All @@ -60,9 +61,9 @@ The <xref:Microsoft.Extensions.Hosting.Host.CreateApplicationBuilder(System.Stri

Adding a configuration provider overrides previous configuration values. For example, the [Command-line configuration provider](configuration-providers.md#command-line-configuration-provider) overrides all values from other providers because it's added last. If `SomeKey` is set in both *appsettings.json* and the environment, the environment value is used because it was added after *appsettings.json*.

### Binding
## Binding

One of the key advantages of using the .NET configuration abstractions is the ability to bind configuration values to instances of .NET objects. For example, the JSON configuration provider can be used to map *appsettings.json* files to .NET objects and is used with [dependency injection](dependency-injection/overview.md). This enables the [options pattern](options.md), which uses classes to provide strongly typed access to groups of related settings. The default binder is reflection-based, but there's a [source generator alternative](configuration-generator.md) that's easy to enable.
One of the key advantages of using the .NET configuration abstractions is the ability to *bind* configuration values to instances of .NET objects. For example, the JSON configuration provider can be used to map *appsettings.json* files to .NET objects and is used with [dependency injection](dependency-injection/overview.md). This enables the [options pattern](options.md), which uses classes to provide strongly typed access to groups of related settings. The default binder is reflection-based, but there's a [source generator alternative](configuration-generator.md) that's easy to enable.

.NET configuration provides various abstractions. Consider the following interfaces:

Expand All @@ -84,7 +85,7 @@ The binder can use different approaches to process configuration values:​
> - Properties are ignored if they have private setters or their type can't be converted.
> - Properties without corresponding configuration keys are ignored.

#### Binding hierarchies
### Binding hierarchies

Configuration values can contain hierarchical data. Hierarchical objects are represented with the use of the `:` delimiter in the configuration keys. To access a configuration value, use the `:` character to delimit a hierarchy. For example, consider the following configuration values:

Expand All @@ -110,7 +111,61 @@ The following table represents example keys and their corresponding values for t
| `"Parent:Child:Name"` | `"Example"` |
| `"Parent:Child:GrandChild:Age"` | `3` |

### Basic example
### Advanced binding scenarios

The configuration binder has specific behaviors and limitations when working with certain types. This section includes the following subsections:

- [Bind to dictionaries](#bind-to-dictionaries)
- [Dictionary keys with colons](#dictionary-keys-with-colons)
- [Bind to IReadOnly* types](#bind-to-ireadonly-types)
- [Bind with parameterized constructors](#bind-with-parameterized-constructors)

#### Bind to dictionaries

When you bind configuration to a <xref:System.Collections.Generic.Dictionary`2> where the value is a mutable collection type (like arrays or lists), repeated binds to the same key extend the collection values instead of replacing them.

The following example demonstrates this behavior:

:::code language="csharp" source="snippets/configuration/binding-scenarios/DictionaryBinding/Program.cs" id="DictionaryWithCollectionValues":::

For more information, see [Binding config to dictionary extends values](../compatibility/extensions/7.0/config-bind-dictionary.md).

#### Dictionary keys with colons

The colon (`:`) character is reserved as a hierarchy delimiter in configuration keys. This means you can't use colons in dictionary keys when binding configuration. If your keys contain colons (such as URLs or other formatted identifiers), the configuration system interprets them as hierarchy paths rather than literal characters. Consider the following workarounds:

- Use alternative delimiter characters (such as double underscores `__`) in your configuration keys and transform them programmatically if needed.
- Manually deserialize the configuration as raw JSON using <xref:System.Text.Json> or a similar library, which supports colons in keys.
- Create a custom mapping layer that translates safe keys to your desired keys with colons.

#### Bind to IReadOnly\* types

The configuration binder doesn't support binding directly to `IReadOnlyList<T>`, `IReadOnlyDictionary<TKey, TValue>`, or other read-only collection interfaces. These interfaces lack the mechanisms the binder needs to populate the collections.

To work with read-only collections, use mutable types for the properties that the binder populates, then expose them as read-only interfaces for consumers:

:::code language="csharp" source="snippets/configuration/binding-scenarios/DictionaryBinding/Program.cs" id="IReadOnlyCollections":::

The configuration class implementation:

:::code language="csharp" source="snippets/configuration/binding-scenarios/DictionaryBinding/Program.cs" id="SettingsWithReadOnly":::

This approach allows the binder to populate the mutable `List<string>` while presenting an immutable interface to consumers through `IReadOnlyList<string>`.

#### Bind with parameterized constructors

Starting with .NET 7, the configuration binder supports binding to types with a single public parameterized constructor. This enables immutable types and records to be populated directly from configuration:

:::code language="csharp" source="snippets/configuration/binding-scenarios/DictionaryBinding/Program.cs" id="ParameterizedConstructor":::

The immutable settings class:

:::code language="csharp" source="snippets/configuration/binding-scenarios/DictionaryBinding/Program.cs" id="AppSettings":::

> [!IMPORTANT]
> The binder only supports types with a single public parameterized constructor. If a type has multiple public parameterized constructors, the binder cannot determine which one to use and binding will fail. Use either a single parameterized constructor or a parameterless constructor with property setters.

## Basic example

To access configuration values in their basic form, without the assistance of the _generic host_ approach, use the <xref:Microsoft.Extensions.Configuration.ConfigurationBuilder> type directly.

Expand Down Expand Up @@ -148,7 +203,7 @@ The `Settings` object is shaped as follows:

:::code source="snippets/configuration/console-raw/NestedSettings.cs":::

### Basic example with hosting
## Basic example with hosting

To access the `IConfiguration` value, you can rely again on the [`Microsoft.Extensions.Hosting`](https://www.nuget.org/packages/Microsoft.Extensions.Hosting) NuGet package. Create a new console application, and paste the following project file contents into it:

Expand All @@ -173,7 +228,7 @@ When you run this application, the `Host.CreateApplicationBuilder` defines the b
> [!TIP]
> Using the raw `IConfiguration` instance in this way, while convenient, doesn't scale very well. When applications grow in complexity, and their corresponding configurations become more complex, we recommend that you use the [_options pattern_](options.md) as an alternative.

### Basic example with hosting and using the indexer API
## Basic example with hosting and using the indexer API

Consider the same _appsettings.json_ file contents from the previous example:

Expand All @@ -189,17 +244,17 @@ The values are accessed using the indexer API where each key is a string, and th

The following table shows the configuration providers available to .NET Core apps.

| Provider | Provides configuration from |
|------------------------------------------------------------------------------------------------------------------------|------------------------------------|
| [Azure App configuration provider](/azure/azure-app-configuration/quickstart-aspnet-core-app) | Azure App Configuration |
| [Azure Key Vault configuration provider](/azure/key-vault/general/tutorial-net-virtual-machine) | Azure Key Vault |
| [Command-line configuration provider](configuration-providers.md#command-line-configuration-provider) | Command-line parameters |
| [Custom configuration provider](custom-configuration-provider.md) | Custom source |
| [Environment Variables configuration provider](configuration-providers.md#environment-variable-configuration-provider) | Environment variables |
| [File configuration provider](configuration-providers.md#file-configuration-provider) | JSON, XML, and INI files |
| [Key-per-file configuration provider](configuration-providers.md#key-per-file-configuration-provider) | Directory files |
| [Memory configuration provider](configuration-providers.md#memory-configuration-provider) | In-memory collections |
| [App secrets (Secret Manager)](/aspnet/core/security/app-secrets) | File in the user profile directory |
| Configuration provider | Provides configuration from |
|--------------------------------------------------------------------------------|-----------------------------|
| [Azure App Configuration](/azure/azure-app-configuration/quickstart-aspnet-core-app) | Azure App Configuration |
| [Azure Key Vault](/azure/key-vault/general/tutorial-net-virtual-machine) | Azure Key Vault |
| [Command-line](configuration-providers.md#command-line-configuration-provider) | Command-line parameters |
| [Custom](custom-configuration-provider.md) | Custom source |
| [Environment variables](configuration-providers.md#environment-variable-configuration-provider) | Environment variables |
| [File](configuration-providers.md#file-configuration-provider) | JSON, XML, and INI files |
| [Key-per-file](configuration-providers.md#key-per-file-configuration-provider) | Directory files |
| [Memory](configuration-providers.md#memory-configuration-provider) | In-memory collections |
| [App secrets (secret manager)](/aspnet/core/security/app-secrets) | File in the user profile directory |

> [!TIP]
> The order in which configuration providers are added matters. When multiple configuration providers are used and more than one provider specifies the same key, the last one added is used.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.2" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using Microsoft.Extensions.Configuration;

// <DictionaryWithCollectionValues>
IConfiguration config = new ConfigurationBuilder()
.AddInMemoryCollection()
.Build();

config["Queue:0"] = "Value1";
var dict = new Dictionary<string, string[]>() { { "Queue", new[] { "InitialValue" } } };

Console.WriteLine("=== Dictionary Binding with Collection Values ===");
Console.WriteLine($"Initially: {string.Join(", ", dict["Queue"])}");

// In .NET 7+, binding extends the collection instead of replacing it.
config.Bind(dict);
Console.WriteLine($"After Bind: {string.Join(", ", dict["Queue"])}");

config["Queue:1"] = "Value2";
config.Bind(dict);
Console.WriteLine($"After 2nd Bind: {string.Join(", ", dict["Queue"])}");
// </DictionaryWithCollectionValues>

Console.WriteLine();

// <IReadOnlyCollections>
Console.WriteLine("=== IReadOnly* Types (NOT Directly Supported) ===");

var readonlyConfig = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["Settings:Values:0"] = "Item1",
["Settings:Values:1"] = "Item2",
["Settings:Values:2"] = "Item3",
})
.Build();

// This class uses List<string> for binding, exposes as IReadOnlyList<string>.
var settings = new SettingsWithReadOnly();
readonlyConfig.GetSection("Settings").Bind(settings);

Console.WriteLine("Values bound to mutable List, exposed as IReadOnlyList:");
foreach (var value in settings.ValuesReadOnly)
{
Console.WriteLine($" {value}");
}
// </IReadOnlyCollections>

Console.WriteLine();

// <ParameterizedConstructor>
Console.WriteLine("=== Parameterized Constructor Binding ===");

var ctorConfig = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
["AppSettings:Name"] = "MyApp",
["AppSettings:MaxConnections"] = "100",
["AppSettings:Timeout"] = "30"
})
.Build();

// Binding to a type with a single parameterized constructor
var appSettings = ctorConfig.GetSection("AppSettings").Get<AppSettings>();
if (appSettings != null)
{
Console.WriteLine($"Name: {appSettings.Name}");
Console.WriteLine($"MaxConnections: {appSettings.MaxConnections}");
Console.WriteLine($"Timeout: {appSettings.Timeout}");
}
// </ParameterizedConstructor>

// <SettingsWithReadOnly>
class SettingsWithReadOnly
{
// Use mutable type for binding
public List<string> Values { get; set; } = [];

// Expose as read-only for consumers
public IReadOnlyList<string> ValuesReadOnly => Values;
}
// </SettingsWithReadOnly>

// <AppSettings>
// Immutable type with single parameterized constructor.
class AppSettings
{
public string Name { get; }
public int MaxConnections { get; }
public int Timeout { get; }

public AppSettings(string name, int maxConnections, int timeout)
{
Name = name;
MaxConnections = maxConnections;
Timeout = timeout;
}
}
// </AppSettings>