Configuration Use Cases in ASP.NET Core

To get the configuration of the application, it is common to use the access method by keyword (key-value). But this is not always convenient. sometimes you need to use ready-made objects in your code with values ​​already set, and with the ability to update values ​​without restarting the application. This example provides a template for using configuration as a middleware for ASP.NET Core applications.



We recommend that you familiarize yourself with the material: Metanit - Configuration , How Configuration Works in .NET Core .



Formulation of the problem



You need to implement an ASP NET Core application with the ability to update configuration in JSON format at runtime. During a configuration update, the currently running sessions should continue to work with the previous configuration options. After updating the configuration, the used objects must be updated / replaced with new ones.



The configuration must be deserialized, there must be no direct access to IConfiguration objects from controllers. The read values ​​should be checked for correctness, if they are absent, they should be replaced with default values. The implementation should work in a Docker container.



Classic configuration work



GitHub: ConfigurationTemplate_1



The project is based on the ASP NET Core MVC template. The JsonConfigurationProvider configuration provider is used to work with JSON configuration files . To add the ability to reload the application configuration during operation, add the parameter: "reloadOnChange: true".



In the Startup.cs file , replace:



public Startup(IConfiguration configuration)
 {
   Configuration = configuration;
 }


On



public Startup(IConfiguration configuration)
 {         
   var builder = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);
   configuration = builder.Build();
   Configuration = configuration;
  }


.AddJsonFile - adds a JSON file, reloadOnChange: true indicates that when the configuration file parameters are changed, they will be reloaded without the need to reload the application.



Content of the appsettings.json file :



{
  "AppSettings": {
    "Parameter1": "Parameter1 ABC",
    "Parameter2": "Parameter2 ABC"  
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}


Application controllers will use the service: ServiceABC instead of directly accessing the configuration. ServiceABC is a class that takes initial values ​​from the configuration file. In this example, the ServiceABC class contains only one Title property . ServiceABC.cs



file content :



public class ServiceABC
{
  public string Title;
  public ServiceABC(string title)
  {
     Title = title;
  }
  public ServiceABC()
  { }
}


To use ServiceABC, you need to add it as a middleware service to your application. Add the service as AddTransient, which is created every time you access it, using the expression:
services.AddTransient<IYourService>(o => new YourService(param));
Great for lightweight services that don't consume memory or resources. Reading the configuration parameters in Startup.cs is carried out using IConfiguration , which uses a query string indicating the full path of the value location, for example: AppSettings: Parameter1.



In the Startup.cs file , add:



public void ConfigureServices(IServiceCollection services)
{
  //  "Parameter1"    ServiceABC
  var settingsParameter1 = Configuration["AppSettings:Parameter1"];
  //  "Parameter1"            
  services.AddScoped(s=> new ServiceABC(settingsParameter1));
  //next
  services.AddControllersWithViews();
}


An example of using the ServiceABC service in a controller, the Parameter1 value will be displayed on the html page.



To use the service in controllers, add it to the constructor, file 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);
    }


Add service visibility ServiceABC file _ViewImports.cshtml



@using ConfigurationTemplate_1.Services


Change Index.cshtml to display options Parameter1 page.



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


Let's start the application:







Outcome



This approach partially solves the problem. This solution does not allow applying configuration changes while the application is running. the service receives the value of the configuration file only at startup, and then works only with this instance. As a result, subsequent changes to the configuration file will not result in changes to the application.



Using IConfiguration as Singleton



GitHub: ConfigurationTemplate_2 The



second option is to put the IConfiguration (as a Singleton) in services. As a result, IConfiguration can be called from controllers and other services. When using AddSingleton, the service is created once, and when using the application, the call goes to the same instance. Use this method with extreme caution, as memory leaks and multithreading problems can occur.



Let's replace the code from the previous example in Startup.cs with a new one, where

services.AddSingleton<IConfiguration>(Configuration);
adds IConfiguration as Singleton to services.



public void ConfigureServices(IServiceCollection services)
{
  //  IConfiguration     
  services.AddSingleton<IConfiguration>(Configuration);
  //  "ServiceABC"                          
  services.AddScoped<ServiceABC>();
  //next
  services.AddControllersWithViews();
}


Change the constructor of the ServiceABC service to accept the IConfiguration



public class ServiceABC
{        
  private readonly IConfiguration _configuration;
  public string Title => _configuration["AppSettings:Parameter1"];        
  public ServiceABC(IConfiguration Configuration)
    {
      _configuration = Configuration;
    }
  public ServiceABC()
    { }
}


As in the previous version, add the service to the constructor and add a link to the namespace
, 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>




Let's start the application:







The ServiceABC service added to the container using AddScoped means that an instance of the class will be created on every page request. As a result, an instance of the ServiceABC class will be created on every http request along with reloading the IConfiguration configuration , and new changes in appsettings.json will be applied.

