ASP.NET Core中的配置用例

要获取应用程序的配置,通常使用关键字(键值)访问方法。但这并不总是很方便。有时您需要在代码中使用已设置值的现成对象,并且能够在不重新启动应用程序的情况下更新值。本示例提供了一个模板,用于将配置用作ASP.NET Core应用程序的中间件。



初步建议您熟悉以下材料: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



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(),它将创建ClientConfigConfigureAppSettingsOptions.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服务)并将引用传递给IServiceCollectionStartup.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:



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


添加一个方法来调用要从StartupStartup.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文件夹仅用于存储配置文件。通过配置验证简化了为管理员部署和配置应用程序的任务。如果将配置错误的输出添加到日志中,则管理员(如果指定了错误的参数)将收到有关该问题的准确信息,而不是因为程序员最近开始写任何异常信息:“出了点问题



没有理想的配置选项供您选择,这完全取决于手头的任务,每个选项各有利弊。



所有配置模板都可在此处获得



文学:



  1. 正确的ASP.NET Core
  2. METANIT-配置。配置基础
  3. 单例设计模式C#.net核心
  4. 在.NET Core中重新加载配置
  5. 在ASP.NET Core RC2中更改文件时重新加载强类型选项
  6. 通过IOptions的ASP.NET Core应用程序配置
  7. METANIT-通过IOptions传递配置
  8. 通过IOptions的ASP.NET Core应用程序配置
  9. METANIT-模型自我验证



All Articles