Aktualisieren Sie die .net-Kernkonfiguration dynamisch über die Azure App-Konfiguration

9

Was ich versuche: Ich versuche, die Azure App-Konfiguration mit einer .net Core 2.1 MVC-Webanwendung mit einem Sentinel-Schlüssel in der Azure App-Konfiguration einzurichten, mit dem Ziel, Schlüssel in Azure und keinen der Schlüssel ändern zu können wird in meinen Apps aktualisiert, bis sich der Sentinel-Wert geändert hat. Theoretisch sollte dies mir ermöglichen, Konfigurationen sicher im laufenden Betrieb auszutauschen.

Was mein Problem ist: Wenn ich dies tue, ist keine WatchAndReloadAll () -Methode verfügbar, um den Sentinel im IWebHostBuilder zu überwachen, und die alternativen Refresh () -Methoden scheinen die Konfiguration nicht so zu aktualisieren, wie sie angeben.

Hintergrundinformationen und was ich versucht habe: Ich habe letzte Woche an VS Live - San Diego teilgenommen und mir eine Demo zur Azure App-Konfiguration angesehen. Ich hatte einige Probleme beim Versuch, die Anwendung dazu zu bringen, Konfigurationswerte zu aktualisieren, als ich sie implementierte. Daher habe ich auch auf diese Demo verwiesen, in der beschrieben wurde, wie dies ebenfalls funktioniert. Der entsprechende Abschnitt befindet sich in ungefähr 10 Minuten. Diese Methode scheint jedoch im IWebHostBuilder nicht verfügbar zu sein.

Dokumentation, auf die ich verweise: In der offiziellen Dokumentation gibt es keinen Verweis auf diese Methode, siehe doc quickstart .net core und doc dynamic configuration .net core

Meine Umgebung: Verwenden von dot net core 2.1 unter Visual Studio Enterprise 2019 mit dem neuesten Preview-Nuget-Paket für Microsoft.Azure.AppConfiguration.AspNetCore 2.0.0-Preview-010060003-1250

Mein Code: In der Demo haben sie einen IWebHostBuilder über die CreateWebHostBuilder-Methode (string [] args) wie folgt erstellt:

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>();
}

Ich habe es auch so versucht, indem ich die aktuelle Dokumentation verwendet habe:

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>();

Dann in meiner Startklasse:

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?}");
    });
}

und schließlich mein Einstellungskonfigurationsmodell:

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

Jetzt ziehe ich in meinem Controller diese Einstellungen und werfe sie in eine Ansichtstasche, um sie in der Ansicht anzuzeigen.

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();
    }
}

Eine einfache Ansicht zum Anzeigen der Änderungen:

<!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>

Ich kann es dazu bringen, die Konfiguration beim ersten Mal herunterzuziehen, aber die Aktualisierungsfunktion scheint in keiner Weise zu funktionieren.

Im letzten Beispiel habe ich erwartet, dass die Konfigurationen aktualisiert werden, wenn der Sentinel auf einen neuen Wert gesetzt wurde, oder zumindest einen Wert 30 Sekunden nach seiner Änderung aktualisieren. Keine Wartezeit aktualisiert die Werte und nur ein vollständiges Herunterfahren und Neustarten der App lädt die neue Konfiguration.

Update: Hinzufügen von app.UseAzureAppConfiguration (); In der Konfigurationsmethode beim Start und durch Festlegen eines expliziten Zeitlimits für die Konfiguration im Cache wurde die Aktualisierungsmethode so festgelegt, dass sie nach einer festgelegten Zeit aktualisiert wird. Die Sentinel-Funktionalität funktioniert jedoch immer noch nicht, und das UpdateAll-Flag für die Aktualisierungsmethode funktioniert nicht.

Nick Gasia Robitsch
quelle
Können Sie mir zeigen, wie und wo Sie auf die Konfiguration zugreifen? Ich habe Ihre Situation in einem meiner eigenen Projekte nachgeahmt und es funktioniert perfekt
Peter Bons
Ich habe irgendwo in Ihrer ConfigureServicesMethode in startuop.cs eine Konfigurationsbindung erwartet , wie services.Configure<LogSettings>(configuration.GetSection("LogSettings"));
Peter Bons
@ PeterBons Ihr Link führt mich zu einem 404.
Nick Gasia Robitsch
@PeterBons Ich habe meinen Beitrag aktualisiert, um die angeforderten Informationen bezüglich der Konfigurationsinjektion / -bindung aufzunehmen. Ich dachte nicht, dass es zu der Zeit relevant war, weil das funktionierte.
Nick Gasia Robitsch
1
Das war's. Bitte.
Peter Bons

Antworten:

6

Ok, nach vielen Tests und Versuchen und Irrtümern funktioniert es.

Mein Problem war ein fehlender Dienst für Azure bei der Konfigurationsmethode. Es gibt hier ein interessantes Verhalten, da die Einstellungen weiterhin heruntergezogen werden und nur nicht aktualisiert werden, wenn dies fehlt. Sobald dies eingegeben wurde und ein ordnungsgemäßer Sentinel gemäß Dokumentation konfiguriert ist, funktioniert es mit dem updateAll-Flag. Dies ist jedoch derzeit nicht dokumentiert.

Hier ist die Lösung:

In 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>();
    }
}

Dann in 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?}");
            });
        }
    }
}

Das Einstellungsmodell, an das ich meine von Azure abgerufenen Daten binde:

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; }
    }
}

Ein generischer Home-Controller, dessen Konfiguration auf ViewBag festgelegt ist, um an unsere Ansicht übergeben zu werden:

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 });
        }
    }
}

Unsere Ansicht:

<!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>

Hoffe das hilft jemand anderem!

Nick Gasia Robitsch
quelle