Aggiorna dinamicamente la configurazione di base .net dalla configurazione dell'app di Azure


9

Cosa sto cercando di fare: sto tentando di configurare la Configurazione app di Azure con un'applicazione Web mvc 2.1 core .net con una chiave sentinella nella Configurazione app di Azure, con l'obiettivo di poter cambiare le chiavi in ​​azzurro e nessuna delle chiavi verrà aggiornato nelle mie app fino a quando il valore della sentinella non verrà modificato. In teoria, ciò dovrebbe consentirmi di configurare in modo sicuro hot swap.

Qual è il mio problema: quando lo faccio, non esiste alcun metodo WatchAndReloadAll () disponibile per guardare la sentinella su IWebHostBuilder e i metodi alternativi Refresh () non sembrano aggiornare la configurazione mentre affermano.

Informazioni di base e cosa ho provato: ho partecipato a VS Live - San Diego, la scorsa settimana e ho visto una demo sulla configurazione dell'app di Azure. Ho avuto dei problemi nel tentativo di ottenere l'applicazione per aggiornare i valori di configurazione durante l'implementazione, quindi ho anche fatto riferimento a questa demo descrivendo come fare anche questo. La sezione pertinente è di circa 10 minuti. Tuttavia, tale metodo non sembra essere disponibile su IWebHostBuilder.

Documentazione a cui mi riferisco: nella documentazione ufficiale non vi è alcun riferimento a questo metodo, consultare doc quickstart .net core e doc dynamic configuration .net core

Il mio ambiente: utilizzo di dot net core 2.1 eseguito da Visual Studio Enterprise 2019, con l'ultimo pacchetto nuget di anteprima per Microsoft.Azure.AppConfiguration.AspNetCore 2.0.0-preview-010060003-1250

Il mio codice: nella demo, hanno creato un IWebHostBuilder tramite il metodo CreateWebHostBuilder (string [] args) in questo modo:

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

Ho anche provato in questo modo, utilizzando la documentazione corrente:

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

Quindi, nella mia classe di avvio:

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

e infine il mio modello di configurazione delle impostazioni:

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

Ora, nel mio controller, tiro quelle impostazioni e le lancio in un sacchetto di visualizzazione per essere visualizzate sulla vista.

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

Una vista semplice per visualizzare le modifiche:

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

Posso ottenerlo per abbassare la configurazione la prima volta, tuttavia, la funzionalità di aggiornamento non sembra funzionare in alcun modo.

Nell'ultimo esempio, mi aspettavo che le configurazioni si aggiornassero quando la sentinella era impostata su qualsiasi nuovo valore, o almeno, per aggiornare un valore 30 secondi dopo che era stato modificato. Nessun intervallo di attesa aggiorna i valori e solo un arresto completo e un riavvio dell'app caricano la nuova configurazione.

Aggiornamento: aggiunta di app.UseAzureAppConfiguration (); nel metodo configure all'avvio e l'impostazione di un timeout esplicito nella cache per la configurazione ha risolto l'aggiornamento del metodo di aggiornamento dopo un determinato periodo di tempo, ma la funzionalità sentinel continua a non funzionare, né il flag updateAll sul metodo di aggiornamento.


Puoi mostrarmi come e dove accedi alla configurazione? Ho imitato la tua situazione in uno dei miei progetti e funziona perfettamente
Peter Bons,

Mi aspettavo una configurazione vincolante da qualche parte nel tuo ConfigureServicesmetodo in startuop.cs, come services.Configure<LogSettings>(configuration.GetSection("LogSettings"));
Peter Bons,

@peterBons il tuo link mi porta a un 404.
Nick Gasia Robitsch

@PeterBons Ho aggiornato il mio post per includere le informazioni richieste in merito all'iniezione / associazione della configurazione. Non pensavo fosse rilevante in quel momento perché funzionava.
Nick Gasia Robitsch,

1
Questo è stato. Prego.
Peter Bons,

Risposte:


6

Ok, dopo molti test, prove ed errori, ho funzionato.

Il mio problema era un servizio mancante per azzurro sul metodo di configurazione. C'è un comportamento interessante qui, in quanto ridurrà comunque le impostazioni, non aggiornerà, se questo manca. Quindi, una volta inserito, e con una sentinella corretta configurata per documentazione, funziona con il flag updateAll. Tuttavia questo non è attualmente documentato.

Ecco la soluzione:

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

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

Il modello di Impostazioni Sto associando i miei dati azzurrati a:

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 controller home generico con la configurazione impostata su ViewBag per passare alla nostra vista:

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

La nostra vista:

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

Spero che questo aiuti qualcun altro!

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.