Aller au contenu

Fondamentaux • Configuration

Pourquoi faire ?

Une application nécessite souvent d’utiliser des données paramétrables : chaîne de connexion, URL d’API, durée de timeout, niveau des logs, etc.

Au lieu d’utiliser des valeurs en dur dans le code, il peut être préférable de placer ces données dans un fichier de configuration. Il s’agit d’un fichier texte (contrairement à un fichier binaire), facilement éditable. Il n’est donc pas nécessaire de recompiler l’application, car ce fichier sera lu à son démarrage.

Fichiers de paramétrages

Fichier appsettings.json

Dans une application .NET, la manière la plus simple de stocker les données de configuration est d’utiliser le fichier appsettings.json. Il contient un ensemble de clef-valeur, stocké au format JSON. Chaque clef doit être unique. Chaque valeur peut être de type :

  • string : une chaine de caractères
  • boolean : un booléen (true, false)
  • number : un nombre entier ou décimal
  • array : un tableau de valeurs de même type
  • object : un nouvel ensemble de clef-valeur

Voici un exemple de fichier de configuration :

appsettings.json
{
"StringProperty": "StringValue",
"BooleanProperty" : true,
"NumberProperty": 3.1415,
"ArrayOfStringProperty": [ "red", "green", "blue" ],
"ObjectProperty": {
"SubProperty1": "https://my-remote-api.url",
"SubProperty2": [ 1, 2, 3 ],
"SubProperty3": false
}
}

Fichiers appsettings.json par environnement

Le fichier peut également être décliné selon l’environnement .NET :

  • appsettings.Development.json
  • appsettings.Production.json
  • appsettings.[ENV].json[ENV] correspond à n’importe quel environnement selon les besoins

Il est possible d’utiliser un fichier appsettings.json contenant des données communes et plusieurs fichiers appsettings.[ENV].json par environnement. Au démarrage de l’application dans un environnement .NET spécifique, les données présentes dans les deux fichiers sont fusionnées. La priorité est donnée aux valeurs du fichier appsettings.[ENV].json en cas de doublon.

Voici un exemple de fichier de configuration pour l’environnement Development :

appsettings.Development.json
{
"Environment": "Development",
"StringProperty": "StringValueForDevelopment",
"BooleanProperty" : false
}

Les données fusionnées seront :

appsettings.json (fusionné)
{
"Environment": "Development",
"StringProperty": "StringValue",
"StringProperty": "StringValueForDevelopment",
"BooleanProperty": true,
"BooleanProperty" : false,
"NumberProperty": 3.1415,
"ArrayOfStringProperty": [
"red",
"green",
"blue"
],
"ObjectProperty": {
"SubProperty1": "https://my-remote-api.url",
"SubProperty2": [ 1, 2, 3 ],
"SubProperty3": false
}
}

Mise en place

Commençons par créer un nouveau projet console. Il nous servira de base de travail.

Terminal window
dotnet new console --name=ReadConfiguration

Ajoutons les paquets Nuget Microsoft.Extensions.Hosting et Microsoft.Extensions.Configuration.Binder

Terminal window
cd ReadConfiguration
dotnet add package Microsoft.Extensions.Hosting
dotnet add package Microsoft.Extensions.Configuration.Binder

Nous pouvons maintenant ouvrir le projet ReadConfiguration dans notre IDE favori.

Ajoutons le fichier appsettings.json, sans oublier de définir l’action de build à Content dans ses propriétés : Définir l'action de build à Content dans les propriétés du fichier

L’arborescence du projet doit ressembler à ça :

  • ReadConfiguration.csproj
    • Program.cs
    • appsettings.json

Le code suivant nous sert de base de travail :

Program.cs
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
// Création du générateur d'hôte d'application
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Génération de l'hôte d'application
IHost host = builder.Build();

Ajoutons la configuration suivante :

appsettings.json
{
"ApplicationName": "MyApplication",
"AllowSimultaneousConnection": true,
"MaxSimultaneousConnection": 2,
"UsersAllowedToBypassConnectionLimitation": [
"admin",
"root",
"super"
],
"ApiConfiguration": {
"Url": "https://my-remote-api.url",
"Headers": {
"AuthKey": "X-Auth-Key",
"AuthValue": "a5f4r2fqW553bnXnAfeZt64g10B",
"ContentType": "application/json"
},
"TimeoutsInMilliseconds": [
1000,
2000,
5000,
10000
],
"DelayBetweenRetriesInSeconds": 1.5
},
"ConnectionStrings": {
"MainDatabase": "data source = MyDatabaseServer; initial catalog = MyCatalog; integrated security = true;"
}
}

