Configuration

This chapter covers the configuration of an ASP.NET Core application. Every application needs configuration in one form or another because it makes it much easier to change the underlying behavior should anything happen—think about connection strings, credentials, Internet Protocol (IP) addresses, or any other kind of data that can change over time and is therefore not appropriate to be hardcoded.

Configuration can be done in many ways, some of which don't even require redeploying your application, which is a huge benefit. Luckily, .NET Core was conceived with this in mind and is also very extensible, so it can cover most scenarios, basic and advanced. It also plays nicely with other aspects, such as security and dependency injection.

Also, a very typical configuration just features switching or toggling: something is either enabled or not. .NET Core 3 introduced a new feature toggling library that is outside the main configuration framework, but it will be covered here.

After reading this chapter, you should be able to understand the following:

  • How the configuration works on the .NET Core framework
  • Which configuration sources we have available
  • How to extend it to be more helpful and match your necessities
  • Runtime host configuration
  • The new feature toggle mechanism introduced in .NET Core 3

Technical requirements

In order to implement the examples introduced in this chapter, you will need the .NET Core 3 software development kit (SDK) and some kind of text editor. Of course, Visual Studio 2019 (any edition) meets all the requirements, but you can also use Visual Studio Code, for example.

The source code can be retrieved from GitHub here: https://github.com/PacktPublishing/Modern-Web-Development-with-ASP.NET-Core-3-Second-Edition.

Getting started

Previous versions of .NET had a relatively simple configuration system, where all settings went into Extensible Markup Language (XML) files with the .config extension. There was a basic schema that could handle both system settings and untyped key-value pairs, but they were all strings. There was also some degree of inheritance, as some of the settings could be defined machine-wide and then overridden per application, and even in virtual applications underneath an Internet Information Services (IIS) application. It was possible to define custom sections with typed settings and complex structures by writing and registering .NET classes.

However, as convenient as this would seem, it turns out it had its limitations—namely, the following:

  • Only XML files were supported; it was not possible to have other configuration sources out of the box.
  • It was difficult to have different configuration files/configuration sections per environment (staging, quality assurance (QA), production, and more).
  • It was not possible to receive notifications when the configuration changed.
  • It was tricky to save changes.

Moreover, as dependency injection was not part of the core .NET infrastructure, there was no way to have configuration values injected into its services automatically. Let's see how .NET Core 3 helps us overcome these limitations.

Configurations in .NET Core

Realizing this, Microsoft made configuration a first-order concept in .NET Core and did so in quite a flexible, extensible way. It all starts with a builder instance; we add providers to it, and when we've finished, we just ask it to build a configuration object that will hold all the values loaded from each provider in memory.

This configuration object will be capable of returning configuration settings from any of the added providers transparently, which means that regardless of the source, we use the same syntax for querying configuration options. It will hold an in-memory representation of all the values loaded from all registered providers, and will allow you to change them, or add new entries.

The base class model for the configuration application programming interface (API) in .NET Core looks like this:

So, the provider mechanism is split into two base interfaces and their implementations, as follows:

  • IConfigurationSource is responsible for creating a concrete instance of an IConfigurationProvider; each of the available providers (coming next) implements this interface.
  • IConfigurationProvider specifies the contract for actually retrieving values, reloading, and more; the root class that implements this is ConfigurationProvider, and there's also a particular implementation that serves as the root for all file-based providers, FileConfigurationProvider.

ConfigurationBuilder itself is just a specific implementation of the IConfigurationBuilder interface, and there are no other implementations. Its contract specifies how we can add providers and build the configuration from them, as illustrated in the following code block:

var builder = new ConfigurationBuilder()     .Add(source1)
    .Add(source2);

var cfg = builder.Build();

As for the configuration itself, there are three base interfaces, as follows:

  • IConfiguration: This specifies the methods for retrieving and setting configuration sections and values, monitoring changes, and more.
  • IConfigurationRoot: This adds a method for reloading the configuration to IConfiguration and the list of providers used to build the configuration.
  • IConfigurationSection: This is a configuration section, meaning that it can be located somewhere beneath the configuration root in a location identified by a path (the keys of all of the parent sections, up to and including its own key) and a key that uniquely identifies that section in its parent.

We will shortly see the ways by which we can use the configuration values, but for now, it is worth mentioning that we can retrieve and set individual settings through the overloaded [] operator in IConfiguration, like this:

cfg["key"] = "value";
string value = cfg["key"];

This takes a string as key and returns a string as the value, and in the next sections, we will see how we can circumvent this limitation. If no entry for the given key exists, it returns null.

All keys are case-insensitive. A path is composed of a colon (:)-combined set of keys and subkeys that can be used to get to a specific value.

The .NET Core configuration has the concept of sections. We can get hold of a particular section, or even check whether it exists altogether, by running the following code:

var section = cfg.GetSection("ConnectionStrings");
var exists = section.Exists();

By convention, sections are separated by :. Getting a value from a section with a section-specific key is the same as retrieving it from the configuration root with a fully qualified key. For example, if you have a key of A:B:C, this is the same as having a key of C inside section B of section A, as illustrated in the following screenshot:

var valueFromRoot = cfg["A:B:C"];
var aSection = cfg.GetSection("A");
var bSection = aSection.GetSection("B");
var valueFromSection = bSection["C"];

For the record, the core configuration API is implemented in the Microsoft.Extensions.Configuration and Microsoft.Extensions.Configuration.Binder NuGet packages, which are automatically included by other packages, such as those of the specific providers. Let's now have a look at the available providers.

ASP.NET Core 2 and later automatically registers the IConfiguration instance in the dependency injection framework; for previous versions, you need to do this manually.

Providers

The available Microsoft configuration providers (and their NuGet packages) are as follows:

  • JavaScript Object Notation (JSON) files: Microsoft.Extensions.Configuration.Json
  • XML files: Microsoft.Extensions.Configuration.Xml
  • Initialization (INI) files: Microsoft.Extensions.Configuration.Ini
  • User secrets: Microsoft.Extensions.Configuration.UserSecrets
  • Azure Key Vault: Microsoft.Extensions.Configuration.AzureKeyVault
  • Environment variables: Microsoft.Extensions.Configuration.EnvironmentVariables
  • Command line: Microsoft.Extensions.Configuration.CommandLine
  • Memory: Microsoft.Extensions.Configuration
  • Docker secrets: Microsoft.Extensions.Configuration.DockerSecrets
