Loupe

ASP.NET Core: quand les environnements ne suffisent pas, utilisez des sous-environnements !

ASP.NET Core possède la notion d'environnement, qui permet à une application d'utiliser des paramètres différents selon l'environnement dans lequel elle s'exécute. Par exemple, on peut avoir des environnements Development/Staging/Production, avec chacun leur fichier de paramètres, et un fichier de paramètres communs partagés par tous les environnements :

  • appsettings.json : paramètres globaux
  • appsettings.Development.json : paramètres spécifiques à l'environnement Development
  • appsettings.Staging.json : paramètres spécifiques à l'environnement Staging
  • appsettings.Production.json : paramètres spécifiques à l'environnement Production

Avec la configuration par défaut de l'hôte, les paramètres spécifiques à l'environnement héritent des paramètres globaux, donc il n'est pas nécessaire de respécifier les paramètres inchangés dans chaque environnement s'ils sont déjà définis dans le fichier de paramètres globaux.

Bien sûr, on peut nommer les environnements comme on le souhaite ; Development/Staging/Production est juste une convention.

On peut spécifier quel environnement utiliser via la variable d'environnement ASPNETCORE_ENVIRONMENT, ou via le paramètre de ligne de commande --environment. Quand on travaille dans Visual Studio, la sélection de l'environnement se fait habituellement dans un profil de démarrage dans Properties/launchSettings.json.

Limitations

Cette fonctionnalité est bien pratique, mais parfois, ça ne suffit pas. Dans un environnement donné, on pourrait vouloir utiliser des paramètres différents selon le scénario qu'on souhaite tester.

Prenons un exemple concret. Je travaille sur une solution qui contient, entre autres, une API web et un serveur d'authentification. L'API authentifie les utilisateurs à l'aide de tokens JWT, qui sont fournis par le serveur d'authentification. La plupart du temps, quand je travaille sur l'API, je n'ai pas besoin de modifier le serveur d'authentification, et j'utilise simplement celui qui est déployé dans l'environnement de développement dans Azure. Mais quand je dois faire des modifications sur le serveur d'authentification, je l'exécute en local, et je dois modifier la configuration de l'API pour qu'elle utilise le serveur d'authentification local. Et je dois faire attention de ne pas commiter ces modifications, pour ne pas casser l'API de dev dans Azure. Ce n'est pas un problème majeur, mais ça m'embête…

Une solution possible serait de créer un nouvel environnement "DevelopmentWithLocalAuth", avec son propre fichier de paramètres. Mais les paramètres seraient les mêmes que ceux de l'environnement Development, la seule différence étant l'URL du serveur d'authentification. Et j'ai horreur d'avoir plusieurs copies de la même chose, car il faut les maintenir synchronisées… Ce que je voudrais vraiment, c'est un moyen d'hériter des paramètres de l'environnement Development, et redéfinir uniquement les paramètres qui changent, sans toucher aux paramètres de l'environnement Development.

Les "sous-environments" à la rescousse

Ne cherchez pas dans la documentation, ce n'est pas vraiment une feature d'ASP.NET Core ; c'est juste un nom que j'ai inventé. Mais en gros, l'idée est qu'il est assez facile d'introduire un autre "niveau" de configuration qui va juste redéfinir certains paramètres de l'environnement "parent".

Par exemple, dans mon scénario, je veux introduire un fichier appsettings.Development.LocalAuth.json qui hérite des paramètres de l'environnement Development et qui redéfinit simplement l'URL du serveur d'authentification :

{
    "Authentication": {
        "Authority": "https://localhost:6001"
    }
}

Un moyen de faire ça est d'ajouter une nouvelle source de configuration dans la configuration de l'hôte dans Program.cs :

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((context, builder) =>
        {
            string subenv = context.Configuration["SubEnvironment"];
            if (!string.IsNullOrEmpty(subenv))
            {
                var env = context.HostingEnvironment;
                builder.AddJsonFile($"appsettings.{env.EnvironmentName}.{subenv}.json", optional: true, reloadOnChange: true);
            }
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

(Ce code est pour ASP.NET Core 3.0, mais on peut faire la même chose en ASP.NET Core 2.0 avec WebHostBuilder au lieu de HostBuilder.)

La partie importante est dans l'appel à ConfigureAppConfiguration. Ce code ajoute un fichier de configuration JSON dont le nom dépend de l'environnement et du sous-environnement. Puisque cette soure de configuration est ajoutée après celles déjà présentes, elle écrase les paramètres fournis par les sources précédentes.

Le nom du sous-environnement est récupéré depuis la configuration de l'hôte, qui est elle-même basée sur les variables d'environnement commençant par ASPNETCORE_ et sur les paramètres de ligne de commande. Pour spécifier qu'on veut travailler dans le sous-environnement "LocalAuth", il faut donc définir la variable d'environnement ASPNETCORE_SUBENVIRONMENT à "LocalAuth".

Et c'est tout ! Avec ça, on peut affiner les environnements existants pour des scénarios spécifiques, sans avoir à les redéfinir entièrement.

Note : Étant donné que la source de configuration est ajoutée en dernier, elle écrase les paramètres de TOUTES les sources de configuration précédentes, pas seulement ceux des fichiers appsettings*.json. Le host builder par défaut ajoute les secrets utilisateur, les variables d'environnement, et les paramètres de ligne de commande après les fichiers JSON, donc les paramètres définis de cette façon seront aussi écrasés par ceux définis dans le sous-environnement. Ce n'est pas idéal, mais probablement pas un problème majeur dans la plupart des scénarios. Si on veut absolument respecter l'ordre des sources, la solution est d'insérer la nouvelle source de configuration juste après les sources JSON existantes, mais avant les secrets utilisateurs. Cela rend le code un peu plus complexe, mais ça reste faisable :

        ...
        .ConfigureAppConfiguration((context, builder) =>
        {
            string subenv = context.Configuration["SubEnvironment"];
            if (!string.IsNullOrEmpty(subenv))
            {
                var env = context.HostingEnvironment;
                var newSource = new JsonConfigurationSource
                {
                    Path = $"appsettings.{env.EnvironmentName}.{subenv}.json",
                    Optional = true,
                    ReloadOnChange = true
                };
                newSource.ResolveFileProvider();

                var lastJsonConfigSource = builder.Sources
                    .OfType<JsonConfigurationSource>()
                    .LastOrDefault(s => !s.Path.Contains("secrets.json"));
                if (lastJsonConfigSource != null)
                {
                    var index = builder.Sources.IndexOf(lastJsonConfigSource);
                    builder.Sources.Insert(index + 1, newSource);
                }
                else
                {
                    builder.Sources.Insert(0, newSource);
                }
            }
        })
        ...
Photo de profil

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus