Au-delà de appsettings.json — guide pratique de la gestion de configuration dans les applications métier Windows (environnements, secrets et emplacements d'écriture)

· · C#, .NET, appsettings.json, IConfiguration, IOptions, Generic Host, Gestion de configuration, Développement Windows, Conseil technique

« La chaîne de connexion doit changer selon l’environnement. » « Je veux conserver les préférences d’affichage de chaque utilisateur. » « La clé API a fini directement dans appsettings.json. » Après avoir déposé un appsettings.json et l’avoir lu via IConfiguration, les applications métier rencontrent vite les mêmes problèmes : réglages par environnement, réglages modifiables, secrets et modifications pendant l’exécution.

Cet article rassemble IConfiguration, la superposition de fournisseurs, les options typées, les emplacements d’écriture, les secrets, les limites du rechargement et la migration de app.config/Settings.settings.

1. L’essentiel d’abord

La gestion de configuration commence par choisir un emplacement pour chaque nature de réglage.

Nature du réglage Exemple Emplacement de premier choix Pourquoi
Valeurs par défaut de l’application Niveau de journal par défaut, paramètres UI appsettings.json Livré avec le build et indépendant de l’environnement
Réglages par environnement Chaînes de connexion et API différentes entre staging et production appsettings.{Environment}.json + variables d’environnement Utilise l’ordre d’écrasement standard
Réglages par utilisateur Dernier dossier ouvert, position de fenêtre, préférences d’affichage Fichier propre sous %LOCALAPPDATA% (ou %APPDATA%) Chaque utilisateur doit pouvoir l’écrire
Réglages par machine, partagés Port COM d’un appareil, adresse du serveur de licences Fichier propre sous %ProgramData% Défini une fois par administrateur, partagé entre utilisateurs
Secrets Mots de passe de chaîne de connexion, clés API, jetons Fichier protégé par DPAPI en production, user-secrets en développement Jamais en clair ; le mécanisme de développement ne sert pas en production
Valeurs modifiables à l’exécution Feature flags, niveau de journal dynamique appsettings.json (reloadOnChange) + IOptionsMonitor À réserver aux valeurs réellement applicables sans redémarrage
  • Une seule règle de priorité : le fournisseur ajouté le plus tard gagne. Par défaut : appsettings.jsonappsettings.{Environment}.json → user secrets en développement → variables d’environnement → arguments. Une même clé lue plus tard écrase la précédente. 1
  • Host.CreateApplicationBuilder établit cette superposition par défaut. Même WinForms/WPF en bénéficie avec le Generic Host. 23
  • Basculez l’environnement avec DOTNET_ENVIRONMENT ou ASPNETCORE_ENVIRONMENT. Avec WebApplication, DOTNET_ENVIRONMENT gagne ; sans les deux, l’environnement est Production. Définissez comment les applications desktop et services Windows reçoivent cette variable. 4
  • Recevez les réglages avec la famille IOptions<T>, pas comme DTO lu à la main. IOptions<T> calcule une fois au démarrage ; IOptionsMonitor<T> reflète les changements sans redémarrage. 5
  • Échouez au démarrage, pas pendant la nuit en production. ValidateDataAnnotations() combiné à ValidateOnStart() révèle la mauvaise configuration dès le lancement. 6
  • Ne stockez jamais les secrets en clair dans appsettings.json. Utilisez user-secrets en développement et DPAPI (ProtectedData) ou Credential Manager en production. User-secrets n’est pas chiffré et est réservé au développement. 78

2. Fondation du système .NET — IConfiguration et fournisseurs

IConfiguration présente comme un magasin clé-valeur unique plusieurs fournisseurs de configuration : JSON, variables d’environnement, arguments de ligne de commande, INI, XML, collections mémoire, etc. 9 Lorsque plusieurs fournisseurs définissent la même clé, le dernier ajouté gagne. 1

Host.CreateApplicationBuilder(args) charge par défaut, de la priorité la plus faible à la plus forte : appsettings.json, appsettings.{Environment}.json, Secret Manager en développement, variables d’environnement et arguments de ligne de commande. Voici le minimum.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// appsettings.json / appsettings.{Environment}.json / environment variables /
// command-line arguments are already loaded by default, in the priority order shown above
string? connectionString = builder.Configuration.GetConnectionString("Main");

using IHost host = builder.Build();
await host.RunAsync();

WinForms peut employer le même builder après ajout de Microsoft.Extensions.Hosting. Generic Host réunit configuration, DI et journalisation ; voir What Is Generic Host? et Using Generic Host + BackgroundService in a Desktop App.

Les réglages d’hôte, nom d’environnement ou content root, lisent le préfixe DOTNET_, mais les réglages ordinaires lus via IConfiguration ne l’utilisent pas. Pour un préfixe propre, ajoutez-le explicitement. 10

using Microsoft.Extensions.Configuration;

// Added after the default providers, so it has the highest priority
builder.Configuration.AddEnvironmentVariables(prefix: "MyApp_");

3. Réglages par environnement — appsettings.{Environment}.json

Pour des chaînes de connexion ou endpoints différents, utilisez appsettings.Development.json, appsettings.Staging.json et appsettings.Production.json. Ce sont des fichiers de différences qui écrasent la base : n’écrivez que les valeurs qui changent. 1

Le nom d’environnement vient de DOTNET_ENVIRONMENT ou ASPNETCORE_ENVIRONMENT. Sous Windows les noms de variables sont insensibles à la casse, sous Linux ils y sont sensibles : gardez une casse cohérente si des conteneurs sont envisagés. 4

Pour une application desktop ou un service Windows, le transport de la variable est lui-même une décision :

  • variable machine, par exemple setx DOTNET_ENVIRONMENT Production /M, héritée par tous les processus ;
  • variable fournie au lancement du service, car un service n’hérite pas les variables utilisateur de la session interactive ; voir How to Build and Operate Windows Services ;
  • argument de ligne de commande, souvent plus fiable dans le Planificateur de tâches, par exemple --environment Production, voir Task Scheduler Tasks Not Running, or Ending With 0x1.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

var options = new HostApplicationBuilderSettings
{
    Args = args,
    // For execution paths where the environment variable isn't passed through (Task Scheduler, etc.),
    // also allow switching the environment via command-line arguments
};
HostApplicationBuilder builder = Host.CreateApplicationBuilder(options);

Console.WriteLine($"Current environment: {builder.Environment.EnvironmentName}");

4. Options Pattern — recevoir les réglages comme types

L’accès par clés IConfiguration["Key:SubKey"] ne détecte pas les fautes de frappe et devient illisible avec une arborescence profonde. Le modèle pratique est de lier une section à un POCO et de recevoir un type.

public sealed class ExternalApiOptions
{
    public const string SectionName = "ExternalApi";

    public required string BaseUrl { get; set; }
    public required string ApiKey { get; set; }
    public int TimeoutSeconds { get; set; } = 30;
}
using Microsoft.Extensions.DependencyInjection;

builder.Services
    .AddOptions<ExternalApiOptions>()
    .Bind(builder.Configuration.GetSection(ExternalApiOptions.SectionName));
Interface Durée d’enregistrement Répercute les changements Usage principal
IOptions<T> Singleton Non, calculé une fois au démarrage Valeurs supposées immuables, option la plus simple
IOptionsSnapshot<T> Scoped Recalculé à chaque reconstruction de scope Contextes à scope net, par exemple requête web
IOptionsMonitor<T> Singleton Toujours la dernière valeur et notifications OnChange Services résidents devant détecter les changements

Sans scope HTTP, dans une application desktop ou un service Windows, IOptionsSnapshot<T> se comporte comme IOptions<T> à moins de créer soi-même le scope. Pour détecter les changements, choisissez directement IOptionsMonitor<T>.

Lisez BaseUrl, ApiKey et timeout via IOptionsMonitor à chaque appel, mais obtenez HttpClient via IHttpClientFactory afin de réutiliser le pool de connexions. Créer et disposer un HttpClient à chaque appel reconstruit les sockets et peut épuiser les ports éphémères.

using Microsoft.Extensions.Options;

public sealed class ExternalApiClient(
    IHttpClientFactory httpClientFactory,
    IOptionsMonitor<ExternalApiOptions> optionsMonitor)
{
    public async Task<string> FetchAsync(CancellationToken cancellationToken)
    {
        // Get the latest value on every call. Reflects updates if the settings file has changed
        ExternalApiOptions current = optionsMonitor.CurrentValue;

        // Get HttpClient itself via the factory, reusing the internal connection pool
        HttpClient client = httpClientFactory.CreateClient(nameof(ExternalApiClient));

        using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(new Uri(current.BaseUrl), "status"));
        request.Headers.Add("X-Api-Key", current.ApiKey);

        using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(current.TimeoutSeconds));
        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);

        using HttpResponseMessage response = await client.SendAsync(request, linkedCts.Token);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync(cancellationToken);
    }
}

