📜 ⬆️ ⬇️

How configuration in .NET Core works

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


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. In many examples, the following code is found, 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 see 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 for searching and transforming the xml- document section.


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


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 the 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 see how the configuration works in .NET Core.


Configuration


The configuration in .NET Core is represented by an IConfiguration interface object .


 public interface IConfiguration {  string this[string key] { get; set; }  IConfigurationSection GetSection(string key);  IEnumerable<IConfigurationSection> GetChildren();  IChangeToken GetReloadToken(); } 


A 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, a json object of the form


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

will be reduced to a flat view:


 Settings:Key = I am options 

Here the key is Settings: Key , and the value 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<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath); } 


The following providers are available from the box:



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


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

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




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 has an element with the key key, the element from the JsonConfigurationProvider will be replaced with the element from the CommandLineConfigurationProvider since 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 do not 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 IConfigurationProvider and IConfigurationSource interfaces . IConfigurationSource is a new interface that we have not discussed in this article.


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

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


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


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


Create a class YamlConfigurationProvider and make it a successor to FileConfigurationProvider .


 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 instance of FileConfigurationSource , which contains the IFileProvider . IFileProvider is used to read a file, and to subscribe to a 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 count 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<object, object>; Data = new Dictionary<string, string>(); foreach (var pair in yamlObject) { FillData(String.Empty, pair); } } } } private void FillData(string prefix, KeyValuePair<object, object> 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<object, object> section: { foreach (var sectionPair in section) FillData(pair.Key.ToString(), sectionPair); break; } } } } 

To create an instance of our configuration provider, you must implement a FileConfigurationSource .


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

Here it is important to note that to initialize the properties of the base class, you must call the this.EnsureDefaults (builder) method.


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 the IConfigurationBuilder , but I immediately take out the initialization logic of the YamlConfigurationProvider to 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)); } } 

Calling the AddYaml Method
 public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .ConfigureAppConfiguration((context, builder) => { builder.AddYaml("appsettings.yaml"); }) .UseStartup<Startup>(); } 

Change tracking


The new api- configuration has the opportunity to re-read the configuration source when it changes. In this case, the application does not restart.
How it works:



Let's see how change tracking is implemented in FileConfigurationProvider .


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

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


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.


You can find the draft application with examples from this article in the repository on Github .
Share in the comments, who uses what approaches to the organization of the configuration?
Thanks for attention.


')

Source: https://habr.com/ru/post/453416/


All Articles