Le modèle d'options dans ASP.NET Core offre un moyen structuré d'accéder aux valeurs de configuration. Il permet de lier des sections de configuration à des objets fortement typés, de valider ces objets et de surveiller leurs modifications. Ce mécanisme est essentiel pour gérer efficacement les paramètres d'application.
Fondamentaux du Modèle d'Options
Dépendances Essentielles
Pour exploiter pleinement le modèle d'options, les packages NuGet suivants sont généralement requis :
Microsoft.Extensions.Options: Le cœur du modèle d'options, permettant la configuration en mémoire.Microsoft.Extensions.Options.ConfigurationExtensions: Fournit les extensions pour lier des options à des sources de configuration comme des fichiers JSON.Microsoft.Extensions.DependencyInjection: Indispensable car les options s'intègrent étroitement avec le conteneur d'injection de dépendances.Microsoft.Extensions.Options.DataAnnotations: Permet la validation d'options à l'aide d'attributs de Data Annotations.
Interfaces Clés
Le modèle repose sur plusieurs interfaces importantes :
IOptions<TOptions>: Fournit un accès singleton aux options. La valeur est calculée une seule fois au démarrage de l'application et ne se rafraîchit pas automatiquement en cas de modification de la configuration. Ne supporte pas les options nommées.IOptionsSnapshot<TOptions>: Fournit un accès scope aux options. La valeur est recalculée à chaque nouvelle résolution du scope (par exemple, par requête HTTP), permettant de refléter les modifications de configuration récentes. Supporte les options nommées.IOptionsMonitor<TOptions>: Fournit un accès singleton aux options et permet de surveiller les modifications de configuration. Il expose un événementOnChangepour réagir aux mises à jour. Supporte les options nommées.IOptionsFactory<TOptions>: Une interface interne utilisée pour créer et configurer des instances d'options, appliquant toutes les configurations et validations enregistrées.IConfigureOptions<TOptions>: Interface pour enregistrer des actions de configuration qui sont appliquées lors de la création d'une instance d'option. Plusieurs implémentations peuvent être enregistrées pour une même option.IPostConfigureOptions<TOptions>: Similaire àIConfigureOptions, mais ces actions sont exécutées après toutes les actionsIConfigureOptions. Utile pour des ajustements finaux.OptionsChangeTokenSource<TOptions>: Un composant interne qui génère des jetons de changemant pour signaler les modifications de configuraton.IValidateOptions<TOptions>: Interface pour implémenter des logiques de validation personnalisées pour les options.
Utilisation Basique
Pour commencer, ajoutons les packages nécessaires et configurons une classe d'options simple.
Dépendances
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
</ItemGroup>
Configuration Directe
Voici comment configurer des options en utilisant des délégués Configure et PostConfigure.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
// Définition de notre classe d'options
public class ParametresApplication
{
public string PremierReglage { get; set; } = "Défaut A";
public string SecondReglage { get; set; } = "Défaut B";
}
var collectionServices = new ServiceCollection();
// 1. Configuration initiale via un délégué Configure.
// Plusieurs appels à Configure sont exécutés séquentiellement.
collectionServices.Configure<ParametresApplication>(options =>
{
options.PremierReglage = "Valeur configurée 1";
});
// 2. Configuration post-exécution via un délégué PostConfigure.
// Ces délégués s'exécutent après tous les délégués Configure.
collectionServices.PostConfigure<ParametresApplication>(options =>
{
options.SecondReglage = "Valeur post-configurée 2";
});
// L'approche AddOptions().Configure().PostConfigure() est une syntaxe fluide
// qui réalise la même chose en interne.
// collectionServices.AddOptions<ParametresApplication>()
// .Configure(options => { options.PremierReglage = "Valeur chaînée 3"; })
// .PostConfigure(options => { options.SecondReglage = "Valeur chaînée 4"; });
var fournisseurServices = collectionServices.BuildServiceProvider();
// Résolution des services d'options
var optionsFactory = fournisseurServices.GetRequiredService<IOptionsFactory<ParametresApplication>>();
var optionsInstance = fournisseurServices.GetRequiredService<IOptions<ParametresApplication>>();
var optionsMonitor = fournisseurServices.GetRequiredService<IOptionsMonitor<ParametresApplication>>();
var optionsSnapshot = fournisseurServices.GetRequiredService<IOptionsSnapshot<ParametresApplication>>();
Console.WriteLine($"IOptions - Premier Reglage: {optionsInstance.Value.PremierReglage}, Second Reglage: {optionsInstance.Value.SecondReglage}");
Console.WriteLine("Appuyez sur une touche pour terminer.");
Console.ReadKey();
Mécenismes Internes
Lors de l'enregistrement et de l'utilisation du modèle d'options, plusieurs étapes se déroulent en coulisses :
- Un appel à
Configureenregistre une implémentation deIConfigureOptions<TOptions>dans le conteneur de services. Ce service encapsule le délégué de configuration que nous avons fourni. - Un appel à
PostConfigureenregistre une implémentation deIPostConfigureOptions<TOptions>, agissant de manière similaire mais étant exécuté plus tard. - La première utilisation de
ConfigureouPostConfigurepour un type d'option donné déclenche un appel interne àAddOptions(). AddOptions()enregistre les services fondamentaux commeIOptions<TOptions>,IOptionsSnapshot<TOptions>,IOptionsMonitor<TOptions>,IOptionsFactory<TOptions>etIOptionsMonitorCache<TOptions>si ce n'est pas déjà fait.- L'
IOptionsFactory<TOptions>est le pivot. Sa méthodeCreateest responsable de :- Instancier l'objet d'option (généralement via son constructeur sans paramètres).
- Exécuter tous les délégués
IConfigureOptions<TOptions>enregistrés sur l'instance d'option. - Exécuter ensuite tous les délégués
IPostConfigureOptions<TOptions>. - Enfin, exécuter toutes les logiques de validation
IValidateOptions<TOptions>.
IOptions<TOptions>utiliseIOptionsFactorypour créer une instance d'option avec le nom par défaut (Options.DefaultName). Étant un singleton, cette instance est calculée une seule fois.IOptionsSnapshot<TOptions>utilise égalementIOptionsFactorypour créer une instance. Parce qu'il est enregistré comme un service de durée de vie de scope, une nouvelle instance est créée (et donc reconfigurée) à chaque nouvelle résolution dans un scope donné, permettant de refléter des changements récents.IOptionsMonitor<TOptions>est un singleton. Il fournit une méthodeGetqui peut récupérer des options nommées et est capable de notifier les changements via son événementOnChange, ce qui le rend idéal pour les configurations dynamiques.
Surveillance des Modifications de Configuration
L'une des fonctionnalités puissantes est la capacité de réagir aux changements de configuration en temps réel.
Dépendances
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="7.0.0" />
</ItemGroup>
Fichier de Configuration (appsettings.json)
{
"MonApplication": {
"PremierParametre": "Initial Alpha",
"SecondParametre": "Initial Beta"
}
}
Code de Surveillance
Assurez-vous que reloadOnChange est défini à true lors de l'ajout du fichier JSON.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
using System.IO;
using System.Threading;
public class ParametresGlobaux
{
public string PremierParametre { get; set; } = string.Empty;
public string SecondParametre { get; set; } = string.Empty;
}
var configurationManager = new ConfigurationManager();
configurationManager.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); // Important pour la surveillance
var collectionServices = new ServiceCollection();
collectionServices.Configure<ParametresGlobaux>(configurationManager.GetSection("MonApplication"));
var fournisseurServices = collectionServices.BuildServiceProvider();
Console.WriteLine("Modifiez appsettings.json et observez les changements... (Ctrl+C pour quitter)");
// IOptionsMonitor peut enregistrer un délégué pour notifier les changements.
// C'est un singleton, donc l'événement OnChange n'est enregistré qu'une fois.
var monitorGlobaux = fournisseurServices.GetRequiredService<IOptionsMonitor<ParametresGlobaux>>();
monitorGlobaux.OnChange(options =>
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"[CHANGEMENT] Les options ont été mises à jour par IOptionsMonitor. Premier: {options.PremierParametre}, Second: {options.SecondParametre}");
Console.ResetColor();
});
while (true)
{
Thread.Sleep(5000); // Attente de 5 secondes
using (var scope = fournisseurServices.CreateScope())
{
var optionsFixes = scope.ServiceProvider.GetRequiredService<IOptions<ParametresGlobaux>>();
var optionsSnapshot = scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<ParametresGlobaux>>();
var optionsMonitor = scope.ServiceProvider.GetRequiredService<IOptionsMonitor<ParametresGlobaux>>();
Console.WriteLine("----------------------------------------------------");
Console.WriteLine($"IOptions (fixe): Premier: {optionsFixes.Value.PremierParametre}, Second: {optionsFixes.Value.SecondParametre}");
Console.WriteLine($"IOptionsSnapshot (par scope): Premier: {optionsSnapshot.Value.PremierParametre}, Second: {optionsSnapshot.Value.SecondParametre}");
Console.WriteLine($"IOptionsMonitor (courant): Premier: {optionsMonitor.CurrentValue.PremierParametre}, Second: {optionsMonitor.CurrentValue.SecondParametre}");
}
}
Options Nommées
Le modèle d'options prend en charge les options nommées, ce qui est utile lorsque vous avez plusieurs configurations du même type d'option, mais avec des paramètres différents.
Dépendances
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
</ItemGroup>
Exemple de Code pour Options Nommées
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
public class ConfigurationServiceExterne
{
public string UrlBase { get; set; } = "https://default.api";
public int TimeoutSecondes { get; set; } = 30;
}
var collectionServices = new ServiceCollection();
// Configure l'option nommée "ServiceA"
collectionServices.Configure<ConfigurationServiceExterne>("ServiceA", options =>
{
options.UrlBase = "https://api.servicea.com";
options.TimeoutSecondes = 60;
});
// Configure l'option nommée "ServiceB"
collectionServices.Configure<ConfigurationServiceExterne>("ServiceB", options =>
{
options.UrlBase = "https://api.serviceb.com/v2";
options.TimeoutSecondes = 120;
});
// Configure l'option par défaut (sans nom spécifique)
collectionServices.Configure<ConfigurationServiceExterne>(options =>
{
options.UrlBase = "https://api.default.com";
options.TimeoutSecondes = 90;
});
var fournisseurServices = collectionServices.BuildServiceProvider();
// Récupération des options nommées
var optionsMonitor = fournisseurServices.GetRequiredService<IOptionsMonitor<ConfigurationServiceExterne>>();
var configServiceA = optionsMonitor.Get("ServiceA");
var configServiceB = optionsMonitor.Get("ServiceB");
var configDefaut = optionsMonitor.Get(Options.DefaultName); // Ou simplement optionsMonitor.CurrentValue si vous voulez l'option par défaut
Console.WriteLine($"Configuration Service A: URL="{configServiceA.UrlBase}", Timeout={configServiceA.TimeoutSecondes}s");
Console.WriteLine($"Configuration Service B: URL="{configServiceB.UrlBase}", Timeout={configServiceB.TimeoutSecondes}s");
Console.WriteLine($"Configuration par Défaut: URL="{configDefaut.UrlBase}", Timeout={configDefaut.TimeoutSecondes}s");
Console.WriteLine("Appuyez sur une touche pour terminer.");
Console.ReadKey();
Configuration Avancée avec DI
Dépendances
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
</ItemGroup>
Injection de Dépendances dans la Configuration
Il est possible d'injecter des services du conteneur DI lors de la configuration des options.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
// Classe d'options
public class AppConfigSettings
{
public string MessageGlobal { get; set; } = "Défaut";
public int NbreRetries { get; set; } = 3;
}
// Service qui sera injecté pour aider à configurer les options
public class ServiceConfigurationHelper
{
private readonly IConfiguration _config; // Exemple d'injection de IConfiguration
public ServiceConfigurationHelper(IConfiguration config)
{
_config = config;
}
public void Initialiser(AppConfigSettings settings)
{
settings.MessageGlobal = "Initialisé par DI Helper";
settings.NbreRetries = _config.GetValue<int>("DefaultRetries", 5); // Lecture d'une valeur de config
}
}
var collectionServices = new ServiceCollection();
// Pour cet exemple, nous simulons IConfiguration
var inMemoryConfig = new ConfigurationBuilder().AddInMemoryCollection(
new Dictionary<string, string?> { { "DefaultRetries", "7" } })
.Build();
collectionServices.AddSingleton<IConfiguration>(inMemoryConfig);
// Enregistrer le service d'aide à la configuration
collectionServices.AddSingleton<ServiceConfigurationHelper>();
collectionServices.AddOptions<AppConfigSettings>()
// Utilisez PostConfigure pour injecter une dépendance (ServiceConfigurationHelper)
.PostConfigure<ServiceConfigurationHelper>((options, helper) =>
{
helper.Initialiser(options);
});
var fournisseurServices = collectionServices.BuildServiceProvider();
var appOptions = fournisseurServices.GetRequiredService<IOptions<AppConfigSettings>>();
Console.WriteLine($"Message: {appOptions.Value.MessageGlobal}, Retries: {appOptions.Value.NbreRetries}");
Console.WriteLine("Appuyez sur une touche pour terminer.");
Console.ReadKey();
Implémentation de IConfigureOptions<T>
Vous pouvez externaliser la logique de configuration dans une classe dédiée implémentant IConfigureOptions<T>.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
public class ModuleConfig
{
public string NomModule { get; set; } = "Inconnu";
public bool EstActif { get; set; } = false;
}
// Implémentation de IConfigureOptions pour ModuleConfig
internal class ConfigureModuleParDefaut : IConfigureOptions<ModuleConfig>
{
public void Configure(ModuleConfig config)
{
config.NomModule = "Module Principal";
config.EstActif = true;
}
}
var collectionServices = new ServiceCollection();
collectionServices.AddOptions(); // Nécessaire pour enregistrer les services d'options
collectionServices.AddSingleton<IConfigureOptions<ModuleConfig>, ConfigureModuleParDefaut>();
var fournisseurServices = collectionServices.BuildServiceProvider();
var moduleOptions = fournisseurServices.GetRequiredService<IOptions<ModuleConfig>>();
Console.WriteLine($"Nom du module: {moduleOptions.Value.NomModule}, Actif: {moduleOptions.Value.EstActif}");
Console.WriteLine("Appuyez sur une touche pour terminer.");
Console.ReadKey();
Implémentation de IPostConfigureOptions<T>
De même, la logique de post-configuration peut être placée dans une classe implémentant IPostConfigureOptions<T>.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
public class ParametresRapport
{
public string FormatRapport { get; set; } = "PDF";
public string CheminSauvegarde { get; set; } = "C:\\Temp";
}
// Implémentation de IPostConfigureOptions pour ParametresRapport
internal class PostConfigureParametresRapport : IPostConfigureOptions<ParametresRapport>
{
public void PostConfigure(string? name, ParametresRapport parametres)
{
// Supposons qu'une configuration initiale a déjà défini FormatRapport
// Ici, nous nous assurons que le chemin de sauvegarde est toujours absolu ou défini par défaut
if (string.IsNullOrWhiteSpace(parametres.CheminSauvegarde) || !Path.IsPathRooted(parametres.CheminSauvegarde))
{
parametres.CheminSauvegarde = Path.Combine(Environment.CurrentDirectory, "Rapports");
}
}
}
var collectionServices = new ServiceCollection();
collectionServices.AddOptions();
// Configuration initiale (sera écrasée si PostConfigure a une valeur non null pour FormatRapport)
collectionServices.Configure<ParametresRapport>(p => p.FormatRapport = "Excel");
collectionServices.AddSingleton<IPostConfigureOptions<ParametresRapport>, PostConfigureParametresRapport>();
var fournisseurServices = collectionServices.BuildServiceProvider();
var rapportOptions = fournisseurServices.GetRequiredService<IOptions<ParametresRapport>>();
Console.WriteLine($"Format du rapport: {rapportOptions.Value.FormatRapport}, Chemin de sauvegarde: {rapportOptions.Value.CheminSauvegarde}");
Console.WriteLine("Appuyez sur une touche pour terminer.");
Console.ReadKey();
Validation des Options
La validation est cruciale pour s'assurer que les options chargées sont valides et cohérentes.
Dépendances
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.Options.DataAnnotations" Version="7.0.0" />
</ItemGroup>
Validation par Délégué
Utilisez la méthode Validate pour ajouter une logique de validation personnalisée sous forme de délégué.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
public class ConfigurationService
{
public string NomService { get; set; } = string.Empty;
public string EndpointUrl { get; set; } = string.Empty;
}
var collectionServices = new ServiceCollection();
// Configurez les options avec des valeurs potentiellement invalides
collectionServices.Configure<ConfigurationService>(c =>
{
c.NomService = " "; // Nom invalide
c.EndpointUrl = "http://localhost:8080/api";
});
// Ajoutez une validation via un délégué
collectionServices.AddOptions<ConfigurationService>()
.Validate(options =>
{
if (string.IsNullOrWhiteSpace(options.NomService))
{
return false; // Le nom du service ne doit pas être vide
}
if (!Uri.TryCreate(options.EndpointUrl, UriKind.Absolute, out _))
{
return false; // L'URL de l'endpoint doit être valide
}
return true;
}, "La configuration du service est invalide : Le nom ou l'URL est manquant ou incorrect.");
var fournisseurServices = collectionServices.BuildServiceProvider();
try
{
var serviceOptions = fournisseurServices.GetRequiredService<IOptions<ConfigurationService>>();
Console.WriteLine($"Nom: {serviceOptions.Value.NomService}, Endpoint: {serviceOptions.Value.EndpointUrl}");
}
catch (OptionsValidationException ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine($"Erreur de validation des options: {ex.Message}");
Console.ResetColor();
}
Console.WriteLine("Appuyez sur une touche pour terminer.");
Console.ReadKey();
Validation par Attributs de Données (Data Annotations)
Pour une validation plus déclarative, vous pouvez utiliser les attributs System.ComponentModel.DataAnnotations.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using System;
using System.ComponentModel.DataAnnotations; // Nécessaire pour les attributs
public class ParametresBaseDeDonnees
{
[Required(ErrorMessage = "Le nom de la connexion est obligatoire.")]
[MinLength(5, ErrorMessage = "Le nom de la connexion doit avoir au moins 5 caractères.")]
public string NomConnexion { get; set; } = string.Empty;
[Required(ErrorMessage = "La chaîne de connexion est obligatoire.")]
[RegularExpression(@"^(Data Source=|Server=).+", ErrorMessage = "La chaîne de connexion semble invalide.")]
public string ChaineConnexion { get; set; } = string.Empty;
}
var collectionServices = new ServiceCollection();
// Configurez les options avec des valeurs à valider
collectionServices.Configure<ParametresBaseDeDonnees>(c =>
{
c.NomConnexion = " "; // Trop court
c.ChaineConnexion = "invalide"; // Ne correspond pas au regex
});
// Active la validation par Data Annotations
collectionServices.AddOptions<ParametresBaseDeDonnees>()
.ValidateDataAnnotations();
var fournisseurServices = collectionServices.BuildServiceProvider();
try
{
var dbOptions = fournisseurServices.GetRequiredService<IOptions<ParametresBaseDeDonnees>>();
Console.WriteLine($"Nom Connexion: {dbOptions.Value.NomConnexion}, Chaîne Connexion: {dbOptions.Value.ChaineConnexion}");
}
catch (OptionsValidationException ex)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Erreur de validation des options de base de données:");
foreach (var failure in ex.Failures)
{
Console.WriteLine($"- {failure}");
}
Console.ResetColor();
}
Console.WriteLine("Appuyez sur une touche pour terminer.");
Console.ReadKey();