Enregistrez le client nommé avec builder.Services.AddHttpClient(nameof(ExternalApiClient));. Les valeurs de requête peuvent changer, tandis que la réutilisation des connexions reste à la fabrique.

La validation avec DataAnnotations et ValidateOnStart() fait en sorte qu’une application à configuration invalide ne démarre pas. 6

using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;

public sealed class ExternalApiOptions
{
    public const string SectionName = "ExternalApi";

    [Required, Url]
    public required string BaseUrl { get; set; }

    [Required, MinLength(16)]
    public required string ApiKey { get; set; }

    [Range(1, 300)]
    public int TimeoutSeconds { get; set; } = 30;
}

builder.Services
    .AddOptions<ExternalApiOptions>()
    .Bind(builder.Configuration.GetSection(ExternalApiOptions.SectionName))
    .ValidateDataAnnotations()
    .ValidateOnStart(); // Validation runs at host startup (during StartAsync/RunAsync), before hosted services start

Sans ValidateOnStart(), la validation est différée au premier accès. Elle s’exécute à StartAsync/RunAsync, non juste après Build() : les tests qui n’appellent que Build() ne la déclenchent pas. Faites de ValidateOnStart() une règle pour éviter l’échec « l’application démarre mais plante à l’ouverture de l’écran utilisant le réglage ». 6