IConfiguration

L’interface Microsoft.Extensions.Configuration.IConfiguration représente l’ensemble des propriétés de configuration sous forme de clef-valeur. Cela inclus :

  • les données du fichier appsettings.json (s’il existe)
  • les données du fichier appsettings.[ENV].json (s’il existe) correspondant à l’environnement .NET courant
  • les variables d’environnement
  • les arguments passés à la ligne de commande
  • etc.

L'interface IConfiguration regroupe toutes les sources de configuration en une seule représentation

Ajoutons cette ligne dans le code de Program.cs pour charger la configuration :

Program.cs
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
// Création du générateur d'hôte d'application
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Génération de l'hôte d'application
IHost host = builder.Build();
// Récupération du service IConfiguration depuis l'injection de dépendances
IConfiguration? configuration = host.Services.GetService<IConfiguration>();

Choisir la clef

Exemples de clef utilisées pour lire une valeur de configuration

Pour accéder à la valeur d’une clef située à la racine de la configuration, il suffit d’utiliser la valeur de la clef.

ClefValeur
”ApplicationName""MyApplication"
"AllowSimultaneousConnection”true
”MaxSimultaneousConnection”2

Pour accéder à une valeur imbriquée, chaque clef est séparée par :.

ClefValeur
”ApiConfiguration:Url""https://my-remote-api.url"
"ApiConfiguration:Headers:AuthKey""X-Auth-Key"
"ApiConfiguration:Headers:AuthValue""a5f4r2fqW553bnXnAfeZt64g10B"
"ApiConfiguration:DelayBetweenRetriesInSeconds”1.5

Pour accéder à l’entrée d’un tableau, la clef est suivi de l’index de l’entrée, séparés par :.

ClefValeur
”UsersAllowedToBypassConnectionLimitation:0""admin"
"UsersAllowedToBypassConnectionLimitation:1""root"
"ApiConfiguration:TimeoutsInMilliseconds:1”2000
”ApiConfiguration:TimeoutsInMilliseconds:2”5000

Lire une valeur via l’indexeur

Il est possible de lire une valeur de configuration via l’indexeur, en utilisant [string key]. Le type retourné est alors string?. Si la clef n’existe pas alors l’indexeur retourne null.

Program.cs
// ...
// Récupération du service IConfiguration depuis l'injection de dépendances
IConfiguration? configuration = host.Services.GetService<IConfiguration>();
// → applicationName = "MyApplication"
string? applicationName = configuration["ApplicationName"];
// → unknownKeyValue = `null`
string? unknownKeyValue = configuration["RandomKey"];

Lire une valeur via GetValue

La méthode d’extension GetValue<T>(string key) (assembly Microsoft.Extensions.Configuration.Binder) permet d’obtenir la valeur de configuration en spécifiant son type. Si la clef n’existe pas alors la méthode retourne null.

Program.cs
// ...
// Récupération du service IConfiguration depuis l'injection de dépendances
IConfiguration? configuration = host.Services.GetService<IConfiguration>();
// → maxSimultaneousConnection = 2 et son type est `int?`
int? maxSimultaneousConnection = configuration.GetValue<int>("MaxSimultaneousConnection");
// → delayBetweenRetriesInSeconds = 1.5 et son type est `decimal?`
decimal? delayBetweenRetriesInSeconds = configuration.GetValue<decimal>("ApiConfiguration:DelayBetweenRetriesInSeconds");

Lire une chaine de connexion via GetConnectionString

La méthode d’extension GetConnectionString(string name) permet de récupérer la valeur d’une chaine de connexion. C’est un raccourci de GetSection("ConnectionStrings")[name].

Program.cs
// ...
// Récupération du service IConfiguration depuis l'injection de dépendances
IConfiguration? configuration = host.Services.GetService<IConfiguration>();
// → connectionString = "data source = MyDatabaseServer; initial catalog = MyCatalog; integrated security = true;"
string? connectionString = configuration.GetConnectionString("MainDatabase");

