Arrêtons de parler de DDD et de réflexion pendant un moment. Je propose de parler du simple, de l'organisation des paramètres de l'application.
Après que mes collègues et moi avons décidé de passer à .NET Core, la question s'est posée de savoir comment organiser les fichiers de configuration, comment effectuer des transformations, etc. dans le nouvel environnement. Le code suivant se trouve dans de nombreux exemples, et beaucoup l'ont utilisé avec succès.
public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) { Environment = environment; Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); }
Mais voyons comment fonctionne la configuration, et dans quels cas utiliser cette approche, et dans lequel faire confiance aux développeurs de .NET Core. Je demande un chat.
Comme avant
Comme toute histoire, cet article a un début. L'un des premiers problèmes après le passage à ASP.NET Core a été la transformation des fichiers de configuration.
Rappelez-vous comment c'était avant avec web.config
La configuration se composait de plusieurs fichiers. Le fichier principal était web.config , et des transformations ( web.Development.config , etc.) lui étaient déjà appliquées, selon la configuration de l'assembly. Dans le même temps, les attributs xml ont été activement utilisés pour rechercher et transformer la section du document xml .
Mais comme nous le savons dans ASP.NET Core, le fichier web.config a été remplacé par appsettings.json et il n'y a plus le mécanisme de transformation habituel.
Que nous dit google?Le résultat de recherche pour "Transformer en ASP.NET Core" sur google a donné le code suivant:
public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) { Environment = environment; Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); }
Dans le constructeur de la classe Startup , nous créons un objet de configuration à l'aide de ConfigurationBuilder . Dans ce cas, nous indiquons explicitement les sources de configuration que nous voulons utiliser.
Et tels:
public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) { Environment = environment; Configuration = new ConfigurationBuilder() .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); }
Selon la variable d'environnement, l'une ou l'autre source de configuration est sélectionnée.
Ces réponses se trouvent souvent sur SO et d'autres ressources moins populaires. Mais le sentiment n'est pas parti. que nous allons mal. Que faire si je souhaite utiliser des variables d'environnement ou des arguments de ligne de commande dans la configuration? Pourquoi dois-je écrire ce code dans chaque projet?
À la recherche de la vérité, j'ai dû approfondir la documentation et le code source. Et je veux partager les connaissances acquises dans cet article.
Voyons comment fonctionne la configuration dans .NET Core.
La configuration
La configuration dans .NET Core est représentée par un objet d' interface IConfiguration .
public interface IConfiguration { string this[string key] { get; set; } IConfigurationSection GetSection(string key); IEnumerable<IConfigurationSection> GetChildren(); IChangeToken GetReloadToken(); }
- indexeur [clé de chaîne] , qui permet d'obtenir la valeur du paramètre de configuration par clé
- GetSection (chaîne clé) renvoie la section de configuration qui correspond à la clé clé
- GetChildren () renvoie un ensemble de sous-sections de la section de configuration actuelle
- GetReloadToken () renvoie une instance de IChangeToken qui peut être utilisée pour recevoir des notifications lorsque la configuration change
Une configuration est une collection de paires clé-valeur. Lors de la lecture à partir d'une source de configuration (fichier, variables d'environnement), les données hiérarchiques sont réduites à une structure plate. Par exemple, json est un objet de la forme
{ "Settings": { "Key": "I am options" } }
sera réduite à une vue à plat:
Settings:Key = I am options
Ici, la clé est Paramètres: Clé et la valeur est Je suis des options .
Les fournisseurs de configuration sont utilisés pour remplir la configuration.
Fournisseurs de configuration
Un objet d'interface est responsable de la lecture des données de la source de configuration.
IConfigurationProvider :
public interface IConfigurationProvider { bool TryGet(string key, out string value); void Set(string key, string value); IChangeToken GetReloadToken(); void Load(); IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath); }
- TryGet (string key, out string value) permet d'obtenir la valeur du paramètre de configuration par clé
- Set (clé de chaîne, valeur de chaîne) est utilisé pour définir la valeur du paramètre de configuration
- GetReloadToken () renvoie une instance de IChangeToken qui peut être utilisée pour recevoir des notifications lorsqu'une source de configuration change
- Méthode Load () qui est responsable de la lecture de la source de configuration
- GetChildKeys (IEnumerable <string> earlyKeys, string parentPath) vous permet d'obtenir une liste de toutes les clés fournies par ce fournisseur de configuration
Les fournisseurs suivants sont disponibles dans la boîte:
- Json
- Ini
- Xml
- Variables d'environnement
- Inmemory
- Azure
- Fournisseur de configuration personnalisé
Les conventions suivantes pour l'utilisation des fournisseurs de configuration sont acceptées.
- Les sources de configuration sont lues dans l'ordre dans lequel elles ont été spécifiées.
- Si les mêmes clés sont présentes dans différentes sources de configuration (la comparaison n'est pas sensible à la casse), la dernière valeur ajoutée est utilisée.
Si nous créons une instance d'un serveur Web à l'aide de CreateDefaultBuilder , les fournisseurs de configuration suivants sont connectés par défaut:

- ChainedConfigurationProvider via ce fournisseur, vous pouvez obtenir des valeurs et des clés de configuration qui ont été ajoutées par d'autres fournisseurs de configuration
- JsonConfigurationProvider utilise des fichiers json comme source de configuration. Comme vous pouvez le voir, trois fournisseurs de ce type sont ajoutés à la liste des fournisseurs. Le premier utilise appsettings.json comme source, le second utilise appsettings. {Environment} .json . Le troisième lit les données de secrets.json . Si vous générez l'application dans la configuration Release , le troisième fournisseur ne sera pas connecté, car il n'est pas recommandé d'utiliser des secrets dans l'environnement de production
- EnvironmentVariablesConfigurationProvider récupère les paramètres de configuration des variables d'environnement
- CommandLineConfigurationProvider permet d'ajouter des arguments de ligne de commande à la configuration
La configuration étant stockée sous forme de dictionnaire, il est nécessaire de s'assurer que les clés sont uniques. Par défaut, cela fonctionne comme ceci.
Si le fournisseur CommandLineConfigurationProvider a un élément avec la clé key et que le fournisseur JsonConfigurationProvider a un élément avec la clé key, l'élément de JsonConfigurationProvider sera remplacé par l'élément de CommandLineConfigurationProvider car il est enregistré en dernier et a une priorité plus élevée.
Rappelons un exemple du début de l'article public IConfiguration Configuration { get; set; } public IHostingEnvironment Environment { get; set; } public Startup(IConfiguration configuration, IHostingEnvironment environment) { Environment = environment; Configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .AddJsonFile($"appsettings.{Environment.EnvironmentName}.json") .Build(); }
Nous n'avons pas besoin de créer IConfiguration nous-mêmes afin de transformer les fichiers de configuration, car cela est activé par défaut. Cette approche est nécessaire lorsque l'on veut limiter le nombre de sources de configuration.
Fournisseur de configuration personnalisé
Pour écrire votre fournisseur de configuration, vous devez implémenter les interfaces IConfigurationProvider et IConfigurationSource . IConfigurationSource est une nouvelle interface que nous n'avons pas encore considérée dans cet article.
public interface IConfigurationSource { IConfigurationProvider Build(IConfigurationBuilder builder); }
L'interface consiste en une seule méthode Build qui prend IConfigurationBuilder comme paramètre et renvoie une nouvelle instance de IConfigurationProvider .
Pour implémenter nos fournisseurs de configuration, les classes abstraites ConfigurationProvider et FileConfigurationProvider sont à notre disposition. Dans ces classes, la logique des méthodes TryGet , Set , GetReloadToken , GetChildKeys est déjà implémentée et il reste à implémenter uniquement la méthode Load .
Regardons un exemple. Il est nécessaire d'implémenter la lecture de la configuration à partir du fichier yaml , et il est également nécessaire que nous puissions changer la configuration sans redémarrer notre application.
Créez la classe YamlConfigurationProvider et faites-en l'héritier de FileConfigurationProvider .
public class YamlConfigurationProvider : FileConfigurationProvider { private readonly string _filePath; public YamlConfigurationProvider(FileConfigurationSource source) : base(source) { } public override void Load(Stream stream) { throw new NotImplementedException(); } }
Dans l'extrait de code ci-dessus, vous pouvez remarquer certaines fonctionnalités de la classe FileConfigurationProvider . Le constructeur accepte une instance de FileConfigurationSource , qui contient le IFileProvider . IFileProvider est utilisé pour lire un fichier et pour s'abonner à un événement de modification de fichier. Vous pouvez également remarquer que la méthode Load accepte un flux dans lequel le fichier de configuration est ouvert en lecture. Il s'agit d'une méthode de la classe FileConfigurationProvider et ne se trouve pas dans l'interface IConfigurationProvider .
Ajoutez une implémentation simple qui nous permet de lire le fichier yaml . Pour lire le fichier, je vais utiliser le package YamlDotNet .
Implémentation de YamlConfigurationProvider public class YamlConfigurationProvider : FileConfigurationProvider { private readonly string _filePath; public YamlConfigurationProvider(FileConfigurationSource source) : base(source) { } public override void Load(Stream stream) { if (stream.CanSeek) { stream.Seek(0L, SeekOrigin.Begin); using (StreamReader streamReader = new StreamReader(stream)) { var fileContent = streamReader.ReadToEnd(); var yamlObject = new DeserializerBuilder() .Build() .Deserialize(new StringReader(fileContent)) as IDictionary<object, object>; Data = new Dictionary<string, string>(); foreach (var pair in yamlObject) { FillData(String.Empty, pair); } } } } private void FillData(string prefix, KeyValuePair<object, object> pair) { var key = String.IsNullOrEmpty(prefix) ? pair.Key.ToString() : $"{prefix}:{pair.Key}"; switch (pair.Value) { case string value: Data.Add(key, value); break; case IDictionary<object, object> section: { foreach (var sectionPair in section) FillData(pair.Key.ToString(), sectionPair); break; } } } }
Pour créer une instance de notre fournisseur de configuration, vous devez implémenter FileConfigurationSource .
Implémentation de YamlConfigurationSource public class YamlConfigurationSource : FileConfigurationSource { public YamlConfigurationSource(string fileName) { Path = fileName; ReloadOnChange = true; } public override IConfigurationProvider Build(IConfigurationBuilder builder) { this.EnsureDefaults(builder); return new YamlConfigurationProvider(this); } }
Il est important de noter ici que pour initialiser les propriétés de la classe de base, vous devez appeler la méthode this.EnsureDefaults (builder) .
Pour enregistrer un fournisseur de configuration personnalisé dans l'application, vous devez ajouter l'instance de fournisseur à IConfigurationBuilder . Vous pouvez appeler la méthode Add depuis IConfigurationBuilder , mais je mettrai immédiatement la logique d' initialisation YamlConfigurationProvider dans la méthode d' extension .
Implémenter YamlConfigurationExtensions public static class YamlConfigurationExtensions { public static IConfigurationBuilder AddYaml( this IConfigurationBuilder builder, string filePath) { if (builder == null) throw new ArgumentNullException(nameof(builder)); if (string.IsNullOrEmpty(filePath)) throw new ArgumentNullException(nameof(filePath)); return builder .Add(new YamlConfigurationSource(filePath)); } }
Appelez la méthode AddYaml public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, builder) => { builder.AddYaml("appsettings.yaml"); }) .UseStartup<Startup>(); }
Suivi des modifications
Dans la nouvelle configuration de l' API , il est devenu possible de relire la source de configuration lorsqu'elle est modifiée. En même temps, l'application ne redémarre pas.
Comment ça marche:
- Le fournisseur de configuration surveille les modifications de la source de configuration
- Si un changement de configuration se produit, un nouveau IChangeToken est créé .
- Lorsque IChangeToken est modifié , un rechargement de configuration est appelé
Voyons comment le suivi des modifications est implémenté dans FileConfigurationProvider .
ChangeToken.OnChange(
Deux paramètres sont transmis à la méthode OnChange de la classe statique ChangeToken . Le premier paramètre est une fonction qui renvoie un nouveau IChangeToken lorsque la source de configuration (dans ce cas, le fichier) change, il s'agit du soi-disant producteur . Le deuxième paramètre est la fonction de rappel (ou consommateur ), qui sera appelée lorsque la source de configuration est modifiée.
En savoir plus sur la classe ChangeToken .
Tous les fournisseurs de configuration n'implémentent pas le suivi des modifications. Ce mécanisme est disponible pour les descendants de FileConfigurationProvider et AzureKeyVaultConfigurationProvider .
Conclusion
Dans .NET Core, nous avons un mécanisme simple et pratique pour gérer les paramètres des applications. De nombreux modules complémentaires sont disponibles prêts à l'emploi, de nombreuses choses sont utilisées par défaut.
Bien sûr, chaque personne décide de la façon de l'utiliser, mais je suis pour le fait que les gens connaissent leurs outils.
Cet article ne couvre que les bases. En plus des bases, les IOptions, les scripts de post-configuration, la validation des paramètres et bien plus sont à notre disposition. Mais c'est une autre histoire.
Vous pouvez trouver le projet d'application avec des exemples de cet article dans le référentiel sur Github .
Partagez dans les commentaires qui utilise quelles approches de gestion de la configuration?
Merci de votre attention.
upd .: Comme AdAbsurdum l'a correctement suggéré, lorsque vous travaillez avec des tableaux, les éléments ne seront pas toujours remplacés lors de la fusion d'une configuration à partir de deux sources.
Prenons un exemple. Lors de la lecture d'un tableau depuis appsettings.json, nous obtenons cette vue plate:
array:0=valueA
Lors de la lecture à partir de appsettings.Development.json :
array:0=valueB array:1=value
En conséquence, la configuration sera:
array:0=valueB array:1=value
Tous les éléments avec des indices uniques ( tableau: 1 dans l'exemple) seront ajoutés au tableau résultant. Les éléments provenant de différentes sources de configuration, mais ayant le même index ( tableau: 0 dans l'exemple) seront fusionnés et l'élément qui a été ajouté en dernier sera utilisé.