5. Où placer les réglages modifiables ?

appsettings.json contient des valeurs par défaut en lecture seule : ce n’est pas le fichier que l’application doit réécrire. Une installation sous Program Files interdit l’écriture aux utilisateurs standard et produit une exception ou une virtualisation divergent entre utilisateurs.

Séparez les réglages modifiables selon leur nature et ancrez-les aux dossiers spéciaux de Environment.GetFolderPath. 11

Emplacement Obtention Utilisation
%LOCALAPPDATA%\CompanyName\AppName Environment.SpecialFolder.LocalApplicationData Valeurs et données par utilisateur par défaut
%APPDATA%\CompanyName\AppName (Roaming) Environment.SpecialFolder.ApplicationData Uniquement les réglages qui suivent l’utilisateur dans un profil itinérant
%ProgramData%\CompanyName\AppName Environment.SpecialFolder.CommonApplicationData Réglages par machine partagés, nécessite une conception ACL
using System;
using System.IO;
using System.Text.Json;

public sealed class UserSettingsStore
{
    private readonly string _filePath;

    public UserSettingsStore()
    {
        string root = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
        string dir = Path.Combine(root, "KomuraSoft", "MyApp");
        Directory.CreateDirectory(dir);
        _filePath = Path.Combine(dir, "user-settings.json");
    }

    public UserSettings Load()
    {
        if (!File.Exists(_filePath))
        {
            return new UserSettings();
        }
        string json = File.ReadAllText(_filePath);
        return JsonSerializer.Deserialize<UserSettings>(json) ?? new UserSettings();
    }

    public void Save(UserSettings settings)
    {
        string json = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
        // Assumes the caller serializes concurrent writes within the same process
        File.WriteAllText(_filePath, json);
    }
}

public sealed class UserSettings
{
    public string? LastOpenedFolder { get; set; }
    public int WindowWidth { get; set; } = 1024;
    public int WindowHeight { get; set; } = 768;
}

Mélanger réglages utilisateur et machine dans un fichier produit des accidents tels que « le réglage de l’utilisateur A gêne B » sur une machine partagée. Voir How Windows User Profiles Work et Choosing a Storage Location for Windows App Data.

6. Secrets — chaînes de connexion et clés API

Mettre un mot de passe de chaîne de connexion ou une clé API directement dans appsettings.json est risqué, même si le fichier est dans .gitignore : il peut se retrouver sur un partage, dans une demande de support ou dans une sauvegarde ancienne.

En développement, utilisez Secret Manager (dotnet user-secrets). Les valeurs sont du JSON en clair dans %APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.json ; ce n’est pas un magasin de confiance et il est réservé au développement. 7

dotnet user-secrets init
dotnet user-secrets set "ExternalApi:ApiKey" "dev-only-value"

En production, employez DPAPI (Data Protection API) de Windows afin de chiffrer une valeur liée aux informations d’identification utilisateur ou machine. ProtectedData.Protect/Unprotect sont les wrappers ; DataProtectionScope.CurrentUser ne permet le déchiffrement que sur la même machine et avec le même compte. DPAPI est propre à Windows et lève PlatformNotSupportedException ailleurs. 8

using System.Security.Cryptography;
using System.Text;

public static class SecretProtector
{
    // Mixing in entropy that indicates the purpose makes it harder to confuse with data protected for other purposes
    private static readonly byte[] Entropy = Encoding.UTF8.GetBytes("MyApp.ExternalApi.ApiKey");

