[From sandbox] How configuration in .NET Core works

[From sandbox] How configuration in .NET Core works


Let's postpone talking about DDD and reflection for a while. I propose to talk about a simple, about the organization of the settings of the application.


After my colleagues and I decided to switch to .NET Core, the question arose of how to organize configuration files, how to perform transformations, etc. in a new environment. Many examples contain the following code, and many use it successfully.


  public IConfiguration Configuration {get;  set;  }
 public IHostingEnvironment Environment {get;  set;  }

 public Startup (IConfiguration configuration, IHostingEnvironment environment)
 {
  Environment = environment;
  Configuration = new ConfigurationBuilder ()
  .AddJsonFile ("appsettings.json")
  .AddJsonFile ($ "appsettings. {Environment.EnvironmentName} .json")
  .Build ();
 }  

But let's understand how the configuration works, and when to use this approach, and in which to trust the developers of .NET Core. I ask under the cat.


As it was before


Like any story, this article has a beginning. One of the first questions after switching to ASP.NET Core was the transformation of configuration files.


Recall how it was before with web.config


The configuration consisted of several files. The main file was web.config , and transformations ( web.Development.config , etc.) were already applied to it, depending on the configuration of the assembly. At the same time, xml -attributes were actively used to search and transform the xml -document section.


But as we know in ASP.NET Core, the web.config file has been replaced by appsettings.json and there is no longer a familiar transformation mechanism.


What does google tell us?

The result of the search "Transformation in ASP.NET Core" in google was the following code:


  public IConfiguration Configuration {get;  set;  }
 public IHostingEnvironment Environment {get;  set;  }

 public Startup (IConfiguration configuration, IHostingEnvironment environment)
 {
  Environment = environment;
  Configuration = new ConfigurationBuilder ()
  .AddJsonFile ("appsettings.json")
  .AddJsonFile ($ "appsettings. {Environment.EnvironmentName} .json")
  .Build ();
 }  

In the Startup class constructor, we create a configuration object using ConfigurationBuilder . At the same time, we explicitly indicate which configuration sources we want to use.


And this:


  public IConfiguration Configuration {get;  set;  }
 public IHostingEnvironment Environment {get;  set;  }

 public Startup (IConfiguration configuration, IHostingEnvironment environment)
 {
  Environment = environment;
  Configuration = new ConfigurationBuilder ()
  .AddJsonFile ($ "appsettings. {Environment.EnvironmentName} .json")
  .Build ();
 }  

Depending on the environment variable, one or another configuration source is selected.


These responses are often found on SO and other less popular resources. But the feeling did not leave. that we are not going there. What if I want to use environment variables or command line arguments in the configuration? Why do I need to write this code in every project?


In search of truth, I had to climb deep into the documentation and source code. And I want to share the knowledge gained in this article.


Let's figure out how the configuration works in the .NET Core.


Configuration


A configuration in .NET Core is represented by a IConfiguration interface object.


  public interface IConfiguration
 {
  string this [string key] {get;  set;  }

  IConfigurationSection GetSection (string key);

  IEnumerable & lt; IConfigurationSection & gt;  GetChildren ();

  IChangeToken GetReloadToken ();
 }  

  • [string key] indexer that allows you to get the value of the configuration parameter by key
  • GetSection (string key) returns the configuration section that corresponds to the key
  • GetChildren () returns a set of subsections of the current configuration section
  • GetReloadToken () returns an instance of IChangeToken that you can use to receive notifications when the configuration changes

The configuration is a set of key-value pairs. When reading from a configuration source (file, environment variables), the hierarchical data is reduced to a flat structure. For example, json object of the form


  {
  "Settings": {
  "Key": "I am options"
  }
 }  

will be flattened:


  Settings: Key = I am options  

Here, the key is Settings: Key , and the value of I am options .
Configuration providers are used to populate the configuration.


Configuration Providers


The interface object is responsible for reading data from the configuration source.
IConfigurationProvider :


  public interface IConfigurationProvider
 {
  bool TryGet (string key, out string value);

  void Set (string key, string value);

  IChangeToken GetReloadToken ();

  void Load ();

  IEnumerable & lt; string & gt;  GetChildKeys (IEnumerable & lt; string & gt; earlierKeys, string parentPath);
 }  

  • TryGet (string key, out string value) allows you to get the value of the configuration parameter by key
  • Set (string key, string value) used to set the value of the configuration parameter
  • GetReloadToken () returns an IChangeToken instance that you can use to receive notifications when the configuration source changes
  • Load () method that is responsible for reading the configuration source
  • GetChildKeys (IEnumerable & lt; string & gt; earlierKeys, string parentPath) gets a list of all the keys that this configuration provider provides

The following providers are available from the box:


  • Json
  • Ini
  • Xml
  • Environment Variables
  • InMemory
  • Azure
  • Custom configuration provider

The following agreements have been adopted for the use of configuration providers.


  1. Configuration sources are read in the order in which they were listed
  2. If there are identical keys in different configuration sources (the comparison is case insensitive), the value that was added last is used.

If we create an instance of the web server using CreateDefaultBuilder , then the following configuration providers are connected by default:



  • ChainedConfigurationProvider through this provider you can get the values ​​and configuration keys that have been added by other configuration providers
  • JsonConfigurationProvider uses json files as the configuration source. As you can see, three providers of this type have been added to the list of providers. The first uses appsettings.json as the source, the second appsettings. {Environment} .json . The third reads data from secrets.json .If you build the application in the Release configuration, the third provider will not be connected, because it is not recommended to use secrets in the Production environment
  • EnvironmentVariablesConfigurationProvider gets configuration settings from environment variables
  • CommandLineConfigurationProvider allows you to add command line arguments to the configuration

Since the configuration is stored as a dictionary, it is necessary to ensure the uniqueness of the keys. By default it works like this.


If the CommandLineConfigurationProvider has an element with the key key and the JsonConfigurationProvider provider has an element with the key key, the element from JsonConfigurationProvider will be replaced with the element from CommandLineConfigurationProvider because it is registered last and has higher priority.


Recall an example from the beginning of the article
  public IConfiguration Configuration {get;  set;  }
 public IHostingEnvironment Environment {get;  set;  }

 public Startup (IConfiguration configuration, IHostingEnvironment environment)
 {
  Environment = environment;
  Configuration = new ConfigurationBuilder ()
  .AddJsonFile ("appsettings.json")
  .AddJsonFile ($ "appsettings. {Environment.EnvironmentName} .json")
  .Build ();
 }  

We don’t need to create the IConfiguration ourselves to transform the configuration files, as this is enabled by default. This approach is necessary when we want to limit the number of configuration sources.


Custom configuration provider


In order to write your configuration provider, you need to implement the interfaces IConfigurationProvider and IConfigurationSource . IConfigurationSource is a new interface that we haven’t reviewed in this article.


  public interface IConfigurationSource
 {
  IConfigurationProvider Build (IConfigurationBuilder builder);
 }  

The

interface consists of a single Build method that takes a IConfigurationBuilder parameter and returns a new instance of IConfigurationProvider .


To implement our configuration providers, we have the abstract classes ConfigurationProvider and FileConfigurationProvider . These classes have already implemented the logic of the methods TryGet , Set , GetReloadToken , GetChildKeys and it remains only to implement the method Load .


Consider an example. You need to implement a configuration read from the yaml file, and it is also necessary that we can change the configuration without restarting our application.


Create the YamlConfigurationProvider class and make it the FileConfigurationProvider successor.


  public class YamlConfigurationProvider: FileConfigurationProvider
 {
  private readonly string _filePath;

  public YamlConfigurationProvider (FileConfigurationSource source)
: base (source)
  {
  }

  public override void Load (Stream stream)
  {
  throw new NotImplementedException ();
  }
 }  

In the above code snippet, you can notice some features of the FileConfigurationProvider class. The constructor accepts an FileConfigurationSource instance that contains IFileProvider . The IFileProvider is used to read the file, and to subscribe to the file change event. You may also notice that the Load method accepts a Stream in which the configuration file is open for reading. This is a method of the FileConfigurationProvider class and is not in the IConfigurationProvider interface.


Add a simple implementation that allows you to read the yaml file.To read the file, I will use the YamlDotNet package.


