Actualizar dinámicamente la configuración principal de .net desde la configuración de la aplicación de Azure

9

Lo que intento hacer: intento configurar la configuración de la aplicación de Azure con una aplicación web .net core 2.1 mvc con una clave centinela en la configuración de la aplicación de Azure, con el objetivo de poder cambiar las claves en azul, y ninguna de las claves se actualizará en mis aplicaciones hasta que el valor centinela haya cambiado. En teoría, esto debería permitirme intercambiar configuraciones de forma segura.

Cuál es mi problema: cuando hago esto, no hay un método WatchAndReloadAll () disponible para ver el centinela en IWebHostBuilder, y los métodos alternativos Refresh () no parecen actualizar la configuración a medida que se establecen.

Información general y lo que he probado: asistí a VS Live - San Diego, la semana pasada y vi una demostración sobre la Configuración de la aplicación de Azure. Tuve algunos problemas al intentar que la aplicación actualizara los valores de configuración cuando lo implique, por lo que también hice referencia a esta demostración que describe cómo hacerlo también. La sección relevante está a unos 10 minutos. Sin embargo, ese método no parece estar disponible en IWebHostBuilder.

Documentación a la que me refiero: en la documentación oficial no hay referencia a este método; consulte doc quickstart .net core y doc dynamic configuration .net core

Mi entorno: uso de dot net core 2.1 que se ejecuta desde Visual Studio Enterprise 2019, con el último paquete nuget de vista previa para Microsoft.Azure.AppConfiguration.AspNetCore 2.0.0-preview-010060003-1250

Mi código: en la demostración, crearon un IWebHostBuilder a través del método CreateWebHostBuilder (string [] args) de esta manera:

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

También lo intenté de esta manera, usando la documentación actual:

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

Luego, en mi clase de inicio:

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

y finalmente mi modelo de configuración de configuración:

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

Ahora, en mi controlador, extraigo esos ajustes y los tiro a una bolsa de visualización para que se muestren en la 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 simple para mostrar los cambios:

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

Puedo hacer que baje la configuración la primera vez, sin embargo, la funcionalidad de actualización no parece funcionar de ninguna manera.

En el último ejemplo, esperaba que las configuraciones se actualizaran cuando el centinela se estableciera en cualquier valor nuevo, o al menos, actualizar un valor 30 segundos después de que se cambiara. Sin esperas, los valores se actualizan, y solo un apagado y reinicio completo de la aplicación carga la nueva configuración.

Actualización: Agregar aplicación.UseAzureAppConfiguration (); en el método de configuración al inicio, y al establecer un tiempo de espera explícito en la memoria caché para la configuración, se arregló el método de actualización para actualizarse después de un período de tiempo fijo, pero la funcionalidad centinela aún no funciona, ni el indicador updateAll en el método de actualización.

Nick Gasia Robitsch
fuente
¿Me puede mostrar cómo y dónde accede a la configuración? He imitado tu situación en uno de mis propios proyectos y funciona perfectamente
Peter Bons
Esperaba un enlace de configuración en algún lugar de su ConfigureServicesmétodo en startuop.cs, como services.Configure<LogSettings>(configuration.GetSection("LogSettings"));
Peter Bons, el
@peterBons su enlace me lleva a un 404.
Nick Gasia Robitsch
@PeterBons He actualizado mi publicación para incluir la información solicitada con respecto a la configuración de inyección / enlace. No pensé que fuera relevante en ese momento porque eso estaba funcionando.
Nick Gasia Robitsch
1
Eso fue todo. De nada.
Peter Bons

Respuestas:

6

Ok, después de muchas pruebas y pruebas y errores, lo tengo funcionando.

Mi problema era que faltaba un servicio para azure en el método de configuración. Aquí hay un comportamiento interesante, ya que aún eliminará la configuración, simplemente no se actualizará, si esto falta. Por lo tanto, una vez que se haya instalado y con un centinela adecuado configurado por documentación, funciona con el indicador updateAll. Sin embargo, esto no está documentado actualmente.

Aquí está la solución:

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

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

El modelo de configuración a la que estoy vinculando mis datos recuperados azules:

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 controlador doméstico genérico con la configuración configurada en ViewBag para pasar a nuestra 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 });
        }
    }
}

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

¡Espero que esto ayude a alguien más!

Nick Gasia Robitsch
fuente