    public static string Protect(string plainText)
    {
        byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
        byte[] protectedBytes = ProtectedData.Protect(plainBytes, Entropy, DataProtectionScope.CurrentUser);
        return Convert.ToBase64String(protectedBytes);
    }

    public static string Unprotect(string protectedBase64)
    {
        byte[] protectedBytes = Convert.FromBase64String(protectedBase64);
        byte[] plainBytes = ProtectedData.Unprotect(protectedBytes, Entropy, DataProtectionScope.CurrentUser);
        return Encoding.UTF8.GetString(plainBytes);
    }
}

Les choix de stockage, de scope CurrentUser/LocalMachine et les pièges des applications partagées sont expliqués dans Storing Sensitive Data in Windows Apps — Avoiding Plaintext Settings with DPAPI. La ligne de partage est simple : protégez toute valeur dont la fuite impose de réémettre un mot de passe ou une clé, d’enquêter sur un accès non autorisé ou de déclarer l’incident. Séparez informations de serveur et informations d’identification afin de ne protéger par DPAPI que la partie réellement secrète.

7. Modifier la configuration pendant l’exécution — réalité de reloadOnChange

L’appel AddJsonFile par défaut de Host.CreateApplicationBuilder définit reloadOnChange: true : appsettings.json et appsettings.{Environment}.json sont rechargés lors de changements, sous le contrôle de PhysicalFileProvider et FileSystemWatcher. 12

  • Seules les valeurs lues par IOptionsMonitor<T> et IOptionsSnapshot<T> sont mises à jour. IOptions<T> garde la valeur de démarrage.
  • FileSystemWatcher n’est pas toujours fiable dans les conteneurs Docker ou partages réseau. DOTNET_USE_POLLING_FILE_WATCHER=true active la surveillance par interrogation de 4 secondes, sans intervalle configurable. 13 Voir aussi A Practical Guide to FileSystemWatcher.
  • Une modification peut déclencher plusieurs notifications. Débouncez ou comparez un hash si un changement redémarre une opération coûteuse.

N’essayez pas toujours d’appliquer un changement sans redémarrage. Les flags et niveaux de journal légers se prêtent à IOptionsMonitor; une chaîne de connexion ou une taille de pool en conflit avec des ressources déjà actives doit être explicitement documentée comme « appliquée au redémarrage ». La clarté opérationnelle est plus importante que la présence de reloadOnChange.

8. Carte de migration depuis app.config / Settings.settings

.NET Framework .NET Notes
<appSettings> dans App.config / Web.config appsettings.json + IConfiguration La hiérarchie s’exprime naturellement avec l’imbrication JSON
ConfigurationManager.AppSettings["Key"] builder.Configuration["Key"] ou IOptions<T> Accès par chaîne remplacé par accès typé
ConfigurationManager.ConnectionStrings builder.Configuration.GetConnectionString("Name") La convention ConnectionStrings est conservée
Settings.settings (scope utilisateur) Fichier JSON propre sous %LOCALAPPDATA% Pas d’équivalent généré de ApplicationSettingsBase ; sérialisez/sauvegardez vous-même
Settings.settings (scope application) appsettings.json Valeurs par défaut en lecture seule
Chiffrement de <connectionStrings> (aspnet_regiis, etc.) DPAPI (ProtectedData) Le mécanisme de chiffrement change entièrement et doit être reconstruit

Deux pièges sont courants. Ajouter le package System.Configuration.ConfigurationManager permet de conserver temporairement la lecture de App.config, mais ne doit pas devenir l’état final : planifiez la migration vers appsettings.json. 14 De même, .NET ne fournit pas le mécanisme généré de Properties.Settings.Default.Save() pour les réglages utilisateur ; préparez une classe de stockage comme au chapitre 5. Pour le reste de l’inventaire de migration, voir The .NET Framework → .NET Pre-Migration Checklist.

Résumé

La configuration .NET associe fournisseurs superposés via IConfiguration, valeurs typées via la famille IOptions et environnement via les variables d’environnement. Les difficultés propres aux applications métier Windows surviennent ensuite : emplacement des réglages modifiables, protection des secrets, limite des changements à l’exécution et retrait des actifs app.config.

Allez au-delà de « tout est dans appsettings.json » et séparez emplacement et traitement selon la nature du réglage. L’inventaire réel des fichiers et du déploiement est souvent nécessaire pour choisir une stratégie de migration ; contactez-nous en cas de doute.

Articles connexes

Domaines de conseil associés