Thus, if during the operation of the application, change the Parameter1 parameter to “NEW !!! Parameter1 ABC ”, the next time you access the start page, the new parameter value will be displayed.



Let's refresh the page after changing the appsettings.json file :







Outcome



The disadvantage of this approach is manual reading of each parameter. And if you add parameter validation, then the check will be performed not after changing the appsettings.json file, but every time you use ServiceABC , which is an unnecessary action. In the best case, parameters should be validated only once after each file change.



Configuration deserialization with validation (IOptions option)



GitHub: ConfigurationTemplate_3

Read about Options here .



This option eliminates the need to use ServiceABC . Instead, the AppSettings class is used , which contains the settings from the configuration file and the ClientConfig object . The ClientConfig object needs to be initialized after changing the configuration, because a ready-made object is used in controllers.

ClientConfig is a class that interacts with external systems, the code of which cannot be changed. If you only deserialize the data of the AppSettings class , then ClientConfigwill be null. Therefore, it is necessary to subscribe to the read configuration event and initialize the ClientConfig object in the handler .



To transfer the configuration not in the form of key-value pairs, but as objects of certain classes, we will use the IOptions interface . Additionally, IOptions, in contrast to ConfigurationManager, allows you to deserialize individual sections. To create the ClientConfig object, you will need to use IPostConfigureOptions , which is executed after all configuration has been processed. IPostConfigureOptions will be executed every time the configuration is read, most recently.



Let's create 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;
    }
}


It will accept parameters as a constructor in the form of a ClientConfigOptions object :



public class ClientConfigOptions
{
  public string Parameter1;
  public string Parameter2;
} 


Let's create the AppSettings settings class and define the ClientConfigBuild () method in it , which will create the ClientConfig object . AppSettings.cs



file :



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


Let's create a configuration handler that will be processed last. To do this, it must be inherited from IPostConfigureOptions . The last called PostConfigure will execute ClientConfigBuild () , which will create ClientConfig . ConfigureAppSettingsOptions.cs



file :



public class ConfigureAppSettingsOptions: IPostConfigureOptions<AppSettings>
{
  public ConfigureAppSettingsOptions()
    { }
  public void PostConfigure(string name, AppSettings options)
    {            
      options.ClientConfigBuild();
    }
}


Now it remains to make changes only in Startup.cs , the changes will affect only the ConfigureServices (IServiceCollection services) function .



First, let's read the AppSettings section in appsettings.json



// configure strongly typed settings objects
var appSettingsSection = Configuration.GetSection("AppSettings");
services.Configure<AppSettings>(appSettingsSection);


Further, for each request, a copy of AppSettings will be created for the possibility of calling post-processing:



services.AddScoped(sp => sp.GetService<IOptionsSnapshot<AppSettings>>().Value);


Let's add a post-processing of the AppSettings class as a service:



services.AddSingleton<IPostConfigureOptions<AppSettings>, ConfigureAppSettingsOptions>();


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


To get access to the configuration, it will be enough to simply inject AppSettings from the controller . HomeController.cs



file :



