Dynamicznie aktualizuj podstawową konfigurację .net z konfiguracji aplikacji Azure

9

Co próbuję zrobić: próbuję skonfigurować konfigurację aplikacji Azure za pomocą aplikacji sieci web .net core 2.1 mvc z kluczem wartownika w konfiguracji aplikacji Azure, aby móc zmieniać klucze w kolorze lazurowym i żaden z kluczy będzie aktualizować w moich aplikacjach, dopóki wartość wartownika nie ulegnie zmianie. Teoretycznie powinno to pozwolić mi na bezpieczne konfiguracje wymiany podczas pracy.

Mój problem polega na tym: kiedy to robię, nie ma dostępnej metody WatchAndReloadAll () do obserwowania wartownika na IWebHostBuilder, a alternatywne metody Refresh () nie wydają się odświeżać konfiguracji w jej stanie.

Informacje podstawowe i to, czego próbowałem: W ubiegłym tygodniu uczestniczyłem w VS Live - San Diego i oglądałem prezentację konfiguracji aplikacji Azure. Miałem pewne problemy z próbą nakłonienia aplikacji do odświeżenia wartości konfiguracji podczas jej zaimplementowania, więc odwołałem się również do tego demo, opisując, jak to zrobić. Odpowiednia sekcja trwa około 10 minut. Jednak ta metoda nie wydaje się być dostępna w IWebHostBuilder.

Dokumentacja, do której się odwołuję: W oficjalnej dokumentacji nie ma odniesienia do tej metody, patrz dokument Szybki start .net core i doc. Konfiguracja dynamiczna .net core

Moje środowisko: używanie dot net core 2.1 z Visual Studio Enterprise 2019 z najnowszym pakietem nuget podglądu dla Microsoft.Azure.AppConfiguration.AspNetCore 2.0.0-Preview-010060003-1250

Mój kod: W wersji demo utworzyli IWebHostBuilder za pomocą metody CreateWebHostBuilder (string [] args) w następujący sposób:

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

Próbowałem również w ten sposób, korzystając z bieżącej dokumentacji:

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

Następnie w mojej klasie startowej:

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

i wreszcie mój model konfiguracji ustawień:

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

Teraz w moim kontrolerze wyciągam te ustawienia i wrzucam je do torby widokowej, która będzie wyświetlana w widoku.

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

Prosty widok do wyświetlenia zmian:

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

Mogę go pobrać po raz pierwszy, ale funkcja odświeżania nie działa w żaden sposób.

W ostatnim przykładzie spodziewałem się, że konfiguracje zaktualizują się, gdy wartownik zostanie ustawiony na dowolną nową wartość, lub przynajmniej zaktualizuje wartość 30 sekund po jej zmianie. Brak czasu oczekiwania aktualizuje wartości, a tylko pełne zamknięcie i ponowne uruchomienie aplikacji ładuje nową konfigurację.

Aktualizacja: dodawanie aplikacji UseAzureAppConfiguration (); w metodzie config podczas uruchamiania i ustawienie jawnego limitu czasu w pamięci podręcznej dla konfiguracji poprawiło odświeżanie metody odświeżania po ustalonym czasie, ale funkcja wartownika nadal nie działa, podobnie jak flaga updateAll w metodzie odświeżania.

Nick Gasia Robitsch
źródło
Czy możesz mi pokazać, jak i gdzie masz dostęp do konfiguracji? Naśladowałem twoją sytuację w jednym z moich własnych projektów i działa doskonale
Peter Bons,
Spodziewałem się powiązania konfiguracji gdzieś w twojej ConfigureServicesmetodzie w startuop.cs, na przykład services.Configure<LogSettings>(configuration.GetSection("LogSettings"));
Peter Bons
@peterBons Twój link zabiera mnie do 404.
Nick Gasia Robitsch
@PeterBons Zaktualizowałem swój post, aby zawierał wymagane informacje dotyczące iniekcji / wiązania konfiguracji. Nie sądziłem, że to było wtedy odpowiednie, ponieważ to działało.
Nick Gasia Robitsch
1
To było to. Nie ma za co.
Peter Bons,

Odpowiedzi:

6

Ok, po wielu testach i próbach i błędach, mam to działające.

Moim problemem była brakująca usługa lazura w metodzie konfiguracji. Jest tu kilka interesujących zachowań, ponieważ nadal będą one pobierać ustawienia, po prostu nie zaktualizują się, jeśli tego brakuje. Więc kiedy to zostało wprowadzone, a przy odpowiedniej konfiguracji wartownika w dokumentacji, działa z flagą updateAll. Jednak nie jest to obecnie udokumentowane.

Oto rozwiązanie:

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

Następnie w 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?}");
            });
        }
    }
}

Model ustawień Wiążę moje pobrane dane lazurowe z:

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

Ogólny kontroler domowy z konfiguracją ustawioną na ViewBag, aby przejść do naszego widoku:

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

Nasz widok:

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

Mam nadzieję, że to pomoże komuś innemu!

Nick Gasia Robitsch
źródło