Some of these are based upon the FileConfigurationProviderclass: JSON, XML, and INI.

When you reference these packages, you automatically make their extensions available. So, for example, if you want to add the JSON provider, you have two options, detailed next.

You can add a JsonConfigurationSource directly, like this:

var jsonSource = new JsonConfigurationSource { 
Path = "appsettings.json" }; builder.Add(jsonSource);

Alternatively, you can use the AddJsonFile extension method, like this:

builder.AddJsonFile("appsettings.json");

Most likely, the extension methods are what you need. As I said, you can have any number of providers at the same time, as illustrated in the following code snippet:

builder
    .AddJsonFile("appsettings.json")
    .AddEnvironmentVariables()
    .AddXmlFile("web.config");

You just need to keep in mind that if two providers return the same configuration setting, the order by which they were added matters; the result you get will come from the last provider added, as it will override the previous ones. So, for example, imagine you are adding two JSON configuration files, one that is common across all environments (development, staging, and production), and another for a specific environment; in this case, you would likely have the following:

builder
    .AddJsonFile("appsettings.json")
    .AddJsonFile($"appsettings.{env.EnvironmentName}.json");
This is so the environment-specific configuration file takes precedence.

Each provider will, of course, feature different properties for setting up; all file-based providers will require, for instance, a file path, but that doesn't make sense when we're talking about environment variables.

File-based providers

Both JSON, XML, and INI configuration sources are based on files. Therefore, their classes inherit from the FileConfigurationSource abstract base class. This class offers the following configuration properties:

  • Path: The actual, fully qualified physical path where the file is to be found; this is a required setting.
  • Optional: A Boolean flag for specifying whether the absence of the file causes a runtime error (false) or not (true); the default is false.
  • ReloadOnChange: Here, you decide whether to automatically detect changes to the source file (true) or not (false); the default is false.
  • ReloadDelay: The delay, in milliseconds, before reloading the file in the event that a change was detected (ReloadOnChange set to true); the default is 250 milliseconds.
  • OnLoadException: A delegate to be called should an error occur while parsing the source file; this is empty by default.
  • FileProvider: The file provider that actually retrieves the file; the default is an instance of PhysicalFileProvider, set with the folder of the Path property.

All of the extension methods allow you to supply values for each of these properties, except OnLoadException. You are also free to specify your own concrete implementation of IFileProvider, which you should do if you have specific needs, such as getting files from inside a ZIP file. ConfigurationBuilder has an extension method, SetBasePath, that sets a default PhysicalFileProvider pointing to a folder on your filesystem so that you can pass relative file paths to the configuration source's Path property.

If you set ReloadOnChange to true, .NET Core will start an operating system-specific file that monitors a watch on the source file; because these things come with a cost, try not to have many watches.

A typical example would be as follows:

builder
    .SetBasePath(@"C:Configuration")
    .AddJsonFile(path: "appsettings.json", optional: false, 
reloadOnChange: true) .AddJsonFile(path: $"appsettings.{env.EnvironmentName}.json",
optional: true, reloadOnChange: true);