Implementing YamlConfigurationProvider
  public class YamlConfigurationProvider: FileConfigurationProvider
 {
  private readonly string _filePath;

  public YamlConfigurationProvider (FileConfigurationSource source)
: base (source)
  {
  }

  public override void Load (Stream stream)
  {
  if (stream.CanSeek)
  {
  stream.Seek (0L, SeekOrigin.Begin);
  using (StreamReader streamReader = new StreamReader (stream))
  {
  var fileContent = streamReader.ReadToEnd ();
  var yamlObject = new DeserializerBuilder ()
  .Build ()
  .Deserialize (new StringReader (fileContent)) as IDictionary & lt; object, object & gt ;;

  Data = new Dictionary & lt; string, string & gt; ();

  foreach (var pair in yamlObject)
  {
  FillData (String.Empty, pair);
  }
  }
  }
  }

  private void FillData (string prefix, KeyValuePair & lt; object, object & gt; pair)
  {
  var key = String.IsNullOrEmpty (prefix)
  ?  pair.Key.ToString ()
: $ "{prefix}: {pair.Key}";

  switch (pair.Value)
  {
  case string value:
  Data.Add (key, value);
  break;

  case IDictionary & lt; object, object & gt;  section:
  {
  foreach (var sectionPair in section)
  FillData (pair.Key.ToString (), sectionPair);

  break;
  }
  }
  }
 }  

To create an instance of our configuration provider, you need to implement FileConfigurationSource .


Implementing YamlConfigurationSource
  public class YamlConfigurationSource: FileConfigurationSource
 {
  public YamlConfigurationSource (string fileName)
  {
  Path = fileName;
  ReloadOnChange = true;
  }

  public override IConfigurationProvider Build (IConfigurationBuilder builder)
  {
  this.EnsureDefaults (builder);
  return new YamlConfigurationProvider (this);
  }
 }  

It is important to note here that you need to call the this.EnsureDefaults (builder) method to initialize the properties of the base class.


To register a custom configuration provider in an application, you must add an instance of the provider to IConfigurationBuilder . You can call the Add method from IConfigurationBuilder , but I will immediately remove the YamlConfigurationProvider initialization logic

in the extension method.


Implementing YamlConfigurationExtensions
  public static class YamlConfigurationExtensions
 {
  public static IConfigurationBuilder AddYaml (
  this IConfigurationBuilder builder, string filePath)
  {
  if (builder == null)
  throw new ArgumentNullException (nameof (builder));

  if (string.IsNullOrEmpty (filePath))
  throw new ArgumentNullException (nameof (filePath));

  return builder
  .Add (new YamlConfigurationSource (filePath));
  }
 }  

Call the AddYaml method
  public class Program
 {
  public static void Main (string [] args)
  {
  CreateWebHostBuilder (args) .Build (). Run ();
  }

  public static IWebHostBuilder CreateWebHostBuilder (string [] args) = & gt;
  WebHost.CreateDefaultBuilder (args)
  .ConfigureAppConfiguration ((context, builder) = & gt;
  {
  builder.AddYaml ("appsettings.yaml");
  })
  .UseStartup & lt; Startup & gt; ();
 }  

Tracking Changes


In the new api configuration, it is possible to reread the configuration source as it changes. In this case, the application does not restart.
How it works:


  • The configuration provider tracks the configuration source change
  • If a configuration change occurs, a new IChangeToken
  • is created
  • When IChangeToken is changed, a configuration reload is called

Let's see how the tracking of changes in FileConfigurationProvider is implemented.


  ChangeToken.OnChange (
//producer
  () = & gt;  Source.FileProvider.Watch (Source.Path),
//consumer
  () = & gt;  {
  Thread.Sleep (Source.ReloadDelay);
  Load (reload: true);
  });  

Two parameters are passed to the OnChange method of the static ChangeToken class. The first parameter is a function that returns a new IChangeToken when the configuration source (in this case, the file) changes, this is the so-called producer . The second parameter is the function callback (or consumer ), which will be called when the configuration source changes.
Learn more about the ChangeToken class.

< br/>

Not all configuration providers implement change tracking. This mechanism is available for the descendants of FileConfigurationProvider and AzureKeyVaultConfigurationProvider .


Conclusion


In .NET Core, we have an easy, convenient mechanism for managing application settings. Many add-ons are available out of the box, many things are used by default.
Of course, everyone decides for himself how to use it, but I want people to know their tools.


This article only covers the basics. In addition to the basics, IOptions, post-configuration scripts, settings validation, and much more are available. But that's another story.


The application project with examples from this article can be found in the repository at Github .
Share in the comments who uses configuration approaches?
Thank you for your attention.

Source text: [From sandbox] How configuration in .NET Core works