KomuraSoft LLC assure le conseil technique sur la conception de configuration des applications métier Windows, l’exploitation des réglages par environnement et la stratégie de migration des actifs app.config existants.

Références

  1. Microsoft Learn, Configuration in .NET - Alternative hosting approach. Ordre de priorité des fournisseurs construits par défaut par Host.CreateApplicationBuilder 2 3

  2. Microsoft Learn, .NET Generic Host - Host builder settings. Ordre par défaut de configuration d’hôte et d’application chargé par Host.CreateApplicationBuilder

  3. Microsoft Learn, Use the .NET Generic Host in a Windows Forms app. Intégration de Generic Host dans Windows Forms pour DI, configuration et journalisation. 

  4. Microsoft Learn, ASP.NET Core runtime environments - Environment variables that determine the runtime environment. Priorité DOTNET_ENVIRONMENT/ASPNETCORE_ENVIRONMENT, défaut Production et différence de casse Windows/Linux.  2

  5. Microsoft Learn, Options pattern in .NET - Options interfaces. Durée de vie et réflexion des changements dans IOptions<TOptions>, IOptionsSnapshot<TOptions> et IOptionsMonitor<TOptions>

  6. Microsoft Learn, Options pattern in .NET - Options validation. Validation DataAnnotations et validation au démarrage avec ValidateOnStart() 2 3

  7. Microsoft Learn, Safe storage of app secrets in development in ASP.NET Core - Use the Secret Manager tool. user-secrets est du JSON en clair et réservé au développement.  2

  8. Microsoft Learn, ProtectedData Class. Protect/Unprotect, scopes CurrentUser/LocalMachine et limite Windows-only.  2

  9. Microsoft Learn, Configuration providers in .NET. Fournisseurs JSON, environnement, arguments, INI, XML et autres unifiés par IConfiguration

  10. Microsoft Learn, Configuration providers in .NET - Environment variable configuration provider. Variables DOTNET_, variables de réglages et préfixe personnalisé. 

  11. Microsoft Learn, Environment.GetFolderPath Method. Obtention des dossiers spéciaux par Environment.SpecialFolder et GetFolderPath

  12. Microsoft Learn, Detect changes with change tokens in ASP.NET Core - Monitor for configuration changes. reloadOnChange et PhysicalFileProvider utilisant FileSystemWatcher

  13. Microsoft Learn, Options pattern in .NET - IOptionsMonitor. Limites des notifications et DOTNET_USE_POLLING_FILE_WATCHER à 4 secondes pour conteneurs/partages. 

  14. Microsoft Learn, Modernize after upgrading to .NET from .NET Framework - App.config. Migration App.config vers appsettings.json et compatibilité transitoire ConfigurationManager. 

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ces pages replacent le sujet dans un contexte plus large de services et de décisions.

Cet article est directement lié aux services suivants.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Peut-on placer appsettings.json dans le même dossier que l'exécutable ?
Oui pour des valeurs par défaut en lecture seule. Mais si le dossier est installé sous Program Files, l'application ne peut pas réécrire appsettings.json : les utilisateurs standard n'y ont pas droit d'écriture et vous obtenez une exception ou une virtualisation du système de fichiers différente par utilisateur. Placez les réglages modifiables dans un fichier séparé sous %LOCALAPPDATA% ou %ProgramData%.
Les variables d'environnement ou appsettings.json doivent-ils être prioritaires ?
Ne changez pas l'ordre par défaut : appsettings.json → appsettings.{Environment}.json → variables d'environnement → arguments de ligne de commande. Les valeurs par défaut expédiables dans la sortie de build vont dans appsettings.json ; les valeurs qui varient selon le déploiement ou le conteneur vont dans les variables d'environnement. Cette séparation évite les ambiguïtés.
Comment découper les classes de réglages ?
Découpez les options par fonction ou responsabilité : ConnectionOptions pour les chaînes de connexion, ExternalApiOptions pour une API externe, etc., plutôt que de mélanger des réglages sans rapport. La validation et le rechargement par IOptionsSnapshot/IOptionsMonitor se séparent alors naturellement, et la validation DataAnnotations reste contenue dans chaque classe.
Faut-il migrer les fichiers INI ou le Registre ?
Pour reconstruire la gestion de configuration, standardisez sur appsettings.json + IConfiguration. .NET fournit un fournisseur INI, ce qui permet de lire les INI existants pendant une migration progressive. Le Registre ne fait pas partie des fournisseurs standard ; pour des actifs qui en dépendent, créez une passerelle en lecture seule et transférez progressivement les valeurs vers appsettings.json.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.

Retour au blog