This would result in the appsettings.json file being loaded from the C:Configuration folder (and throwing an exception if it is not present), and then loading appsettings.Development.json (this time, ignoring it if the file doesn't exist). Whenever there's a change in either file, they are reloaded and the configuration is updated.

Very important: in operating systems or filesystems where the case matters, such as Linux, make sure that the name of the file that takes the environment name (for example, appsettings.Development.json) is in the right case—otherwise, it won't be found!

If, however, we wanted to add an error handler, we need to add the configuration source manually, as follows:

var jsonSource = new JsonConfigurationSource { Path = "filename.json" };
jsonSource.OnLoadException = (x) =>
{
    if (x.Exception is FileNotFoundException ex)
    {
        Console.Out.WriteLine($"File {ex.FileName} not found");
        x.Ignore = true;
    }
};
builder.Add(jsonSource);

This way, we can prevent certain errors from crashing our application.

All file-based providers are added by an extension method with the name AddxxxFile, where xxx is the actual type—Json, Xml, or Ini—and always takes the same parameters (path, optional, and reloadOnChange).

JSON provider

We typically add a JSON configuration file using the AddJsonFile extension method. The JSON provider will load a file containing JSON contents and make its structure available for configuration, using dotted notation. A typical example is shown in the following code snippet:

{
  "ConnectionStrings": {
      "DefaultConnection": "Server=(localdb)mssqllocaldb; 
Database=aspnetcore" } }

Any valid JSON content will work. As of now, it is not possible to specify a schema. Sections are just sub-elements of the JSON content.

An example of code used to load a configuration value would be as follows:

var defaultConnection = cfg["ConnectionStrings:DefaultConnection"];

XML provider

XML is becoming less and less common, with JSON, inversely, becoming increasingly popular; however, there are still good reasons to use XML. So, we add an XML file using the AddXmlFile extension method, and as far as configuration is concerned, we need to wrap our XML contents in a settings node; the XML declaration is optional. Refer to the following example:

<settings Flag="2">
    <MySettings>
        <Option>10</Option>
    </MySettings>
</settings>

Again, as of now, it is not possible to specify a validating schema. With this provider, sections are implemented as sub-elements.

Two examples of this are as follows:

var flag = cfg["Flag"];
var option = cfg["MySettings:Option"];

INI provider

INI files are a thing of the past, but, for historical reasons, Microsoft is still supporting them (actually, Linux also makes use of INI files too). In case you're not familiar with its syntax, this is what it looks like:

[SectionA]
Option1=Value1
Option2=Value2

[SectionB]
Option1=Value3

You add INI files to the configuration through the AddIniFile extension method.

One word of advice: both XML and JSON file formats support anything that INI files do, so unless you have a very specific requirement, you're better off with either JSON or XML.

Sections in INI files just map to the intrinsic sections provided by the INI file specification.

A single example is as follows:

var optionB2 = cfg["SectionB:Option1"];

Other providers

Besides file-based providers, there are other ways to store and retrieve configuration information. Here, we list the currently available options in .NET Core.

User secrets

.NET Core introduced user secrets as a means of storing sensitive information per user. The benefit of this is that it is kept in a secure manner, out of configuration files, and is not visible by other users. A user secrets store is identified (for a given user) by userSecretsId, which the Visual Studio template initializes as a mix of a string and a globally unique identifier (GUID), such as aspnet-Web-f22b64ea-be5e-432d-abc6-0275a9c00377.

Secrets in a store can be listed, added, or removed through the dotnet executable, as illustrated in the following code snippet:

dotnet user-secrets list                 --lists all the values in the 
store dotnet user-secrets set "key" "value" --set "key" to be "value" dotnet user-secrets remove "key" --remove entry for "key" dotnet user-secrets clear --remove all entries

You will need the Microsoft.Extensions.SecretManager.Tools package. The dotnet user-secrets command will only work when in the presence of a project file that specifies the userSecretsId store ID. The AddUserSecrets extension method is what we use to add user secrets to the configuration, and it will either pick up this userSecretsIdsetting automatically, or you can provide your own at runtime, as follows:

builder.AddUserSecrets(userSecretdId: "[User Secrets Id]");

Another option is to get the user secrets ID from an assembly, in which case this needs to be decorated with the UserSecretsIdAttribute attribute, as follows:

[assembly: UserSecretsId("aspnet-Web-f22b64ea-be5e-432d-abc6-0275a9c00377")

In this case, the way to load it is demonstrated in the following code snippet:

builder.AddUserSecrets<Startup>();
Be warned: if you have more than one assembly with the same user secret ID (by mistake), the application will throw an exception when loading them.

Yet another way to specify user secrets (in ASP.NET Core 2.x) is through the .csproj file, by using a UserSecretsId element, as illustrated in the following code snippet:

<PropertyGroup>
<TargetFramework>netcoreapp2.1</TargetFramework>
<UserSecretsId>9094c8e7-0000-0000-0000-c26798dc18d2</UserSecretsId>
</PropertyGroup>

Regardless of how you specify the user secrets ID, as with all the other providers, the way to load a value is as follows:

var value = cfg["key"];

In case you are interested, you can read more about .NET Core user secrets here: https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets

Azure Key Vault

Azure Key Vault is an Azure service that you can leverage for enterprise-level secure key-value storage. The full description is outside the scope of this book, but you can read about it here: https://azure.microsoft.com/en-us/services/key-vault. Suffice to say that you add the Azure Key Vault provider through the AddAzureKeyVault extension method, as depicted in this line of code:

builder.AddAzureKeyVault(vault: "https://[Vault].vault.azure.net/",
clientId: "[Client ID]", clientSecret: "[Client Secret]");

After this, all are added to the configuration object, and you can retrieve them in the usual way.

Command line

Another very popular way to get configuration settings is the command line. Executables regularly expect information to be passed in the command line, so as to dictate what should be done or to control how it should happen.

The extension method to use is AddCommandLine, and it expects a required and an optional parameter, as follows:

builder.AddCommandLine(args: Environment.GetCommandLineArgs().Skip(1).ToArray());

The args parameter will typically come from Environment.GetCommandLineArgs(), and we take the first parameter out, as this is the entry assembly's name. If we are building our configuration object in Program.Main, we can use its args parameter too.

Now, there are several ways to specify parameters. One way is illustrated in the following code snippet:

    Key1=Value1
    --Key2=Value2
    /Key3=Value3
    --Key4 Value4
    /Key5 Value5

Here is another example:

dotnet run MyProject Key1=Value1 --Key2=Value2 /Key3=Value3 --Key4 Value4 /Key5 Value5

If the value has spaces in it, you need to wrap it in quotes ("). You can't use - (single dash), as this would be interpreted as a parameter to dotnet instead.

The optional parameter to AddCommandLine, switchMappings, is a dictionary that can be used to create new keys that will duplicate those from the command line, as follows:

var switchMappings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
     { { "--Key1", "AnotherKey" } };

builder.AddCommandLine(
args: Environment.GetCommandLineArgs().Skip(1).ToArray(),
switchMappings: switchMappings);

These keys can even have special characters in them—for example, --a:key and /some.key are valid keys.

Again, use the same syntax to retrieve their values.

Environment variables

Environment variables exist in all operating systems and can also be regarded as a source of configuration. Many tools out there, such as Docker, rely on environment variables for getting their operating context.

Adding environment variables to a .NET Core configuration is straightforward; you just need to call AddEnvironmentVariables. By default, this will bring all the existing environment variables into the configuration, but we can also specify a prefix, and filter out all variables that do not start with it, as follows:

builder.AddEnvironmentVariables(prefix: "ASPNET_");

So, this will add both ASPNET_TargetHost and ASPNET_TargetPort, but not PATH or COMPUTERNAME.

Sections are supported if you separate names with double underscores (for example, __). For example, say you have this environment variable:

ASPNETCORE__ADMINGROUP__USERS=rjperes,pm

You could access the ADMINGROUP section like this:

var group = cfg
.GetSection("ASPNETCORE")
.GetSection("ADMINGROUP");
var users = group["USERS"];

Memory

The memory provider is a convenient way of specifying values dynamically at runtime and for using dictionary objects. We add the provider with the AddInMemoryCollection extension method, as follows:

var properties = new Dictionary<string, string> { { "key", "value" } };
builder.AddInMemoryCollection(properties);

The advantage of this approach is that it is easy to populate a dictionary with whatever values we want, particularly in unit tests.

Docker

The ability to have secrets coming from Docker-stored files is relatively new in .NET Core. Basically, it will try to load text files in a specific directory inside a Docker instance as the values where the key is the filename itself. This is an actual feature of Docker, about which you can read more here: https://docs.docker.com/engine/swarm/secrets

The AddDockerSecrets extension method takes two optional parameters—the user secrets directory and whether or not this directory itself is optional; in other words, just ignore it if it's not there. This is illustrated in the following code snippet:

builder.AddDockerSecrets(secretsPath: "/var/lib/secrets", optional: true); 

It is possible to specify these two parameters plus an ignore prefix and a delegate for filtering out files by their names if we use the overload that takes a configuration object, as illustrated in the following code block:

builder.AddDockerSecrets(opt =>
{
    opt.SecretsDirectory = "/var/lib/secrets";
    opt.Optional = true;
    opt.IgnorePrefix = "ignore.";
    opt.IgnoreCondition = (filename) => !filename.Contains($".{env.EnvironmentName}.");
}); 

Here, we are filtering out both files starting with ignore., as well as those that do not contain the current environment name (for example, .Development.). Pretty cool!

Default providers

The ASP.NET Core code included in the default application templates (WebHostBuilder.CreateDefaultBuilder) registers the following providers:

  • JSON
  • Environment
  • Command line
  • User secrets

Of course, you can add new providers to the configuration builder to match your needs. Next, we will see how we can create a custom provider for specific configuration needs.

Creating a custom provider

Although we have several options for storing configuration values, you may have your own specific needs. For example, if you are using Windows, you might want to store your configuration settings in the Registry. For that, you need a custom provider. Let's see how we can build one.

First, you need to add the Microsoft.Win32.Registry NuGet package to your project. Then, we start by implementing IConfigurationSource, as follows:

public sealed class RegistryConfigurationSource : IConfigurationSource
{
    public RegistryHive Hive { get; set; } = RegistryHive.CurrentUser;

    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return new RegistryConfigurationProvider(this);
    }
}

As you can see from the preceding code block, the only configurable property is Hive, by means of which you can specify a specific Registry hive, with CurrentUser (HKEY_CURRENT_USER) being the default.

Next, we need an IConfigurationProvider implementation. Let's inherit from the ConfigurationProvider class, as this takes care of some of the basic implementations, such as reloading (which we do not support as we go directly to the source). The code can be seen here:

public sealed class RegistryConfigurationProvider : ConfigurationProvider
{
    private readonly RegistryConfigurationSource _configurationSource;

    public RegistryConfigurationProvider(
RegistryConfigurationSource configurationSource) { _configurationSource = configurationSource; } private RegistryKey GetRegistryKey(string key) { RegistryKey regKey; switch (_configurationSource.Hive) { case RegistryHive.ClassesRoot: regKey = Registry.ClassesRoot; break; case RegistryHive.CurrentConfig: regKey = Registry.CurrentConfig; break; case RegistryHive.CurrentUser: regKey = Registry.CurrentUser; break; case RegistryHive.LocalMachine: regKey = Registry.LocalMachine; break; case RegistryHive.PerformanceData: regKey = Registry.PerformanceData; break; case RegistryHive.Users: regKey = Registry.Users; break; default: throw new InvalidOperationException($"Supplied hive
{_configurationSource.Hive} is invalid."); } var parts = key.Split(''); var subKey = string.Join("", parts.Where(
(x, i) => i < parts.Length - 1)); return regKey.OpenSubKey(subKey); } public override bool TryGet(string key, out string value) { var regKey = this.GetRegistryKey(key); var parts = key.Split(''); var name = parts.Last(); var regValue = regKey.GetValue(name); value = regValue?.ToString(); return regValue != null; } public override void Set(string key, string value) { var regKey = this.GetRegistryKey(key); var parts = key.Split(''); var name = parts.Last(); regKey.SetValue(name, value); } }

This provider class leverages the Registry API to retrieve values from the Windows Registry, which, of course, will not work on non-Windows machines. The TryGet and Set methods, defined in the ConfigurationProvider class, both delegate to the private GetRegistryKey method, which retrieves a key-value pair from the Registry.

Finally, let's add a friendly extension method to make registration simpler, as follows:

public static class RegistryConfigurationExtensions
{
    public static IConfigurationBuilder AddRegistry(
this IConfigurationBuilder builder, RegistryHive hive = RegistryHive.CurrentUser) { return builder.Add(new RegistryConfigurationSource { Hive = hive }); } }

Now, you can use this provider, as follows:

builder
    .AddJsonFile("appsettings.json")
    .AddRegistry(RegistryHive.LocalMachine);

Nice and easy, don't you think? Now, let's see how we can use the configuration files for the providers that we registered.

Using configuration values

So, we've now seen how to set up configuration providers, but how exactly can we use these configuration values? Let's see in the following sections.

Getting and setting values explicitly

Remember that the .NET configuration allows you to set both reading and writing, both using the [] notation, as illustrated in the following code snippet:

var value = cfg["key"];
cfg["another.key"] = "another value";

Of course, setting a value in the configuration object does not mean that it will get persisted into any provider; the configuration is kept in memory only.

It is also possible to try to have the value converted to a specific type, as follows:

cfg["count"] = "0";
var count = cfg.GetValue<int>("count");
Don't forget that the value that you want to convert needs to be convertible from a string; in particular, it needs to have TypeConverter defined for that purpose, which all .NET Core primitive types do. The conversion will take place using the current culture.

Configuration sections

It is also possible to use configuration sections. A configuration section is specified through a colon (:), as in section:subsection. An infinite nesting of sections can be specified. But—I hear you ask—what is a configuration section, and how do we define one? Well, that depends on the configuration source you're using.

In the case of JSON, a configuration section will basically map to a complex property. Have a look at the following code snippet to view an example of this:

{
    "section-1": {
        "section-2": {
            "section-3": {
              "a-key": "value"
            }
        }
    }
}
Not all providers are capable of handling configuration sections or handle them in the same way. In XML, each section corresponds to a node; for INI files, there is a direct mapping; and for the Azure Key Vault, user secrets, memory (dictionaries), and providers, sections are specified as keys separated by colons (for example, ASPNET:Variable, MyApp:Variable, Data:Blog:ConnectionString, and more). For environment variables, they are separated by double underscores (__). The example Registry provider I showed earlier does not, however, support them.

We have a couple of sections here, as follows:

  • The root section
  • section-1
  • section-2
  • section-3

So, if we wanted to access a value for the a-key key, we would do so using the following syntax:

var aKey = cfg["section-1:section-2:section-3:a-key"];

Alternatively, we could ask for the section-3 section and get the a-key value directly from it, as illustrated in the following code snippet:

var section3 = cfg.GetSection("section-1:section-2:section-3");
var aKey = section3["a-key"];
var key = section3.Key;    //section-3
var path = section3.Path;  //section-1:section-2:section-3

A section will contain the path from where it was obtained. This is defined in the IConfigurationSection interface, which inherits from IConfiguration, thus making all of its extension methods available too.

By the way, you can ask for any configuration section and a value will always be returned, but this doesn't mean that it exists. You can use the Exists extension method to check for that possibility, as follows:

var fairyLandSection = cfg.GetSection("fairy:land");
var exists = fairyLandSection.Exists();  //false

A configuration section may have children, and we can list them using GetChildren, like this:

var section1 = cfg.GetSection("section-1");
var subSections = section1.GetChildren();  //section-2

.NET Core includes a shorthand for a typical configuration section and connection strings. This is the GetConnectionString extension method, and it basically looks for a connection string named ConnectionStrings and returns a named value from it. You can use the JSON schema introduced when we discussed the JSON provider as a reference, as follows:

var blogConnectionString = cfg.GetConnectionString("DefaultConnection");

Getting all values

It may not be that useful, but it is possible to get a list of all configuration values (together with their keys) present in a configuration object. We do this using the AsEnumerable extension method, illustrated in the following code snippet:

var keysAndValues = cfg.AsEnumerable().ToDictionary(kv => kv.Key, kv => kv.Value);

There's also a makePathsRelative parameter, which, by default, is false and can be used in a configuration section to strip out the section's key from the returned entries' keys. Say, for example, that you are working on the section-3 section. If you call AsEnumerable with makePathsRelative set to true, then the entry for a-key will appear as a-key instead of section-1:section-2:section-3:a-key.

Binding to classes

Another interesting option is to bind the current configuration to a class. The binding process will pick up any sections and their properties present in the configuration and try to map them to a .NET class. Let's say we have the following JSON configuration:

{
    "Logging": {
        "IncludeScopes": false,
        "LogLevel": {
          "Default": "Debug",
          "System": "Information",
          "Microsoft": "Information"
        }
    }
}

We also have a couple of classes, such as these ones:

public class LoggingSettings
{
    public bool IncludeScopes { get; set; }
    public LogLevelSettings LogLevel { get; set; }
}

public class LogLevelSettings { public LogLevel Default { get; set; } public LogLevel System { get; set; } public LogLevel Microsoft { get; set; } }
LogLevel comes from the Microsoft.Extensions.Logging namespace.

You can bind the two together, like this:

var settings = new LoggingSettings { LogLevel = new LogLevelSettings() };
cfg.GetSection("Logging").Bind(settings);

The values of LoggingSettings will be automatically populated from the current configuration, leaving untouched any properties of the target instance for which there are no values in the configuration. Of course, this can be done for any configuration section, so if your settings are not stored at the root level, it will still work.

Mind you, these won't be automatically refreshed whenever the underlying data changes. We will see in a moment how we can do that.

Another option is to have the configuration build and return a self-instantiated instance, as follows:

var settings = cfg.GetSection("Logging").Get<LoggingSettings>();

For this to work, the template class cannot be abstract and needs to have a public parameterless constructor defined.

Don't forget that an error will occur if—and only if—a configuration value cannot be bound, either directly as a string or through TypeConverter to the target property in the Plain Old CLR Object (POCO) class. If no such property exists, it will be silently ignored. The TypeConverter class comes from the System.ComponentModel NuGet package and namespace.

Since when using a file-based configuration, all properties are stored as strings, the providers need to know how to convert these into the target types. Fortunately, the included providers know how to do this for most types, such as the following:

  • Strings
  • Integers
  • Floating points (provided the decimal character is the same as per the current culture)
  • Booleans (true or false in any casing)
  • Dates (the format must match the current culture or be compliant Request for Comments (RFC) 3339/International Organization for Standardization (ISO) 8601)
  • Time (hh:mm:ss or RFC 3339/ISO 8601)
  • GUIDs
  • Enumerations

Injecting values

OK—so, we now know how to load configuration values from several sources, and we also know a couple of ways to ask for them explicitly. However, .NET Core relies heavily on dependency injection, so we might want to use that for configuration settings as well.

First, it should be fairly obvious that we can register the configuration object itself with the dependency injection framework, as follows:

var cfg = builder.Build();
services.AddSingleton(cfg);

Wherever we ask for an IConfigurationRoot object, we will get this one. We can also register it as the base IConfiguration, which is safe as well, although we miss the ability to reload the configuration (we will cover this in more detail later on). This is illustrated here:

services.AddSingleton<IConfiguration>(cfg);
Since version 2.0, ASP.NET Core automatically registers the configuration object (IConfiguration) with the dependency injection framework.

We might also be interested in injecting a POCO class with configuration settings. In that case, we use Configure, as follows:

services.Configure<LoggingSettings>(settings =>
{
    settings.IncludeScopes = true;
    settings.Default = LogLevel.Debug;
});

Here, we are using the Configure extension method, which allows us to specify values for a POCO class to be created at runtime whenever it is requested. Rather than doing this manually, we can ask the configuration object to do it, as follows:

services.Configure<LoggingSettings>(settings =>
{
    cfg.GetSection("Logging").Bind(settings);
});

Even better, we can pass named configuration options, as follows:

services.Configure<LoggingSettings>("Elasticsearch", settings =>
{
    this.Configuration.GetSection("Logging:Elasticsearch").Bind(settings);
});

services.Configure<LoggingSettings>("Console", settings =>
{
this.Configuration.GetSection("Logging:Console").Bind(settings);
});

In a minute, we will see how we can use these named configuration options.

We can even pass in the configuration root itself, or a sub-section of it, which is way simpler, as illustrated in the following code snippet:

services.Configure<LoggingSettings>(cfg.GetSection("Logging"));

Of course, we might as well register our POCO class with the dependency injection framework, as follows:

var cfg = builder.Build();
var settings = builder.GetSection("Logging").Get<LoggingSettings>();
services.AddSingleton(settings);

If we use the Configure method, the configuration instances will be available from the dependency injection framework as instances of IOptions<T>, where T is a template parameter of the type passed to Configure— as per this example, IOptions<LoggingSettings>.

The IOptions<T> interface specifies a Value property by which we can access the underlying instance that was passed or set in Configure. The good thing is that this is dynamically executed at runtime if—and only if—it is actually requested, meaning no binding from configuration to the POCO class will occur unless we explicitly want it.

A final note: before using Configure, we need to add support for it to the services collection as follows:

services.AddOptions();

For this, the Microsoft.Extensions.Options NuGet package will need to be added first, which will ensure that all required services are properly registered.

Retrieving named configuration options

When we register a POCO configuration by means of the Configure family of methods, essentially we are registering it to the dependency injection container as IOption<T>. This means that whenever we want to have it injected, we can just declare IOption<T>, such as IOption<LoggingSettings>. But if we want to use named configuration values, we need to use IOptionsSnapshot<T> instead. This interface exposes a nice Get method that takes as its sole parameter the named configuration setting, as follows:

public HomeController(IOptionsSnapshot<LoggingSettings> settings)
{
var elasticsearchSettings = settings.Get("Elasticsearch");
var consoleSettings = settings.Get("Console");
}

You must remember that we registered the LoggingSettings class through a call to the Configure method, which takes a name parameter.

Reloading and handling change notifications

You may remember that when we talked about the file-based providers, we mentioned the reloadOnChange parameter. This sets up a file-monitoring operation by which the operating system notifies .NET when the file's contents have changed. Even if we don't enable that, it is possible to ask the providers to reload their configuration. The IConfigurationRoot interface exposes a Reload method for just that purpose, as illustrated in the following code snippet:

var cfg = builder.Build();
cfg.Reload();

So, if we reload explicitly the configuration, we're pretty confident that when we ask for a configuration key, we will get the updated value in case the configuration has changed in the meantime. If we don't, however, the APIs we've already seen don't ensure that we get the updated version every time. For that, we can do either of the following:

  • Register a change notification callback, so as to be notified whenever the underlying file content changes
  • Inject a live snapshot of the data, whose value changes whenever the source changes too

For the first option, we need to get a handle to the reload token, and then register our callback actions in it, as follows:

var token = cfg.GetReloadToken();
token.RegisterChangeCallback(callback: (state) =>
{
    //state will be someData
    //push the changes to whoever needs it
}, state: "SomeData");

For the latter option, instead of injecting IOptions<T>, we need to use IOptionsSnapshot<T>. Just by changing this, we can be sure that the injected value will come from the current, up-to-date configuration source, and not the one that was there when the configuration object was created. Have a look at the following code snippet for an example of this:

public class HomeController : Controller
{
    private readonly LoggingSettings _settings;

    public HomeController(IOptionsSnapshot<LoggingSettings> settings)
    {
        _settings = settings.Value;
    }
}

It is safe to always use IOptionsSnapshot<T> instead of IOptions<T> as the overhead is minimal.

Running pre- and post-configuration actions

There's a new feature since ASP.NET Core 2.0: running pre- and post-configuration actions for configured types. What this means is, after all the configuration is done, and before a configured type is retrieved from dependency injection, all instances of registered classes are given a chance to execute and make modifications to the configuration. This is true for both unnamed as well as named configuration options.

For unnamed configuration options (Configure with no name parameter), there is an interface called IConfigureOptions<T>, illustrated in the following code snippet:

public class PreConfigureLoggingSettings : IConfigureOptions<LoggingSettings>
{
public void Configure(LoggingSettings options)
{
//act upon the configured instance
}
}

And, for named configuration options (Configure with the name parameter), we have IConfigureNamedOptions<T>, as illustrated in the following code snippet:

public class PreConfigureNamedLoggingSettings : IConfigureNamedOptions<LoggingSettings>
{
public void Configure(string name, LoggingSettings options)
{
//act upon the configured instance
}

public void Configure(LoggingSettings options)
{
}
}

These classes, when registered, will be fired before the delegate passed to the Configure method. The configuration is simple, as can be seen in the following code snippet:

services.ConfigureOptions<PreConfigureLoggingSettings>();
services.ConfigureOptions<PreConfigureNamedLoggingSettings>();

But there's more: besides running actions before the configuration delegate, we can also run afterward. Enter IPostConfigureOptions<T>—this time, there are no different interfaces for named versus unnamed configuration options' registrations, as illustrated in the following code snippet:

public class PostConfigureLoggingSettings : IPostConfigureOptions<LoggingSettings>
{
public void PostConfigure(string name, LoggingSettings options) { ... }
}

To finalize, each of these classes is instantiated by the dependency injection container, which means that we can use constructor injection! This works like a charm, and can be seen in the following code snippet:

public PreConfigureLoggingSettings(IConfiguration configuration) { ... }

This is true for IConfigureOptions<T>, IConfigureNamedOptions<T>, and IPostConfigureOptions<T> as well.

And now, let's see some of the changes from previous versions.

Changes from version 2.x

The big change from version 2.0 was that, as of 2.1, the configuration is done by convention—that is, the process of adding appsettings.json JSON files (generic and optional per environment) and all that is hidden from the users.

This is defined in the WebHost.CreateDefaultBuilder method. You can, however, still build your own ConfigurationBuilder and add whatever you like to it. To do this, you call the ConfigureAppConfiguration method, as described in Chapter 1, Getting Started with ASP.NET Core, and illustrated in the following code block:

Host    .CreateDefaultBuilder(args)
    .ConfigureAppConfiguration(builder =>
    {
var jsonSource = new JsonConfigurationSource { Path =
"appsettings.json" }; builder.Add(jsonSource);
})
.ConfigureWebHostDefaults(builder =>
{
builder.UseStartup<Startup>();
});

Or, if you just want to add a single entry to the configuration that is built by default (or, to the one you're modifying), you call the UseSettings extension method, as follows:

Host    .CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder =>
{
builder.UseSetting("key", "value");
builder.UseStartup<Startup>();
});

So, when the Startup class is instantiated, it will get passed an IConfiguration object that is built from the code that you put in here.

Warning: when using UseSetting, the value will be written to all registered configuration providers.

After seeing how the application configuration is done, let's see how we can do the same for the host.

Configuring the runtime host

.NET Core 3 introduced a not-so-well-known configuration mechanism that still has some use: a runtime host configuration. The idea here is that you provide configuration settings, as key-value pairs, in the .csproj file. You can retrieve them programmatically from the AppContext class. Here is an example project file:

<ProjectSdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<RuntimeHostConfigurationOptionInclude="Foo"Value="Bar"/>
</ItemGroup>
</Project>

The "Foo" setting is retrievable through a call to the GetData method of the AppContext class, as illustrated in the following code snippet:

var bar = AppContext.GetData("Foo");

If the named entry does not exist, GetData just returns null. Mind you, GetData is prototyped as returning an object, but in this case, it will return a string.

Normally, you wouldn't want to do that, but should you ever want to create or modify one entry of a runtime host configuration setting, you can do that through the application domain, as follows:

AppDomain.CurrentDomain.SetData("Foo", "ReBar");

Mind you, this is not a replacement for a well-structured and properly defined configuration. What .NET Core does is, at run and deployment time, it copies the contents of the RuntimeHostConfigurationOption sections (and some more) to a generated ${project}.runtimeconfig.json file that is placed together with the generated binary.

We'll now see a new feature of ASP.NET Core: feature toggles.

Understanding feature toggling

.NET Core 3 introduced the Microsoft.FeatureManagement.AspNetCore library, which is very handy for doing feature toggling. In a nutshell, a feature is either enabled or not, and this is configured through the configuration (any source) by a Boolean switch.

For more complex scenarios, you can define a configuration to be made available for a particular feature; this can be taken into consideration to determine whether or not it is enabled.

Feature toggling can be applied to an action method by applying the [FeatureGate] attribute with any number of feature names, as follows:

[FeatureGate("MyFeature1", "MyFeature2")]
public IActionResult FeactureEnabledAction() { ... }

When the[FeatureGate]attribute is applied to an action method and the feature is disabled, any attempts to access it will result in an HTTP 404 Not Found result. It can take any number of feature names and as well as an optional requirement type, which can be eitherAllorAny, meaning that either all features need to be enabled or at least one has to be enabled. This is illustrated in the following code snippet:

[FeatureGate(RequirementType.All, "MyFeature1", "MyFeature2")]
public IActionResult FeactureEnabledAction() { ... }

Alternatively, this can be asked for explicitly, through an instance of an injected IFeatureManager, as follows:

public HomeController(IFeatureManager featureManager)
{
_featureManager = featureManager;
}

public async Task<IActionResult> Index()
{
var isEnabled = await _featureManager.IsEnabledAsync("MyFeature");
}

Of course, you can inject IFeatureManager anywhere. An example of this can be seen in the following code snippet:

@inject IFeatureManager FeatureManager

@if (await FeatureManager.IsEnabledAsync("MyFeature")) {
<p>MyFeature is enabled!</p>
}

But another option, on a view, would be to use the <feature> tag helper, like this:

<feature name="MyFeature">
<p>MyFeature is enabled!</p>
</feature>

Similar to the [FeatureGate] attribute, you can specify multiple feature names in the name attribute, and you can also specify one of Any or All in requirement. You can also negate the value, as follows:

<feature name="MyFeature">
<p>MyFeature is enabled!</p>
</feature>
<feature name="MyFeature" negate="true">
<p>MyFeature is disabled!</p>
</feature>

This is useful—as you can see—because you can provide content for both when the feature is enabled and when it is not.

Tag helpers need to be registered—this normally happens on the _ViewImports.cshtml file, as follows:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

At the very least, we need to have the following configuration—for example—on an appsettings.json file, for a feature named MyFeature:

{
"FeatureManagement": {
"MyFeature": true
}
}

The default is always false, meaning that the feature is disabled. Any changes done at runtime to the configuration file are detected by the feature management library.

Setup is pretty straightforward—in theConfigureServicesmethod, just call theAddFeatureManagementextension method. This is what registers theIFeatureManagerinterface (plus a few others that we will see later), as follows:

services
.AddFeatureManagement()
.AddFeatureFilter<MyFeatureFilter>();

And there is another overload of AddFeatureManagement that takes as a parameter an IConfiguration object, should you wish to build your own. Next, you need to register as many feature filters as you want to use, with consecutive calls to AddFeatureFilter.

Included feature filters

The feature filters package includes the following filters:

  • PercentageFilter: This allows a certain defined percentage of items to pass.
  • TimeWindowFilter: A feature is enabled only during the defined date-and-time window.

Each of these filters has its own configuration schema—let's have a look at each.

Percentage filter

The percentage filter takes as its sole parameter—well, the percentage we're interested in. Every time it is invoked, it will return enabled approximately that percentage of times. The configuration in the appsettings.json file should look like this:

"FeatureManagement": {
"HalfTime": {
"EnabledFor": [
{
"Name": "Microsoft.Percentage",
"Parameters": {
"Value": 50
}
}
]
}
}

You can see that you declare the name of the feature gate, "HalfTime", and the percentage parameter—50, in this example.

You also declare the attribute, as follows:

[FeatureGate("HalfTime")]
public IActionResult Action() { ... }

Time window filter

This one allows a feature to be made available automatically when a certain date and time comes. A configuration for Christmas Day looks like this:

"FeatureManagement": {
"Christmas": {
"EnabledFor": [
{
"Name": "Microsoft.TimeWindow",
"Parameters": {
"Start": "25 Dec 2019 00:00:00 +00:00",
"End": "26 Dec 2019 00:00:00 +00:00"
}
}
]
}
}

Notice the format of the date and time—this is culture-agnostic. You need to declare both the start and end time, together with the name of the feature gate: "Christmas".

The feature gate declaration is illustrated in the following code snippet:

[FeatureGate("Christmas")]
public IActionResult Action() { ... }

Custom feature filters

Building a simple feature filter is straightforward—just implement IFeatureFilter, which only has a single method, as follows:

[FilterAlias("MyFeature")]
public class MyFeatureFilter : IFeatureFilter
{
public bool Evaluate(FeatureFilterEvaluationContext context)
{
//return true or false
}
}

Then, register it on ConfigureServices, like this:

services
.AddFeatureManagement()
.AddFeatureFilter<MyFeatureFilter>();

The FeatureFilterEvaluationContext class provides only two properties, as follows:

  • FeatureName (string): The name of the current feature
  • Parameters (IConfiguration): The configuration object that is used to feed the feature filter

However, we can leverage the built-in dependency injection mechanism of .NET Core and have it inject into our feature filter something such as IHttpContextAccessor, from which we can gain access to the current HTTP context, and from it to pretty much anything you need. This can be achieved as follows:

private readonly HttpContext _httpContext;

public MyFeatureFilter(IHttpContextAccessor httpContextAccessor)
{
this._httpContext = httpContextAccessor.HttpContext;
}

You are also not limited to a yes/no value from the configuration—you can have rich configuration settings. For example, let's see how we can have our own model in the configuration file— although, for the sake of simplicity, we will make this a simple one. Imagine the following simple class:

public class MySettings
{
public string A { get; set; }
public int B { get; set; }
}

We want to persist this class in a configuration file, like this:

{
"FeatureManagement":{
"MyFeature":{
"EnabledFor":[
{
"Name":"MyFeature",
"Parameters":{
"A":"AAAAA",
"B":10
}
}
]
}
}

This configuration can be read from a custom feature inside the Evaluate method, like this:

var settings = context.Parameters.Get<MySettings>();

The MySettings class is automatically deserialized from the configuration setting and made available to a .NET class.

Consistency between checks

You may notice that for some features—such as the percentage feature—if you call it twice during the same request, you may get different values, as illustrated in the following code snippet:

var isEnabled1 = await _featureManager.IsEnabledAsync("HalfTime");
var isEnabled2 = await _featureManager.IsEnabledAsync("Halftime");

In general, you want to avoid this whenever your feature either does complex calculations or some random operations, and you want to get consistent results for the duration of a request. In this case, you want to use IFeatureManagerSnapshot instead of IFeatureManager. IFeatureManagerSnapshot inherits from IFeatureManager but its implementations cache the results in the request, which means that you always get the same result. And IFeatureManagerSnapshot is also registered on the dependency injection framework, so you can use it whenever you would use IFeatureManager.

Disabled features handler

When you try to access an action method that is decorated with a feature gate that targets a feature (or features) that is disabled, then the action method is not reachable and, by default, we will get an HTTP 403 Forbidden error. However, this can be changed by applying a custom disabled features handler.

A disabled features handler is a concrete class that implements IDisabledFeaturesHandler, such as this one:

public sealed class RedirectDisabledFeatureHandler : IDisabledFeaturesHandler
{
public RedirectDisabledFeatureHandler(string url)
{
this.Url = url;
}

public string Url { get; }

public Task HandleDisabledFeatures(IEnumerable<string> features,
ActionExecutingContext context)
{
context.Result = new RedirectResult(this.Url);
return Task.CompletedTask;
}
}

This class redirects to a Uniform Resource Locator (URL) that is passed as a parameter. You register it through a call to UseDisabledFeaturesHandler, as follows:

services
.AddFeatureManagement()
.AddFeatureFilter<MyFeatureFilter>()
.UseDisabledFeaturesHandler(new
RedirectDisabledFeatureHandler("/Home/FeatureDisabled"));

You can only register one handler, and that's all it takes. Whenever we try to access an action method for which there is a feature gate defined that evaluates to false, it will be called, and the most obvious response will be to redirect to some page, as we can see in the example I gave.

In this section, we learned about a new feature of ASP.NET Core: feature toggling. This is a streamlined version of configuration that is more suitable for on/off switches and has some nice functionality associated. May you find it useful!

Summary

Because JSON is the standard nowadays, we should stick with the JSON provider and enable the reloading of the configuration upon changes. We should add the common file first, and then optional overrides for each of the different environments (beware the order in which you add each source). We learned how the default configuration of ASP.NET Core already loads JSON files, including different ones for the different environments.

We then saw how to use configuration sections to better organize the settings, and we also looked at using POCO wrappers for them.

So, this made us ponder whether we should use IOptions<T> or our own POCO classes to inject configuration values. Well, if you don't want to pollute your classes or assemblies with references to .NET Core configuration packages, you should stick to your POCO classes. We're not too worried about this, so we recommend keeping the interface wrappers.

We will use IOptionsSnapshot<T> instead of IOptions<T> so that we always get the latest version of the configuration settings.

After this, we looked at feature toggling, to quickly enable or disable features that are just on or off.

In this chapter, we saw the many ways in which we can provide configuration to an ASP.NET Core application. We learned how to build a simple provider that takes configuration from the Windows Registry. We then discussed the many ways in which we can inject configuration settings using the built-in dependency injection framework, and how to be notified of changes in the configuration sources.

Questions

After reading the chapter, you should now be able to answer the following questions:

  1. What is the root interface for retrieving configuration values?
  2. What are the built-in file-based configuration providers in .NET Core?
  3. Is it possible to bind configurations to POCO classes out of the box?
  4. What is the difference between the IOptions<T> and IOptionsSnapshot<T> interfaces?
  5. Do we need to register the configuration object explicitly in the dependency injection container?
  6. How can we have optional configuration files?
  7. Is it possible to get notifications whenever a configuration changes?
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.63.87