初步建议您熟悉以下材料:Metanit-Configuration,.NET Core中的配置方式。
问题的提法
您需要实现一个ASP NET Core应用程序,该应用程序能够在运行时以JSON格式更新配置。在配置更新期间,当前正在运行的会话应继续使用先前的配置选项。更新配置后,必须更新/替换使用的对象。
必须对配置进行反序列化,不得从控制器直接访问IConfiguration对象。应该检查读取值的正确性,如果不存在,则应将它们替换为默认值。该实现应在Docker容器中工作。
经典配置工作
GitHub:ConfigurationTemplate_1
该项目基于ASP NET Core MVC模板。JsonConfigurationProvider配置提供程序用于处理JSON配置文件。要添加在操作过程中重新加载应用程序配置的功能,请添加参数:“ reloadOnChange:true”。
在Startup.cs文件中,替换为:
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
上
public Startup(IConfiguration configuration)
{
var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
configuration = builder.Build();
Configuration = configuration;
}
.AddJsonFile-添加一个JSON文件,reloadOnChange:true表示更改配置文件参数后,无需重新加载应用程序即可重新加载它们。appsettings.json
文件的内容:
{
"AppSettings": {
"Parameter1": "Parameter1 ABC",
"Parameter2": "Parameter2 ABC"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
应用程序控制器将使用ServiceABC服务,而不是直接访问配置。ServiceABC是一个类,它从配置文件中获取初始值。在此示例中,ServiceABC类仅包含一个Title属性。ServiceABC.cs的
文件内容:
public class ServiceABC
{
public string Title;
public ServiceABC(string title)
{
Title = title;
}
public ServiceABC()
{ }
}
要使用ServiceABC,您需要将其作为中间件服务添加到您的应用程序中。将服务添加为AddTransient,每次访问该服务时都会使用以下表达式创建该服务:
services.AddTransient<IYourService>(o => new YourService(param));
非常适合不占用内存或资源的轻量级服务。使用IConfiguration读取Startup.cs中的配置参数,该配置使用查询字符串指示值位置的完整路径,例如:AppSettings:Parameter1。
在Startup.cs文件中,添加:
public void ConfigureServices(IServiceCollection services)
{
// "Parameter1" ServiceABC
var settingsParameter1 = Configuration["AppSettings:Parameter1"];
// "Parameter1"
services.AddScoped(s=> new ServiceABC(settingsParameter1));
//next
services.AddControllersWithViews();
}
在控制器 中使用ServiceABC服务的示例,Parameter1值将显示在html页面上。
要在控制器中使用该服务,请将其添加到构造函数文件HomeController.cs中
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly ServiceABC _serviceABC;
public HomeController(ILogger<HomeController> logger, ServiceABC serviceABC)
{
_logger = logger;
_serviceABC = serviceABC;
}
public IActionResult Index()
{
return View(_serviceABC);
}
添加服务可见性ServiceABC文件_ViewImports.cshtml
@using ConfigurationTemplate_1.Services
让我们修改Index.cshtml以在页面上显示Parameter1参数。
@model ServiceABC
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1> ASP.NET Core</h1>
<h4> </h4>
</div>
<div>
<p> ServiceABC,
Parameter1 = @Model.Title</p>
</div>
让我们启动应用程序:
结果
这种方法部分地解决了该问题。此解决方案不允许在应用程序运行时应用配置更改。该服务仅在启动时接收配置文件的值,然后仅与此实例一起使用。因此,对配置文件的后续更改不会导致应用程序的更改。
使用IConfiguration作为Singleton
GitHub:ConfigurationTemplate_2
第二个选项是将IConfiguration(作为Singleton)放入服务中。结果,可以从控制器和其他服务中调用IConfiguration。使用AddSingleton时,将创建一次服务,而使用应用程序时,调用将转到同一实例。使用此方法时要格外小心,因为可能会发生内存泄漏和多线程问题。
让我们来替换代码从前面的例子中Startup.cs有一个新的,在那里
services.AddSingleton<IConfiguration>(Configuration);
将IConfiguration作为Singleton添加到服务。
public void ConfigureServices(IServiceCollection services)
{
// IConfiguration
services.AddSingleton<IConfiguration>(Configuration);
// "ServiceABC"
services.AddScoped<ServiceABC>();
//next
services.AddControllersWithViews();
}
更改ServiceABC服务的构造函数以接受IConfiguration
public class ServiceABC
{
private readonly IConfiguration _configuration;
public string Title => _configuration["AppSettings:Parameter1"];
public ServiceABC(IConfiguration Configuration)
{
_configuration = Configuration;
}
public ServiceABC()
{ }
}
与以前的版本一样,将服务添加到构造函数中,并添加指向命名空间的链接
, HomeController.cs
ServiceABC _ViewImports.cshtml:
Index.cshtml Parameter1 .
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly ServiceABC _serviceABC;
public HomeController(ILogger<HomeController> logger, ServiceABC serviceABC)
{
_logger = logger;
_serviceABC = serviceABC;
}
public IActionResult Index()
{
return View(_serviceABC);
}
ServiceABC _ViewImports.cshtml:
@using ConfigurationTemplate_2.Services;
Index.cshtml Parameter1 .
@model ServiceABC
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1> ASP.NET Core</h1>
<h4> IConfiguration Singleton</h4>
</div>
<div>
<p>
ServiceABC,
Parameter1 = @Model.Title
</p>
</div>
让我们启动 应用程序:使用AddScoped添加到容器
的ServiceABC服务意味着将在每个页面请求上创建该类的实例。结果,将在每个http请求上创建ServiceABC类的实例,并重新加载IConfiguration配置,并应用appsettings.json中的新更改。
因此,如果在应用程序运行期间,将Parameter1参数更改为“ NEW !!!”!Parameter1 ABC”,下次访问起始页面时,将显示新的参数值。
让我们在更改appsettings.json文件后刷新页面:
结果
这种方法的缺点是手动读取每个参数。并且,如果添加了参数验证,那么将不会在更改appsettings.json文件之后执行检查,而是在每次使用ServiceABC时执行此检查,这是不必要的操作。在最佳情况下,每个文件更改后,参数仅应验证一次。
带有验证的配置反序列化(IOptions选项)
GitHub:ConfigurationTemplate_3在此处
了解选项。 此选项消除了使用ServiceABC的需要。而是使用AppSettings类,其中包含来自配置文件和ClientConfig对象的设置。更改配置后,需要初始化ClientConfig对象,因为控制器中使用现成的对象。ClientConfig是与外部系统交互的类,其代码无法更改。如果仅反序列化AppSettings类的数据,则ClientConfig
将为空。因此,有必要订阅读取的配置事件并在处理程序中初始化ClientConfig对象。
为了不以键值对的形式传输配置,而是将其作为某些类的对象,我们将使用IOptions接口。另外,与ConfigurationManager不同,IOptions允许您反序列化各个节。要创建ClientConfig对象,您将需要使用IPostConfigureOptions,该对象将在处理完所有配置后执行。 IPostConfigureOptions将在每次读取配置时执行,最近一次。
让我们创建ClientConfig.cs:
public class ClientConfig
{
private string _parameter1;
private string _parameter2;
public string Value => _parameter1 + " " + _parameter2;
public ClientConfig(ClientConfigOptions configOptions)
{
_parameter1 = configOptions.Parameter1;
_parameter2 = configOptions.Parameter2;
}
}
它将采用ClientConfigOptions对象形式的参数作为构造函数:
public class ClientConfigOptions
{
public string Parameter1;
public string Parameter2;
}
让我们创建AppSettings设置类,并在其中定义ClientConfigBuild()方法,该方法将创建ClientConfig对象。AppSettings.cs
文件:
public class AppSettings
{
public string Parameter1 { get; set; }
public string Parameter2 { get; set; }
public ClientConfig clientConfig;
public void ClientConfigBuild()
{
clientConfig = new ClientConfig(new ClientConfigOptions()
{
Parameter1 = this.Parameter1,
Parameter2 = this.Parameter2
}
);
}
}
让我们创建一个最后处理的配置处理程序。为此,必须从IPostConfigureOptions继承它。最后一个称为PostConfigure的将执行ClientConfigBuild(),它将创建ClientConfig。ConfigureAppSettingsOptions.cs
文件:
public class ConfigureAppSettingsOptions: IPostConfigureOptions<AppSettings>
{
public ConfigureAppSettingsOptions()
{ }
public void PostConfigure(string name, AppSettings options)
{
options.ClientConfigBuild();
}
}
现在仅需在Startup.cs中进行更改,更改将仅影响ConfigureServices(IServiceCollection服务)功能。
首先,让我们阅读appsettings.json中的AppSettings部分
// configure strongly typed settings objects
var appSettingsSection = Configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingsSection);
此外,对于每个请求,将创建AppSettings的副本,以便调用后处理:
services.AddScoped(sp => sp.GetService<IOptionsSnapshot<AppSettings>>().Value);
让我们将AppSettings类的后处理添加为服务:
services.AddSingleton<IPostConfigureOptions<AppSettings>, ConfigureAppSettingsOptions>();
向Startup.cs添加了代码
public void ConfigureServices(IServiceCollection services)
{
// configure strongly typed settings objects
var appSettingsSection = Configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingsSection);
services.AddScoped(sp => sp.GetService<IOptionsSnapshot<AppSettings>>().Value);
services.AddSingleton<IPostConfigureOptions<AppSettings>, ConfigureAppSettingsOptions>();
//next
services.AddControllersWithViews();
}
要访问配置,只需从控制器注入AppSettings就足够了。HomeController.cs
文件:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly AppSettings _appSettings;
public HomeController(ILogger<HomeController> logger, AppSettings appSettings)
{
_logger = logger;
_appSettings = appSettings;
}
让我们更改Index.cshtml以显示lientConfig对象的Value参数
@model AppSettings
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1> ASP.NET Core</h1>
<h4> ( IOptions)</h4>
</div>
<div>
<p>
ClientConfig,
= @Model.clientConfig.Value
</p>
</div>
让我们 启动应用程序:如果在应用程序运行期间,将Parameter1参数更改为“ NEW !!!”!Parameter1 ABC和Parameter2改为NEW!Parameter2 ABC“,那么下次您访问初始页面时,将显示新的Value属性:
结果
这种方法允许您反序列化所有配置值,而无需手动遍历参数。每个http请求都使用自己的AppSettings和lientConfig实例,从而消除了冲突情况。IPostConfigureOptions确保在重新读取所有选项后最后执行它。该解决方案的缺点是为每个请求不断创建ClientConfig实例,这是不切实际的,因为 实际上,仅应在更改配置后重新创建ClientConfig。
使用验证对配置进行反序列化(不使用IOptions)
GitHub:每次您从客户端收到请求时, 使用IPostConfigureOptions的ConfigurationTemplate_4
使用方法都会导致对象ClientConfig的创建。这还不够合理,因为每个请求都具有初始ClientConfig状态,该状态仅在更改appsettings.json配置文件时才会更改。为此,我们将放弃IPostConfigureOptions并创建一个仅在appsettings.json更改时才调用的配置处理程序,结果ClientConfig将仅创建一次,然后将为每个请求提供已创建的ClientConfig实例。
创建一个SingletonAppSettings类配置(Singleton),将从中为每个请求创建设置实例。SingletonAppSettings.cs
文件:
public class SingletonAppSettings
{
public AppSettings appSettings;
private static readonly Lazy<SingletonAppSettings> lazy = new Lazy<SingletonAppSettings>(() => new SingletonAppSettings());
private SingletonAppSettings()
{ }
public static SingletonAppSettings Instance => lazy.Value;
}
让我们回到Startup类,并添加对IServiceCollection接口的引用。
将在配置处理方法中使用
public IServiceCollection Services { get; set; }
让我们更改ConfigureServices(IServiceCollection服务)并将引用传递给IServiceCollection。Startup.cs
文件:
public void ConfigureServices(IServiceCollection services)
{
Services = services;
// AppSettings
var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();
appSettings.ClientConfigBuild();
让我们创建一个Singleton配置并将其添加到服务集合中:
SingletonAppSettings singletonAppSettings = SingletonAppSettings.Instance;
singletonAppSettings.appSettings = appSettings;
services.AddSingleton(singletonAppSettings);
让我们将AppSettings对象添加为“作用域”,并为每个请求创建Singleton的副本:
services.AddScoped(sp => sp.GetService<SingletonAppSettings>().appSettings);
完全ConfigureServices(IServiceCollection服务):
public void ConfigureServices(IServiceCollection services)
{
Services = services;
// AppSettings
var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();
appSettings.ClientConfigBuild();
SingletonAppSettings singletonAppSettings = SingletonAppSettings.Instance;
singletonAppSettings.appSettings = appSettings;
services.AddSingleton(singletonAppSettings);
services.AddScoped(sp => sp.GetService<SingletonAppSettings>().appSettings);
//next
services.AddControllersWithViews();
}
现在,在Configure(IApplicationBuilder应用程序,IWebHostEnvironment env)中添加用于配置的处理程序。令牌用于跟踪appsettings.json文件中的更改。文件更改时,OnChange是调用的函数。OnChange()配置处理程序:
ChangeToken.OnChange(() => Configuration.GetReloadToken(), onChange);
首先,我们读取appsettings.json文件并反序列化AppSettings类。然后,从服务集合中,获取对存储AppSettings对象的Singleton的引用,并将其替换为新对象。
private void onChange()
{
var newAppSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();
newAppSettings.ClientConfigBuild();
var serviceAppSettings = Services.BuildServiceProvider().GetService<SingletonAppSettings>();
serviceAppSettings.appSettings = newAppSettings;
Console.WriteLine($"AppSettings has been changed! {DateTime.Now}");
}
与先前版本(ConfigurationTemplate_3)一样,在HomeController中,我们将插入一个指向AppSettings的链接。
HomeController.cs:
Index.cshtml Value lientConfig:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly AppSettings _appSettings;
public HomeController(ILogger<HomeController> logger, AppSettings appSettings)
{
_logger = logger;
_appSettings = appSettings;
}
Index.cshtml Value lientConfig:
@model AppSettings
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1> ASP.NET Core</h1>
<h4> ( IOptions)</h4>
</div>
<div>
<p>
ClientConfig,
= @Model.clientConfig.Value
</p>
</div>
让我们
启动应用程序:选择启动模式作为控制台应用程序后,在应用程序窗口中,您将看到有关触发配置文件更改事件的消息:
以及新值:
结果
此选项比使用IPostConfigureOptions更好,因为 允许您仅在更改配置文件后才能构建对象,而不是在每次请求时都可以构建对象 结果是减少了服务器响应时间。触发令牌后,将重置令牌的状态。
添加默认值并验证配置
GitHub的:ConfigurationTemplate_5
在前面的例子,如果appsettings.json文件丢失,应用程序会抛出异常,因此,让配置文件可选,并添加默认设置。在Visula Studio中发布通过模板创建的项目应用程序时,appsettings.json文件将与所有二进制文件一起位于同一文件夹中,这在部署到Docker时不方便。文件appsettings.json已移至config /:
.AddJsonFile("config/appsettings.json")
为了能够在没有appsettings.json的情况下启动应用程序,请将optiona l参数更改为true,在这种情况下,这意味着appsettings.json的存在是可选的。Startup.cs
文件:
public Startup(IConfiguration configuration)
{
var builder = new ConfigurationBuilder()
.AddJsonFile("config/appsettings.json", optional: true, reloadOnChange: true);
configuration = builder.Build();
Configuration = configuration;
}
在处理缺少appsettings.json文件的情况下, 将public void ConfigureServices(IServiceCollection服务)添加到配置反序列化行:
var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>() ?? new AppSettings();
让我们添加基于IValidatableObject接口的配置验证。如果缺少配置参数,将使用默认值。
让我们从IValidatableObject继承AppSettings类并实现该方法:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
AppSettings.cs 文件:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
List<ValidationResult> errors = new List<ValidationResult>();
if (string.IsNullOrWhiteSpace(this.Parameter1))
{
errors.Add(new ValidationResult(" Parameter1. " +
" DefaultParameter1 ABC"));
this.Parameter1 = "DefaultParameter1 ABC";
}
if (string.IsNullOrWhiteSpace(this.Parameter2))
{
errors.Add(new ValidationResult(" Parameter2. " +
" DefaultParameter2 ABC"));
this.Parameter2 = "DefaultParameter2 ABC";
}
return errors;
}
添加一个方法来调用要从Startup类Startup.cs
文件中调用的配置检查:
private void ValidateAppSettings(AppSettings appSettings)
{
var resultsValidation = new List<ValidationResult>();
var context = new ValidationContext(appSettings);
if (!Validator.TryValidateObject(appSettings, context, resultsValidation, true))
{
resultsValidation.ForEach(
error => Console.WriteLine($" : {error.ErrorMessage}"));
}
}
让我们在ConfigureServices(IServiceCollection服务)中添加对配置验证方法的调用。如果没有appsettings.json文件,则需要使用默认值初始化AppSettings对象。Startup.cs
文件:
var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>() ?? new AppSettings();
参数检查。如果使用默认值,则将在控制台中显示一条指示该参数的消息。
//Validate
this.ValidateAppSettings(appSettings);
appSettings.ClientConfigBuild();
让我们在onChange()中更改配置检查
private void onChange()
{
var newAppSettings = Configuration.GetSection("AppSettings").Get<AppSettings>() ?? new AppSettings();
//Validate
this.ValidateAppSettings(newAppSettings);
newAppSettings.ClientConfigBuild();
var serviceAppSettings = Services.BuildServiceProvider().GetService<SingletonAppSettings>();
serviceAppSettings.appSettings = newAppSettings;
Console.WriteLine($"AppSettings has been changed! {DateTime.Now}");
}
如果您从appsettings.json文件中删除Parameter1键,则在保存文件后,控制台应用程序窗口中将出现一条有关缺少参数的消息:
结果
更改配置文件夹中配置位置的路径是一个很好的解决方案。允许您不将所有文件混合在一个堆中。config文件夹仅用于存储配置文件。通过配置验证简化了为管理员部署和配置应用程序的任务。如果将配置错误的输出添加到日志中,则管理员(如果指定了错误的参数)将收到有关该问题的准确信息,而不是因为程序员最近开始写任何异常信息:“出了点问题。”
没有理想的配置选项供您选择,这完全取决于手头的任务,每个选项各有利弊。
所有配置模板都可在此处获得。
文学: