Mettre à jour dynamiquement la configuration de base .net à partir de la configuration des applications Azure

9

Ce que j'essaie de faire: j'essaie de configurer Azure App Configuration avec une application Web .net core 2.1 mvc avec une clé sentinelle dans Azure App Configuration, dans le but de pouvoir changer les clés en azur et aucune des clés sera mis à jour dans mes applications jusqu'à ce que la valeur sentinelle ait changé. En théorie, cela devrait me permettre de procéder à des swaps à chaud en toute sécurité.

Quel est mon problème: lorsque je fais cela, aucune méthode WatchAndReloadAll () n'est disponible pour surveiller la sentinelle sur IWebHostBuilder, et les méthodes alternatives Refresh () ne semblent pas actualiser la configuration comme elles le disent.

Informations générales et ce que j'ai essayé: J'ai assisté à VS Live - San Diego, la semaine dernière et j'ai regardé une démo sur Azure App Configuration. J'ai eu quelques problèmes pour obtenir l'application pour actualiser les valeurs de configuration lors de l'implémentation, j'ai donc également référencé cette démo décrivant comment le faire également. La section pertinente se trouve à environ 10 minutes. Cependant, cette méthode ne semble pas être disponible sur IWebHostBuilder.

Documentation que je référence: Dans la documentation officielle il n'y a aucune référence à cette méthode voir doc quickstart .net core et doc dynamic configuration .net core

Mon environnement: utilisation de dot net core 2.1 en cours d'exécution à partir de Visual Studio Enterprise 2019, avec le dernier package de nuget d'aperçu pour Microsoft.Azure.AppConfiguration.AspNetCore 2.0.0-preview-010060003-1250

Mon code: dans la démo, ils ont créé un IWebHostBuilder via la méthode CreateWebHostBuilder (string [] args) comme ceci:

public static IWebHostBuilder CreateWebHostBuilder(string[] args)
{
    return WebHost.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((hostingContext, config) =>
    {
        var settings = config.Build();
        config.AddAzureAppConfiguration(options =>
        {
            options.Connect(settings["ConnectionStrings:AzureConfiguration"])
            .Use(keyFilter: "TestApp:*")
            .WatchAndReloadAll(key: "TestApp:Sentinel", pollInterval: TimeSpan.FromSeconds(5));
        }); 
    })
    .UseStartup<Startup>();
}

Je l'ai également essayé de cette façon, en utilisant la documentation actuelle:

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((hostingContext, config) =>
    {
        var settings = config.Build();

        config.AddAzureAppConfiguration(options =>
        {
            // fetch connection string from local config. Could use KeyVault, or Secrets as well.
            options.Connect(settings["ConnectionStrings:AzureConfiguration"])
            // filter configs so we are only searching against configs that meet this pattern
            .Use(keyFilter: "WebApp:*")
            .ConfigureRefresh(refreshOptions =>
            { 
                // In theory, when this value changes, on the next refresh operation, the config will update all modified configs since it was last refreshed.
                refreshOptions.Register("WebApp:Sentinel", true);
                refreshOptions.Register("WebApp:Settings:BackgroundColor", false);
                refreshOptions.Register("WebApp:Settings:FontColor", false);
                refreshOptions.Register("WebApp:Settings:FontSize", false);
                refreshOptions.Register("WebApp:Settings:Message", false);
            });
        });
    })
    .UseStartup<Startup>();

Ensuite, dans ma classe de démarrage:

public Startup(IConfiguration configuration)
{
    Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
    services.Configure<Settings>(Configuration.GetSection("WebApp:Settings"));
    services.Configure<CookiePolicyOptions>(options =>
    {
        // This lambda determines whether user consent for non-essential cookies is needed for a given request.
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
        app.UseHsts();
    }

    app.UseAzureAppConfiguration();
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseCookiePolicy();

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

et enfin mon modèle de configuration des paramètres:

public class Settings
{
    public string BackgroundColor { get; set; }
    public long FontSize { get; set; }
    public string FontColor { get; set; }
    public string Message { get; set; }
}

Maintenant, dans mon contrôleur, je tire ces paramètres et les jette dans un sac de vue pour les afficher sur la vue.

public class HomeController : Controller
{
    private readonly Settings _Settings;

    public HomeController(IOptionsSnapshot<Settings> settings)
    {
        _Settings = settings.Value;
    }

    public IActionResult Index()
    {
        ViewData["BackgroundColor"] = _Settings.BackgroundColor;
        ViewData["FontSize"] = _Settings.FontSize;
        ViewData["FontColor"] = _Settings.FontColor;
        ViewData["Message"] = _Settings.Message;

        return View();
    }
}

Une vue simple pour afficher les changements:

<!DOCTYPE html>
<html lang="en">
<style>
    body {
        background-color: @ViewData["BackgroundColor"]
    }
    h1 {
        color: @ViewData["FontColor"];
        font-size: @ViewData["FontSize"];
    }
</style>
<head>
    <title>Index View</title>
</head>
<body>
    <h1>@ViewData["Message"]</h1>
</body>
</html>

Je peux l'obtenir pour tirer la configuration vers le bas la première fois, cependant, la fonctionnalité de rafraîchissement ne semble en aucun cas fonctionner.

Dans le dernier exemple, je m'attendais à ce que les configs se mettent à jour lorsque la sentinelle a été définie sur une nouvelle valeur, ou à tout le moins, à mettre à jour une valeur 30 secondes après sa modification. Aucune longueur d'attente ne met à jour les valeurs, et seul un arrêt complet et un redémarrage de l'application charge la nouvelle configuration.

Mise à jour: ajout de app.UseAzureAppConfiguration (); dans la méthode configure au démarrage et en définissant un délai d'expiration explicite sur le cache pour la configuration, la méthode d'actualisation était actualisée après un certain temps, mais la fonctionnalité sentinelle ne fonctionne toujours pas, pas plus que l'indicateur updateAll sur la méthode d'actualisation.

Nick Gasia Robitsch
la source
Pouvez-vous me montrer comment et où vous accédez à la configuration? J'ai imité votre situation dans l'un de mes propres projets et cela fonctionne parfaitement
Peter Bons
Je m'attendais à une liaison de configuration quelque part dans votre ConfigureServicesméthode dans startuop.cs, comme services.Configure<LogSettings>(configuration.GetSection("LogSettings"));
Peter Bons
@peterBons votre lien me mène à un 404.
Nick Gasia Robitsch
@PeterBons J'ai mis à jour mon message pour inclure les informations demandées concernant l'injection / la liaison de configuration. Je ne pensais pas que c'était pertinent à l'époque parce que cela fonctionnait.
Nick Gasia Robitsch du
1
C'était ça. Je vous en prie.
Peter Bons

Réponses:

6

Ok, après de nombreux tests et essais et erreurs, je le fais fonctionner.

Mon problème était un service manquant pour azure sur la méthode de configuration. Il y a un comportement intéressant ici, dans la mesure où il abaissera toujours les paramètres, il ne sera simplement pas mis à jour, s'il est manquant. Donc, une fois que cela a été installé et avec une sentinelle appropriée configurée par la documentation, cela fonctionne avec le drapeau updateAll. Cependant, cela n'est actuellement pas documenté.

Voici la solution:

Dans Program.cs:

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration.AzureAppConfiguration;

namespace ASPNetCoreApp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateWebHostBuilder(args).Build().Run();
        }   // Main

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((hostingContext, config) =>
            {
                var settings = config.Build();

                config.AddAzureAppConfiguration(options =>
                {
                    // fetch connection string from local config. Could use KeyVault, or Secrets as well.
                    options.Connect(settings["ConnectionStrings:AzureConfiguration"])
                    // filter configs so we are only searching against configs that meet this pattern
                    .Use(keyFilter: "WebApp:*")
                    .ConfigureRefresh(refreshOptions =>
                    { 
                        // When this value changes, on the next refresh operation, the config will update all modified configs since it was last refreshed.
                        refreshOptions.Register("WebApp:Sentinel", true);
                        // Set a timeout for the cache so that it will poll the azure config every X timespan.
                        refreshOptions.SetCacheExpiration(cacheExpirationTime: new System.TimeSpan(0, 0, 0, 15, 0));
                    });
                });
            })
            .UseStartup<Startup>();
    }
}

Puis dans Startup.cs:

using ASPNetCoreApp.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace ASPNetCoreApp
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // bind the config to our DI container for the settings we are pulling down from azure.
            services.Configure<Settings>(Configuration.GetSection("WebApp:Settings"));
            services.Configure<CookiePolicyOptions>(options =>
            {
                // This lambda determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = SameSiteMode.None;
            });

            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                app.UseHsts();
            }
            // Set the Azure middleware to handle configuration
            // It will pull the config down without this, but will not refresh.
            app.UseAzureAppConfiguration();
            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseCookiePolicy();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

Le modèle de paramètres auquel je lie mes données extraites d'azur:

namespace ASPNetCoreApp.Models
{
    public class Settings
    {
        public string BackgroundColor { get; set; }
        public long FontSize { get; set; }
        public string FontColor { get; set; }
        public string Message { get; set; }
    }
}

Un contrôleur d'accueil générique avec la configuration définie sur le ViewBag pour passer à notre vue:

using ASPNetCoreApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System.Diagnostics;

namespace ASPNetCoreApp.Controllers
{
    public class HomeController : Controller
    {
        private readonly Settings _Settings;

        public HomeController(IOptionsSnapshot<Settings> settings)
        {
            _Settings = settings.Value;
        }
        public IActionResult Index()
        {
            ViewData["BackgroundColor"] = _Settings.BackgroundColor;
            ViewData["FontSize"] = _Settings.FontSize;
            ViewData["FontColor"] = _Settings.FontColor;
            ViewData["Message"] = _Settings.Message;

            return View();
        }

        public IActionResult About()
        {
            ViewData["Message"] = "Your application description page.";

            return View();
        }

        public IActionResult Contact()
        {
            ViewData["Message"] = "Your contact page.";

            return View();
        }

        public IActionResult Privacy()
        {
            return View();
        }

        [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
        public IActionResult Error()
        {
            return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
        }
    }
}

Notre point de vue:

<!DOCTYPE html>
<html lang="en">
<style>
    body {
        background-color: @ViewData["BackgroundColor"]
    }
    h1 {
        color: @ViewData["FontColor"];
        font-size: @ViewData["FontSize"];
    }
</style>
<head>
    <title>Index View</title>
</head>
<body>
    <h1>@ViewData["Message"]</h1>
</body>
</html>

J'espère que ceci aide quelqu'un d'autre!

Nick Gasia Robitsch
la source