Lire une section via GetSection

La méthode d’extension GetSection(string key) permet de charger une section de configuration. Si la clef n’existe pas alors la méthode retourne null.

Program.cs
// ...
// Récupération du service IConfiguration depuis l'injection de dépendances
IConfiguration? configuration = host.Services.GetService<IConfiguration>();
// Chargement de la section "ApiConfiguration"
IConfigurationSection? apiConfigurationSection = configuration.GetSection("ApiConfiguration");

Nous pouvons lire les valeurs de la section en utilisant l’indexeur [string key] ou la méthode GetValue<T>(string key).

Program.cs
// ...
// Récupération du service IConfiguration depuis l'injection de dépendances
IConfiguration? configuration = host.Services.GetService<IConfiguration>();
// Chargement de la section "ApiConfiguration"
IConfigurationSection? apiConfigurationSection = configuration.GetSection("ApiConfiguration");
// → apiConfigurationHeaderAuthValue = "a5f4r2fqW553bnXnAfeZt64g10B"
string? apiConfigurationHeaderAuthValue = apiConfigurationSection["HeaderAuthValue"];
// → delayBetweenRetriesInSeconds = 1.5
decimal? delayBetweenRetriesInSeconds = configuration.GetValue<decimal>("DelayBetweenRetriesInSeconds");

Un tableau est aussi considéré comme une section. Nous pouvons lire la valeur d’une entrée du tableau via l’indexeur [string key] :

Program.cs
// ...
// Récupération du service IConfiguration depuis l'injection de dépendances
IConfiguration? configuration = host.Services.GetService<IConfiguration>();
// Chargement de la section "UsersAllowedToBypassConnectionLimitation"
IConfigurationSection? usersAllowedToBypassConnectionLimitationSection = configuration.GetSection("UsersAllowedToBypassConnectionLimitation");
// → thirdUserAllowedToBypassConnectionLimitationSection = "super"
string? thirdUserAllowedToBypassConnectionLimitationSection = usersAllowedToBypassConnectionLimitationSection["2"];

Typer une section via Get

Une section peut être typée en utilisant la méthode d’extension Get<T>() (assembly Microsoft.Extensions.Configuration.Binder).

Nous pouvons typer une section de type tableau.

Program.cs
// ...
// Récupération du service IConfiguration depuis l'injection de dépendances
IConfiguration? configuration = host.Services.GetService<IConfiguration>();
// Chargement de la section "UsersAllowedToBypassConnectionLimitation"
IConfigurationSection? usersAllowedToBypassConnectionLimitationSection = configuration.GetSection("UsersAllowedToBypassConnectionLimitation");
// → usersAllowedToBypassConnectionLimitation = [ "admin", "root", "super" ] et son type est `IList<string>`
IList<string>? usersAllowedToBypassConnectionLimitation = usersAllowedToBypassConnectionLimitationSection?.Get<IList<string>>();

Il est aussi possible de typer une section de type objet.

Déclarons deux record représentant le format des données de la section “ApiConfiguration” :

ApiHeader.cs
internal record ApiHeaders
{
public string? AuthKey { get; init; }
public string? AuthValue { get; init; }
public string? ContentType { get; init; }
}
ApiConfiguration.cs
internal record ApiConfiguration
{
public string? Url { get; init; }
public ApiHeaders? Headers { get; init; }
public IList<int>? TimeoutsInMilliseconds { get; init; }
public decimal? DelayBetweenRetriesInSeconds { get; init; }
}

Nous pouvons charger les données de la section “ApiConfiguration” dans une variable bien typée :

Program.cs
// ...
// Récupération du service IConfiguration depuis l'injection de dépendances
IConfiguration? configuration = host.Services.GetService<IConfiguration>();
// Chargement de la section "ApiConfiguration"
IConfigurationSection apiConfigurationSection = configuration.GetRequiredSection("ApiConfiguration");
// → apiConfiguration = { Url = "https://my-remote-api.url", Headers = { AuthKey = "X-Auth-Key", AuthValue = "a5f4r2fqW553bnXnAfeZt64g10B", ContentType = "application/json" }, TimeoutsInMilliseconds = [ 1000, 2000, 5000, 10000], DelayBetweenRetriesInSeconds = 1.5 }
ApiConfiguration? apiConfiguration = apiConfigurationSection.Get<ApiConfiguration>();

