Accueil › L'ossature › Chapitre 12
Configuration, options, logs
Une application doit se comporter différemment sur ton portable et en production, sans qu'une seule ligne de code change. Et quand elle dysfonctionne à 3 h du matin, il faut pouvoir comprendre pourquoi. Deux sujets, un même objectif : ne pas coder en dur, ne pas deviner.
La configuration est un empilement de calques
Des calques posés l'un sur l'autre. Chaque source recouvre partiellement la précédente : le calque du bas donne les valeurs générales, celui du haut les remplace ponctuellement. Le dernier posé gagne.
{
"ConnectionStrings": {
"Defaut": "Server=localhost;Database=Boutique;Trusted_Connection=True"
},
"Smtp": {
"Hote": "localhost",
"Port": 25,
"ExpediteurParDefaut": "no-reply@boutique.fr"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
}
}
}
# variable d'environnement : les niveaux sont séparés par DEUX tirets bas
Smtp__Port=587
ConnectionStrings__Defaut="Server=prod;..."
# argument de ligne de commande : deux-points
dotnet run --Smtp:Port=587
# secret de développement (stocké hors du projet, jamais dans Git)
dotnet user-secrets init
dotnet user-secrets set "Smtp:MotDePasse" "vraiSecret"
Smtp:Port ne fonctionne pas comme variable d'environnement sur tous les systèmes : le
deux-points n'est pas un caractère valide dans un nom de variable sous Linux. La forme portable est
Smtp__Port (deux tirets bas). C'est la cause nº 1 des « ma configuration n'est pas prise en compte
dans le conteneur ».
Les secrets ne vont jamais dans le code
| Contexte | Où mettre le mot de passe |
|---|---|
| Ta machine, en développement | dotnet user-secrets (fichier dans ton profil utilisateur) |
| Intégration continue | Les secrets du dépôt (GitHub Actions secrets, variables protégées) |
| Conteneur / Kubernetes | Variables d'environnement injectées, ou secrets montés en fichiers |
| Cloud | Azure Key Vault, AWS Secrets Manager, avec identité managée (aucun secret à stocker) |
| Jamais | appsettings.json versionné, code source, message de commit, capture d'écran |
Supprimer la ligne dans un commit suivant ne suffit pas : l'historique Git la conserve. La seule réponse
correcte est de révoquer et régénérer le secret. Ajoute appsettings.*.json local et
.env à ton .gitignore, et active la détection de secrets sur ton dépôt.
Le pattern Options : de la configuration typée
public class Envoyeur(IConfiguration cfg)
{
public void Envoyer()
{
var hote = cfg["Smtp:Hote"]; // string, peut être null
var port = int.Parse(cfg["Smtp:Port"]!); // 💥 si absent ou mal écrit
// faute de frappe dans la clé =
// erreur à l'exécution, en production
}
}public class SmtpOptions
{
public const string Section = "Smtp";
[Required] public string Hote { get; init; } = "";
[Range(1, 65535)] public int Port { get; init; } = 25;
public string? MotDePasse { get; init; }
}
public class Envoyeur(IOptions<SmtpOptions> options)
{
private readonly SmtpOptions _o = options.Value;
public void Envoyer() => Connecter(_o.Hote, _o.Port);
}builder.Services.AddOptions<SmtpOptions>()
.Bind(builder.Configuration.GetSection(SmtpOptions.Section))
.ValidateDataAnnotations() // applique [Required], [Range]…
.ValidateOnStart(); // ← échoue AU DÉMARRAGE, pas au premier email
ValidateOnStart() : la ligne qui sauve des soirées
Sans elle, une configuration invalide n'explose qu'au premier usage — potentiellement des heures après le déploiement, sur le premier client qui déclenche un envoi d'email. Avec elle, l'application refuse de démarrer et le message dit exactement quelle clé manque. Le déploiement échoue proprement, l'ancienne version reste en ligne.
| Interface | Comportement | Quand |
|---|---|---|
IOptions<T> | Lu une fois, singleton, jamais rafraîchi | Le défaut, dans 95 % des cas |
IOptionsSnapshot<T> | Constant pendant une requête, relu à la suivante | Valeur modifiable à chaud, cohérente sur la requête (scoped) |
IOptionsMonitor<T> | Toujours la dernière valeur, avec notification de changement | Singletons, tâches de fond, drapeaux de fonctionnalité |
Environnements
// La variable ASPNETCORE_ENVIRONMENT décide de tout
// (« Development » sur ta machine via launchSettings.json, « Production » par défaut ailleurs)
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage(); // pile d'appels détaillée : JAMAIS en production
app.MapOpenApi();
}
else
{
app.UseExceptionHandler();
app.UseHsts();
}
// Environnement personnalisé
if (app.Environment.IsEnvironment("Recette")) { /* ... */ }
Journaliser : parler à son futur soi
| Niveau | Sens | Exemple | En production |
|---|---|---|---|
Trace | Détail extrême | Valeur de chaque variable | Jamais (fuite de données) |
Debug | Diagnostic de développement | « cache manqué pour la clé X » | Non |
Information | Événement métier normal | « Commande 42 validée » | Oui, avec parcimonie |
Warning | Anormal mais géré | « Nouvel essai après échec réseau » | Oui |
Error | Échec d'une opération | « Paiement refusé, exception X » | Oui — à surveiller |
Critical | L'application est en danger | « Base inaccessible » | Oui — à alerter |
_log.LogInformation(
"Commande " + id + " validée en " + ms + "ms");
// Résultat : du texte plat.
// Impossible de filtrer « toutes les commandes
// de plus de 500 ms » sans expression
// régulière hasardeuse._log.LogInformation(
"Commande {CommandeId} validée en {DureeMs}ms",
id, ms);
// Le message ET les champs nommés sont
// enregistrés séparément :
// CommandeId=42, DureeMs=812
// → requêtable, agrégeable, graphable._log.LogInformation($"Commande {id} validée") compile et affiche correctement, mais le
champ id est fondu dans le texte : plus aucun champ exploitable, et un message différent à chaque
fois (donc impossible à regrouper). Utilise les accolades comme modèle et passe les valeurs en
arguments. Un analyseur peut te le signaler automatiquement.
public class ServicePaiement(ILogger<ServicePaiement> log) // le T donne la « catégorie »
{
public async Task PayerAsync(int commandeId, decimal montant)
{
// Une portée : TOUS les logs émis à l'intérieur porteront ces champs
using var portee = log.BeginScope("Paiement de la commande {CommandeId}", commandeId);
log.LogInformation("Début du paiement de {Montant:C}", montant);
try
{
await _psp.DebiterAsync(montant);
log.LogInformation("Paiement accepté");
}
catch (PspException ex)
{
// L'exception en PREMIER paramètre : la pile est conservée
log.LogError(ex, "Paiement refusé pour {Montant:C}", montant);
throw;
}
}
}
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Boutique": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore.Database.Command": "Warning"
}
}
}
La catégorie est le nom complet du type (Boutique.Services.ServicePaiement), donc le
préfixe "Boutique" couvre toute ton application. La dernière ligne fait taire le SQL généré par EF
Core, très bavard en Information.
- Mots de passe, jetons, clés d'API, numéros de carte — même « temporairement pour débugger ».
- Données personnelles inutiles au diagnostic : préfère un identifiant à un nom et une adresse.
- Le contenu complet d'une requête ou d'une réponse (volume + risque).
- Un log par élément dans une boucle sur 100 000 éléments : ton stockage de logs coûte plus cher que ta base.
Chaque appel LogInformation alloue (boxing des arguments, tableau de paramètres) même si le
niveau est désactivé. Sur un chemin très chaud, utilise le générateur de source, qui produit du code sans
allocation et vérifie le nombre d'arguments à la compilation :
internal static partial class Journal
{
[LoggerMessage(EventId = 1001, Level = LogLevel.Information,
Message = "Commande {commandeId} validée en {dureeMs}ms")]
public static partial void CommandeValidee(ILogger logger, int commandeId, long dureeMs);
}
Journal.CommandeValidee(_log, 42, 812); // zéro allocation si le niveau est désactivéPour aller au-delà du texte, .NET expose métriques et traces via OpenTelemetry — les trois signaux (logs, métriques, traces) partagent le même identifiant de corrélation, ce qui permet de passer d'une alerte à la trace complète d'une requête. ASP.NET Core 11 émet nativement les attributs OpenTelemetry pour chaque requête. Voir chapitre 19.
Vérifie que c'est passé
🎯 Quiz — 4 questions
1. En production, une variable d'environnement Smtp__Port=587 et
appsettings.json avec "Port": 25. Quelle valeur est utilisée ?
2. Quel est l'intérêt de .ValidateOnStart() ?
3. _log.LogInformation($"Client {id} supprimé") — quel est le problème ?
LogInformation("Client {ClientId} supprimé", id). Le message
devient un modèle stable et ClientId un champ requêtable.4. Un service singleton doit voir une valeur de configuration modifiée à chaud. Que lui injecter ?
IOptions capture la valeur au premier accès et ne bouge plus.
IOptionsMonitor expose toujours la dernière version et notifie les changements.Fiches de révision
Smtp__Port.dotnet user-secrets set "Cle" "valeur" — hors du dossier du projet.LogInformation("... {Id}", id).LogError(ex, "message {X}", x).- La configuration est un empilement : le dernier calque gagne. Les variables d'environnement sont le mécanisme de production.
- Pattern Options +
ValidateOnStart(): configuration typée, validée au démarrage. - Les secrets ne sont jamais dans le dépôt. Un secret commité est un secret à révoquer.
- Logs structurés : accolades comme modèle, valeurs en arguments.
- Jamais de secret ni de donnée personnelle inutile dans les logs.