Configuration d’ASP .NET Core : l’option pattern

Dans un précédent article, nous avons vu comment ASP.NET Core construisait sa configuration à partir de différentes sources et comment ces sources sont priorisées.
Nous vous proposons à présent de nous concentrer sur l’option pattern et les avantages que propose ce pattern pour manipuler la configuration d’une application ASP.NET Core.
Rappel de la lecture de configuration
Voici le support que nous vous proposons pour illustrer notre propos. Imaginons que nous ayons une API (Application Cliente) qui doit communiquer avec un service externe pour connaître la météo d’une ville.
Pour commencer, créons une application cliente, une API avec ASP.NET Core. Celle-ci aura besoin d’un token pour avoir l’accès au service externe.
dotnet new webapi -n LearnOptionPattern
1. Dans le Controller de notre API Client
2. Ajoutons dans le fichier Program.cs le code suivant pour injecter la factory qui créera notre HttpClient.
3. Enfin, dans le service externe se trouve un autre projet API en ASP.NET Core, défini comme suit :
Il s’agit du code par défaut proposé par dotnet cli lors de la création d’un nouveau projet Web API. Nous avons modifié le contrôleur pour qu’il renvoie la météo d’une ville et nous avons ajouté un mécanisme simple d’authentification.
Lorsque les deux projets sont lancés et que l’on appelle notre API Cliente :
La réponse obtenue est la suivante :
Cela montre que notre API communique bien avec le service externe.
Nous pouvons ainsi refactorer le code de notre Web API Cliente, pour mettre dans la configuration de notre application l’url du service externe et le token d’authentification. La manière la plus simple est définie comme suit :
1. Ajoutons dans l’ appsettings.json les nouvelles clés de configuration
NB : Notez que l’on place nos clés de configuration dans la section ExternalService. Cela apporte une meilleure compréhension du contexte des clés de configuration et une meilleure organisation de la configuration.
2. Modifions notre Controller pour qu’il puisse lire la configuration
A ce stade, le code fonctionne comme avant.
Prenons un peu de recul et analysons le code existant. Pour les besoins de l’article, nous avons mis le token dans les configurations de l’API en clair. En temps normal, cette information sensible devrait être placée dans un endroit sécurisé comme un Azure Key Vault.
Injecter le IConfiguration dans le Controller est un antipattern, car le Controller a accès à toutes les clés de configuration de l’application y compris celles qui lui sont inutiles. De plus, le Controller est fortement couplé à la structure de la configuration pour lire les clés de configuration (ExternalService:Url ou ExternalService:Token). Tout changement de la structure de la configuration impactera le contrôleur.
L’option pattern est construit au-dessus de la gestion classique de configuration d’ASP.NET Core. Il permet de mapper des clés de configuration à un objet, limitant ainsi le scope des configurations au sens/au but de l’objet. Voyons comment mettre en application ce pattern avec notre exemple précédent.
Option pattern en action
Pour mettre en place l’option pattern, nous allons créer un objet qui contiendra l’ensemble des configurations nécessaires pour communiquer avec le service externe ainsi que le nom de la section de configuration qui permet de l’alimenter.
A noter : C’est une bonne pratique que l’Option contienne le nom de section de configuration pour l’alimenter.
Afin d’utiliser cette option, nous devons modifier le Program.cs en configurant le gestionnaire de dépendances, afin qu’il puisse gérer l’option via l’injection de dépendances.
Le Controller peut ainsi utiliser l’option via l’interface IOptions<T> qui expose l’instance de l’option dans sa propriété Value.
Dès lors, l’application devrait continuer à fonctionner comme auparavant.
En utilisant l’option pattern, nous avons fait deux choses :
- Découpler le contrôleur de la structure de la configuration
- Fournir les clés de configuration nécessaires au Controller pour appeler le service externe.
Perspectives apportées par l’option pattern
Le bon fonctionnement d’une application nécessite de faire appel à plusieurs services externes. Ces derniers peuvent être – comme dans notre exemple – une web API, une base de données, un cache et bien d’autres. L’exploitation de ces différents services peut nécessiter des configurations spécifiques.
L’un des bonnes pratiques consiste à séparer les configurations en groupes cohérents, facilitant ainsi leur lecture et la compréhension globale de leur utilité. L’option pattern exploite la structure de la configuration et met en place deux designs pattern intéressants :
- L’encapsulation: les classes qui dépendent des paramètres de configuration dépendent uniquement des paramètres de configuration qu’elles utilisent ;
- La séparation de préoccupation (Separation of Concern) : les paramètres des différentes parties de l’application ne sont pas dépendants ou couplés les uns aux autres.
Par cette approche, il devient simple d’ajouter des nouvelles configurations et de les exploiter en minimisant l’impact sur le code existant.
Limitations et contraints sur les classes d’option
Les classes supportées par l’option pattern doivent suivre les contraintes suivantes :
- La classe ne peut pas être abstraite et avoir un constructeur sans paramètres
- Toutes les propriétés en lecture et écriture sont initialisées
- Les champs de classe ne sont pas initialisés
Ainsi, dans notre exemple, les propriétés URL et token sont initialisées par la configuration de l’application mais le champ WeatherServiceConfigurationSectionName n’est pas affecté.
Prise en charge des changements à chaud
L’interface IOption<T> quant à elle est enregistrée dans le gestionnaire de dépendances, en tant que singleton. Elle ne lit la configuration de l’application et ne s’initialise qu’au démarrage de l’application.
Pour illustrer ce fait, depuis notre exemple précédent, au sein de l’application externe, nous décidons de réinitialiser un nouveau token à partir de l’ancien token. Et cela sans arrêter notre application cliente.
1. Lançons l’API client et l’API du service externe
2. Re-générons un nouveau token pour l’API cliente
3. Modifions l’appsettings.json de l’API cliente avec le nouveau token sans arrêter l’application.
4. Une exception devrait apparaître, indiquant que nous ne sommes pas autorisés à utiliser le service, et ce malgré le changement de configuration côté application cliente.
Pour corriger cette erreur, la solution la plus simple serait de redémarrer l’application cliente. Or cette solution est difficile à justifier dans un contexte de production où l’interruption de service doit être la plus mimine possible.
L’option pattern dans ASP.NET Core supporte plusieurs interfaces, notamment le IOptionsSnapshot<TOptions> et le IOptionsMonitor<TOptions> qui couvre cet aspect de rechargement à chaud. Voyons dans le détail comment cela se manifeste.
IOptionsSnapshot
L’IOptionsSnapshot s’utilise de la même façon dans l’IOptions. L’IOptionsSnapshot est enregistré dans le gestionnaire en tant que service scopé : cela signifie qu’il supporte le rechargement de la configuration, puisque l’IOptionsSnapshot est reconstruit à chaque requête http faite. Ainsi, si la configuration change au cours de l’exécution de l’application, le IOptionsSnapshot en tiendra compte. C’est du rechargement à chaud puisque le changement de configuration est fait et pris en compte pendant l’exécution de l’application.
⚠️ Attention : Le rechargement à chaud n’est possible que si le provider de la source de configuration supporte aussi le rechargement lors d’un changement de la configuration. Ce point a été expliqué dans notre article précédent sur la configuration dans ASP.NET Core. Dans notre cas, c’est le JSON provider de l’appsettings.json.
L’IOptionsSnapshot s’utilise de la même façon que l’IOptions.
Si l’on reproduit le scénario précédent, la mise à jour du token permet de continuer à avoir accès au service externe depuis l’application cliente sans redémarrer cette dernière.
IOptionsMonitor
L’IOptionsMonitor est, quant à lui, enregistré dans le gestionnaire de dépendances en tant que singleton comme l’IOptions.
Pourquoi aurait-on besoin de L’IOptionsMonitor ?
Bien qu’il soit possible d’utiliser IOptions (singleton) et IOptionsSnapshot (scopé) dans notre Controller (celui-ci étant scopé), ce n’est pas cas pour les objets singletons ayant besoin d’un accès à la configuration. En effet, ASP.NET Core met en place une protection dans le gestionnaire de dépendances appelée Validation scopée (Scoped Validation). Cette validation interdit à un service singleton d’avoir une dépendance scopée.
IOptions étant singleton mais ne fournissant les configurations initialisés qu’au démarrage et IOptionsSnapshot nous fournissant le rechargement des configurations au prix de la violation de la validation scopée, il nous faut donc l’IOptionsMonitor.
L’IOptionsMonitor peut être injecté en toute sécurité dans un service singleton et profiter de la recharge à chaud des configurations.
L’intégration du IOtionsMinotor dans notre Controller se fait de la façon suivante :
La propriété CurrentValue contient la valeur de la configuration à initialisation de l’application. Comme IOptionsMonitor est enregistré en tant que singleton, la value CurrentValue n’est réinitialisée qu’au redémarrage de l’application (cf. comportement de l’IOptions) ; c’est là que la méthode Onchange entre jeu. Elle va ajouter un délégué qui sera invoqué lorsque la configuration associée change.
Ici notre délégué met à jour le champ options de notre Controller.
L’initialisation de I’OptionsMonitor dans le Program.cs est identique aux deux autres options.
Utiliser les options dans le Program.cs
Le code qui suit nous permet d’utiliser les options depuis le Program.cs.
L’initialisation des options
L’initialisation des propriétés d’un objet option se fait en un pour un, avec les clés de configuration présentes dans la source. Si une propriété de l’option n’a pas d’équivalent dans la source, alors elle est initialisée par la valeur par défaut de son type :
Ici, sans la présence de la clé RetryAllowed, la source de configuration, la valeur par défaut est default(Int) soit 0.
Dès que l’on ajoute la clé de configuration, la valeur est correctement initialisée.
Appsettings.json
Pour identifier ce problème en exploitation, il est possible de mettre en place de la validation sur les options et ainsi avoir des détails clairs sur la nature du problème.
Validation des options
Il est possible d’exploiter les annotations dans la librairie System.ComponentModel.DataAnnotations afin d’ajouter des contraintes de validation sur nos options. Voyons comment la mettre en place dans notre exemple.
Prise en compte des annotations
Pour tenir compte de la validation nous devons modifier la façon d’enregistrer nos options dans le Program.cs.
Program.cs
Nous avons remplacé la méthode Configure par AddOptions. Cette dernière donne accès à deux autres méthodes :
- Bind : qui permet l’initialisation des options avec une section de notre configuration
- ValidationDataAnnotations : qui valide nos options à partir des Annotations présentes dans la définition de nos options.
ExternalApiOptions.cs
Dorénavant, si une erreur se glisse dans notre source de configuration, nous serons en mesure de traiter au mieux les erreurs de configuration. Dans notre exemple ci-dessous, nous avons une typo sur l’une des clés de configuration (Url) et une autre qui n’est pas comprise dans la plage de valeurs acceptée :
appsetting.json
A l’exécution, nous voyons un message d’erreur qui explicite les erreurs décrites précédemment.
Response
Validation personnalisée
Si la validation via les annotations ne permet pas de couvrir votre besoin, il est possible de personnaliser son processus de validation dans le Program.cs par la méthode de Validate. Elle peut ainsi voir au plus 5 dépendances injectées par le gestionnaire de dépendances.
Option pattern dans ASP .Net : l’essentiel à retenir
Nous avons vu, à partir d’un simple cas, comment utiliser l’option pattern dans ASP.NET Core, mettant ainsi en évidences les conséquences de ce pattern sur la lisibilité du code et sa maintenabilité.
Par ailleurs, nous avons vu que l’Option pattern est représenté par trois interfaces : IOptions, IOptionsSnapshot et IOptionsMonitor, chacune répondant à des problématiques de centralisation d’accès à la configuration et de rechargement de la configuration à chaud dans un contexte d’objet singleton ou scopé.
Enfin, nous nous sommes penchés sur la validation de la configuration via la librairie de data annotation de dotnet.
Pour en savoir plus sur l’option pattern en ASP.NET Core, nous vous invitons à consulter les articles suivants :
- Configuration in ASP.NET Core | Microsoft Docs
- Options pattern in ASP.NET Core | Microsoft Docs
- Lien vers le GitHub du code source