IOptions

Le modèle de conception (design pattern en anglais) Options permet d’associer un ensemble de paramètres de configuration à un objet typé. Il est alors possible d’extraire les valeurs d’une section dans un record afin de l’utiliser dans une partie spécifique de l’application.

Reprenons le code de Program.cs afin de configurer un lien entre le record ApiConfiguration et la section du même nom :

Program.cs
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
// Création du générateur d'hôte d'application
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Configuration du lien entre le record ApiConfiguration et la section de configuration "ApiConfiguration"
builder.Services.Configure<ApiConfiguration>(builder.Configuration.GetRequiredSection("ApiConfiguration")));
// Génération de l'hôte d'application
IHost host = builder.Build();
// ...

Une fois le lien configuré, nous pouvons récupérer les données associées à ApiConfiguration dans n’importe quelle partie de l’application, via l’injection de dépendances :

Program.cs
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
// Création du générateur d'hôte d'application
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Configuration du lien entre le record ApiConfiguration et la section de configuration "ApiConfiguration"
builder.Services.Configure<ApiConfiguration>(builder.Configuration.GetRequiredSection("ApiConfiguration")));
// Génération de l'hôte d'application
IHost host = builder.Build();
// ...
// → apiConfigurationOptions = { Url = "https://my-remote-api.url", Headers = { AuthKey = "X-Auth-Key", AuthValue = "a5f4r2fqW553bnXnAfeZt64g10B", ContentType = "application/json" }, TimeoutsInMilliseconds = [ 1000, 2000, 5000, 10000], DelayBetweenRetriesInSeconds = 1.5 }
IOptions<ApiConfiguration> apiConfigurationOptions = host.Services.GetRequiredService<IOptions<ApiConfiguration>>();

Contexte d’exécution

  • RépertoireApplicationProject.csproj [Bibliothèque de classe]
    • LoginUseCase.cs
    • LogoutUseCase.cs
    • RegisterUseCase.cs
  • RépertoireConsoleProject.csproj [Console]
    • appsettings.json
    • Program.cs
  • RépertoireTestProject.csproj [Tests Unitaires]
    • appsettings.json
    • LoginTests.cs
    • LogoutTests.cs
    • RegisterTests.cs

ApplicationProject contient la logique métier. Il a besoin de données de configuration (nombre de connexions échouées avant blocage, durée de session, etc.). Mais ce n’est pas lui qui possède le fichier de configuration. Il pourra néanmoins injecter IConfiguration ou IOptions<T> pour accéder à la configuration.

ConsoleProject et TestProject sont exécutables et référencent ApplicationProject. Chaque projet possède sa propre configuration.

Si nous lançons les tests unitaires alors le contexte d’exécution sera celui de TestProject : c’est bien le fichier appsettings.json de ce projet qui sera chargé et ses valeurs de configuration seront utilisées dans ApplicationProject. Cela permet d’appliquer une configuration spécifique pour les tests.

Évidement cela s’applique de la même façon pour ConsoleProject qui aura son propre contexte d’exécution avec son fichier appsettings.json dédié.

Bonnes pratiques

  • Bien nommer les clefs de configuration afin d’éviter les questions et les confusions.
  • Regrouper sous une section les paramètres qui concernent une partie spécifique de l’application afin de profiter du modèle Options.
  • Ne pas hésiter à placer dans le fichier de configuration des constantes codées en dur si elles sont susceptibles de changer à l’avenir ou de dépendre de l’environnement.

Pour aller plus loin

Nous avons maintenant une vue d’ensemble des techniques proposées par .NET pour construire et lire une configuration depuis le fichier appsettings.json.

Il existe d’autres fournisseurs de configuration disponibles dans .NET : fichier (INI, JSON, XML), variables d’environnement, arguments de ligne de commande, etc.

Il est également possible d’implémenter notre propre fournisseur de configuration personnalisé.

Microsoft propose une documentation complète sur ce sujet (et bien d’autres). N’hésitez pas à la consulter pour approfondir ou éclaircir certains points.