Динамическое обновление конфигурации ядра .NET из конфигурации приложения Azure
Что я пытаюсь сделать: я пытаюсь настроить конфигурацию приложения Azure с веб-приложением mvc.net core 2.1 с контрольным ключом в конфигурации приложения Azure, чтобы иметь возможность изменять ключи в лазурном режиме и ни один из ключей будет обновляться в моих приложениях, пока не изменится значение дозорного. Теоретически это должно позволить мне безопасно выполнять горячую замену конфигураций.
В чем моя проблема: когда я это делаю, нет доступного метода WatchAndReloadAll() для наблюдения за дозорным в IWebHostBuilder, а альтернативные методы Refresh(), похоже, не обновляют конфигурацию, как они заявляют.
Общие сведения и то, что я пробовал: на прошлой неделе я посетил VS Live - Сан-Диего и посмотрел демонстрацию конфигурации приложений Azure. У меня были проблемы с попыткой заставить приложение обновлять значения конфигурации при его внедрении, поэтому я также сослался на эту демонстрацию, описывая, как это сделать. Соответствующий раздел находится примерно через 10 минут. Однако этот метод, похоже, недоступен в IWebHostBuilder.
На документацию, на которую я ссылаюсь: в официальной документации нет ссылки на этот метод, см. Doc quickstart .net core и doc dynamic configuration.net core
Моя среда: использование dot net core 2.1, запущенного из Visual Studio Enterprise 2019, с последним предварительным пакетом nuget для Microsoft.Azure.AppConfiguration.AspNetCore 2.0.0-preview-010060003-1250
Мой код: в демонстрации они создали IWebHostBuilder с помощью метода CreateWebHostBuilder(string[] args) следующим образом:
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>();
}
Я тоже пробовал это, используя текущую документацию:
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>();
Затем в моем классе запуска:
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?}");
});
}
и, наконец, моя модель конфигурации настроек:
public class Settings
{
public string BackgroundColor { get; set; }
public long FontSize { get; set; }
public string FontColor { get; set; }
public string Message { get; set; }
}
Теперь в своем контроллере я беру эти настройки и бросаю их в корзину для отображения в представлении.
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();
}
}
Простой вид для отображения изменений:
<!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>
Я могу заставить его вытащить конфигурацию в первый раз, однако функция обновления, похоже, никоим образом не работает.
В последнем примере я ожидал, что конфиги обновятся, когда для дозорного будет установлено любое новое значение, или, по крайней мере, обновить значение через 30 секунд после его изменения. Никакое ожидание обновляет значения, и только полное выключение и перезапуск приложения загружает новую конфигурацию.
Обновление: добавление app.UseAzureAppConfiguration(); в методе настройки при запуске и установка явного тайм-аута в кеше для конфигурации исправила метод обновления для обновления через фиксированный промежуток времени, но функция дозорного по-прежнему не работает, как и флаг updateAll в методе обновления.
1 ответ
Хорошо, после долгих испытаний, проб и ошибок, все заработало.
Моя проблема заключалась в отсутствии службы для Azure в методе настройки. Здесь есть интересное поведение, так как он все равно сбрасывает настройки, он просто не будет обновляться, если он отсутствует. Итак, после того, как это было введено и с надлежащим сигналом, настроенным в документации, он работает с флагом updateAll. Однако в настоящее время это не задокументировано.
Вот решение:
В 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>();
}
}
Затем в 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?}");
});
}
}
}
Модель настроек, с которой я привязываю полученные данные в лазурном режиме:
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; }
}
}
Общий домашний контроллер с настройкой ViewBag для передачи нашему представлению:
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 });
}
}
}
Наше мнение:
<!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>
Надеюсь, это поможет кому-то другому!