public class HomeController : Controller
{
  private readonly ILogger<HomeController> _logger;
  private readonly AppSettings _appSettings;
  public HomeController(ILogger<HomeController> logger, AppSettings appSettings)
    {
      _logger = logger;
      _appSettings = appSettings;
    }


Let's change Index.cshtml to display the Value parameter of the lientConfig object



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










Let's start the application: If during the operation of the application, change the Parameter1 parameter to “NEW !!! Parameter1 ABC "and Parameter2 to" NEW !!! Parameter2 ABC ", then the next time you access the initial page, the new Value property will be displayed :







Outcome



This approach allows you to deserialize all configuration values ​​without manually iterating over the parameters. Each http request works with its own instance of AppSettings and lientConfig, which eliminates the situation of collisions. IPostConfigureOptions ensures that it is executed last when all options have been reread. The disadvantage of this solution is the constant creation of an instance of the ClientConfig for each request, which is impractical because in fact, the ClientConfig should only be recreated after configuration changes.



Deserializing configuration with validation (without using IOptions)



GitHub: ConfigurationTemplate_4



use approach using IPostConfigureOptions leads to the creation of the object ClientConfig every time you receive a request from the client. This is not rational enough because each request works with an initial ClientConfig state, which changes only when the appsettings.json configuration file is changed. To do this, we will abandon IPostConfigureOptions and create a configuration handler that will be called only when appsettings.json changes, as a result the ClientConfig will be created only once, and then the already created ClientConfig instance will be given for each request.



Create the SingletonAppSettings classconfiguration (Singleton) from which an instance of settings will be created for each request. SingletonAppSettings.cs



file :



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


Let's go back to the Startup class and add a reference to the IServiceCollection interface .

It will be used in the configuration handling method



public IServiceCollection Services { get; set; }


Let's change ConfigureServices (IServiceCollection services) and pass a reference to IServiceCollection . Startup.cs



file :



public void ConfigureServices(IServiceCollection services)
{
  Services = services;
  //  AppSettings  
  var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>();
  appSettings.ClientConfigBuild();


Let's create a Singleton configuration and add it to the service collection:



SingletonAppSettings singletonAppSettings = SingletonAppSettings.Instance;
singletonAppSettings.appSettings = appSettings;
services.AddSingleton(singletonAppSettings);     


Let's add the AppSettings object as a Scoped, with each request a copy from the Singleton will be created:



services.AddScoped(sp => sp.GetService<SingletonAppSettings>().appSettings);


Completely ConfigureServices (IServiceCollection services) :



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


Now add a handler for configuration in Configure (IApplicationBuilder app, IWebHostEnvironment env) . A token is used to track the change in the appsettings.json file. OnChange is the function to be called when the file changes. OnChange () configuration handler :



ChangeToken.OnChange(() => Configuration.GetReloadToken(), onChange);


First, we read the appsettings.json file and deserialize the AppSettings class . Then, from the service collection, we get a reference to the Singleton that stores the AppSettings object , and replace it with a new one.



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


In the HomeController, we will inject a link to AppSettings, as in the previous version (ConfigurationTemplate_3)
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>




Let's







start the application: Having selected the launch mode as a console application, in the application window you can see a message about the triggering of the configuration file change event:







And the new values:







Outcome



This option is better than using IPostConfigureOptions because allows you to build an object only after changing the configuration file, and not on every request. The result is a reduction in server response time. After the token is triggered, the state of the token is reset.



Adding defaults and validating the configuration



GitHub: ConfigurationTemplate_5



In the previous examples, if the appsettings.json file is missing, the application will throw an exception, so let's make the configuration file optional and add the default settings. When you publish a project application created from a template in Visula Studio, the appsettings.json file will be located in the same folder along with all binaries, which is inconvenient when deploying to Docker. File appsettings.json been moved to the config / :



.AddJsonFile("config/appsettings.json")


To be able to launch the application without appsettings.json, change the optiona l parameter to true , which in this case means that the presence of appsettings.json is optional. Startup.cs



file :



public Startup(IConfiguration configuration)
{
  var builder = new ConfigurationBuilder()
     .AddJsonFile("config/appsettings.json", optional: true, reloadOnChange: true);
  configuration = builder.Build();
  Configuration = configuration;
}


Add to public void ConfigureServices (IServiceCollection services) to the configuration deserialization line the case of handling the absence of the appsettings.json file:



 var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>() ?? new AppSettings();


Let's add configuration validation based on the IValidatableObject interface . If configuration parameters are missing, the default value will be used.



Let us inherit the AppSettings class from IValidatableObject and implement the method:



public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)


AppSettings.cs file :



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


Add a method to call the configuration check to be called from the Startup class Startup.cs

file :



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


Let's add a call to the configuration validation method in ConfigureServices (IServiceCollection services). If there is no appsettings.json file, then you need to initialize the AppSettings object with default values. Startup.cs



file :



var appSettings = Configuration.GetSection("AppSettings").Get<AppSettings>() ?? new AppSettings();


Parameter check. If the default value is used, a message indicating the parameter will be displayed in the console.



 //Validate            
this.ValidateAppSettings(appSettings);            
appSettings.ClientConfigBuild();


Let's change the configuration check in 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}");
}


If you delete the Parameter1 key from the appsettings.json file , then after saving the file, a message about the absence of the parameter will appear in the console application window:







Outcome



Changing the path for the location of the configurations in the config folder is a good solution. allows you not to mix all files in one heap. The config folder is defined only for storing configuration files. Simplified the task of deploying and configuring the application for administrators through configuration validation. If you add the output of configuration errors to the log, then the administrator, if incorrect parameters are specified, will receive accurate information about the problem, and not as programmers recently began to write to any exception: "Something went wrong . "



There is no ideal option for working with the configuration, it all depends on the task at hand, each option has its pros and cons.



All configuration templates are available here .



Literature:



  1. Correct ASP.NET Core
  2. METANIT - Configuration. Configuration Basics
  3. Singleton Design Pattern C # .net core
  4. Reloading configuration in .NET core
  5. Reloading strongly typed Options on file changes in ASP.NET Core RC2
  6. ASP.NET Core Application Configuration via IOptions
  7. METANIT - Passing configuration via IOptions
  8. ASP.NET Core Application Configuration via IOptions
  9. METANIT - Model Self-Validation



All Articles