Configuration in ASP.Net Core

Spread the love

NOTE: The code for this article is available from the following GitHub Repo.

Quick jump to:

With ASP.Net Core, configurations are no longer based on XML files like web.config or app.config, were application assemblies, connection string, references and so on were all mixed up together.

Application configuration in ASP.Net Core is based on key-value pair, or JSON structured configuration keys and values, that are established using Configuration Providers.

ASP.Net Core provides a variety of configuration providers, ready to use:

  • Azure Key Vault
  • Command-line arguments
  • Directory files
  • Environment variables
  • In-memory .NET objects
  • Settings files
  • And custom-created providers.

Configuration Sources, Sections and Providers

The core building blocks for configuration in .Net Core are:

  • Configuration Sources: represent a configuration storage source like file or database
  • Configuration Providers: contain the logic for retrieving configuration settings from a source and providing the underlying data as key/value pairs.
  • Configuration Sections: Allows the grouping of values into named sections for easier access.

Configuration Data & hierarchy

The configuration API is can parse hierarchical configuration data by flattening it using a delimiter (“:”).
After the configuration is parsed and loaded, it can be accessed either by key, to retrieve individual value, or by section, if we need to retrieve a group of settings.
It’s important to remember is that the settings keys are case sensitive.

For example, let’s assume the following configuration data provided in a JSON structure:

{
 "AppSettings": {
  "Key1": "Value1",
  "AnotherKey": "Value2"
 },
 "SimpleKey1": "SimpleValue1",
}

When the configuration is read, unique keys are created to reflect the original structure:

  • AppSettings:Key1
  • AppSettings:AnotherKey
  • SimpleKey1

We will see how to access the data later in the post.

Manually storing flattened configuration data

Using the above JSON as reference, we can save the data already flattened as follows:

“AppSettings”:”Key1″:”Value1″
“AppSettings”:”AnotherKey”:”Value2″
“SimpleKey1″:”SimpleValue1”

or in table like format, with key-value columns:
"AppSettings":"Key1" "Value1"
"AppSettings":"AnotherKey" "Value2"
"SimpleKey1" "SimpleValue1"

Storing flattened data that contains arrays is similar, let’s look at the following JSON:

{
 "Actors": [{
   "FirstName": "Jon",
   "LastName": "Snow"
  },
  {
   "FirstName": "Sansa",
   "LastName": "Stark"
  }
 ]
}

Flattended structue will be like this:
Actors:0:FirstName
Actors:0:LastName
Actors:1:FirstName
Actors:1:LastName

Configuration Providers

ASP.Net Core offers various configuration providers out of the box and ready to use. The file providers allow the follows options:

  • Should the configuration file be optional or not.
  • Should the configuration be reloaded if the file changes.

We will review the File Configuration Provider, then create our own custom configuration provider.

File Configuration Provider

The FileConfigurationProvider is for loading configuration data from files. The standard file configuration providers that ship with .Net Core supports loading configuration data from 3 types of files: INI, JSON and XML.

INI Configuration Provider

We can read configuration data from an INI file simply by calling AddIniFile extension method:

WebHost.CreateDefaultBuilder(args).ConfigureAppConfiguration((hostingContext, config) = >{
	config.SetBasePath(Directory.GetCurrentDirectory());
	config.AddIniFile("config.ini", optional: true, reloadOnChange: true);
}).UseStartup<Startup>();

Example of INI configuration:

[section0]
key0=value
key1=value

[section1]
subsection:key=value

[section2:subsection0]
key=value

The INI Configuration Provider loads the following keys with values:

  • section0:key0
  • section0:key1
  • section1:subsection:key
  • section2:subsection0:key

JSON Configuration Provider

We can read configuration data from an JSON file by calling AddJsonFile extension method.
AddJsonFile is automatically called twice when we initialize WebHostBuilder with CreateDefaultBuilder, and loads configuration from:

  • appsettings.json – This file is read first
  • appsettings.{Environment}.json – The environment version of the file is loaded based on the IHostingEnvironment.EnvironmentName.

Since CreateDefaultBuilder also loads Environment variables, User Secrets and Command-line arguments after loading configuration from appsettings files, any configuration related to Environment variables, User Secrets and Command-line might be overridden.

public static void Main(string[] args) {
 CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
 WebHost.CreateDefaultBuilder(args)
 .ConfigureAppConfiguration((hostingContext, config) => {
  config.SetBasePath(Directory.GetCurrentDirectory());
  config.AddJsonFile(
   "config.json", optional: true, reloadOnChange: true);
 })
 .UseStartup<Startup>();

XML Configuration Provider

The XmlConfigurationProvider loads configuration from XML file key-value pairs. To use it, we need to AddXmlFile extension method.

A couple of notes to keep in mind:

  • The root node configuration is ignored when the configuration is loaded.
  • No need to specify Document Type Definition or namespaces in the XML configuration file.
public static void Main(string[] args) {
	CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) = >
    WebHost.CreateDefaultBuilder(args).ConfigureAppConfiguration((hostingContext, config) = >{

	config.SetBasePath(Directory.GetCurrentDirectory());
	config.AddXmlFile("config.xml", optional: true, reloadOnChange: true);
}).UseStartup<Startup>();

Building a custom configuration provider

There might be use cases where you need to fetch configuration from sources that are not supported out of the box by ASP.Net Core, such as a YAML file, PS file and so on.

In this section we will go through the steps needed to create our own configuration provider, and for the sake of example, let’s assume we need to load data about phone number extensions from the following tab delimited text file:

General
    Team
        Lobby 2233
        Service 2244
Facilities
    Electricians 9872
    Household 9873

First, we need to create a new class that derives from ConfigurationProvider. Since we need to read data from a file, we will need a stream object to access the data stream from the file. So instead of deriving from ConfigurationProvider, we will derive our custom class from FileConfigurationProvider.
With that in mind, we create the new class that contains a constructor and a Load() method to load all the configuration data from the source, which is in our example, a tab-delimited text file.

public class PhoneNumbersProvider : FileConfigurationProvider
    {
        public PhoneNumbersProvider(FileConfigurationSource source) : base(source)
        {
        }

        public override void Load(Stream stream)
        {
            string prefixString = string.Empty;
            int tabCount = -1;
            char tab = '\u0009';

            using (var reader = new StreamReader(stream))
            {
                while (!reader.EndOfStream)
                {
                    var line = reader.ReadLine();
                    if (ParseLine(tab, ref line, ref prefixString, ref tabCount)) continue;

                    var key = line.Replace(tab.ToString(), "").Split(" ")[0];
                    var value = line.Replace(tab.ToString(), "").Split(" ")[1];
                    Data.Add(prefixString + ":" + key, value);
                }

            }
        }

        private static bool ParseLine(char tab, ref string line, ref string prefixString, ref int tabCount)
        {
            if (line == null) return true;

            if (!line.Contains(" "))
            {
                var tabsInLine = line.Count(@char => @char == tab);
                line = line.Replace(tab.ToString(), "");
                if (tabsInLine == 0)
                {
                    prefixString = line;
                    tabCount = tabsInLine;
                    return true;
                }

                if (tabsInLine > tabCount)
                {
                    prefixString += ":" + line;
                    tabCount = tabsInLine;
                    return true;
                }

                prefixString =
                    prefixString.Remove(prefixString.LastIndexOf(":", StringComparison.InvariantCultureIgnoreCase) + 1); // 
                tabCount = tabsInLine;
            }

            return false;
        }

        
    }

Then we need to create a new class that derives from ConfigurationSource. As mentioned earlier, since we need to access to a file, we will derive from FileConfigurationSource:

public class PhoneNumbersSource: FileConfigurationSource {
	public override IConfigurationProvider Build(IConfigurationBuilder builder) {
		FileProvider = FileProvider ? ?builder.GetFileProvider();
		return new PhoneNumbersProvider(this);
	}
}

The single Build method on this interface allows implementers to create an instance of their provider and return it to the configuration sub-system.

The next step is to create an extension method, so we can plug in our new PhoneConfigurationProvider into the application configuration:

public static class PhoneNumbersExtensions {
	public static IConfigurationBuilder AddPhoneNumbersFile(this IConfigurationBuilder builder, string path) {
		return AddPhoneNumbersFile(builder, provider: null, path: path, optional: false, reloadOnChange: false);
	}

	public static IConfigurationBuilder AddPhoneNumbersFile(this IConfigurationBuilder builder, string path, bool optional) {
		return AddPhoneNumbersFile(builder, provider: null, path: path, optional: optional, reloadOnChange: false);
	}

	public static IConfigurationBuilder AddPhoneNumbersFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange) {
		return AddPhoneNumbersFile(builder, provider: null, path: path, optional: optional, reloadOnChange: reloadOnChange);
	}

	public static IConfigurationBuilder AddPhoneNumbersFile(this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange) {
		if (provider == null && Path.IsPathRooted(path)) {
			provider = new PhysicalFileProvider(Path.GetDirectoryName(path));
			path = Path.GetFileName(path);
		}
		var source = new PhoneNumbersSource {
			FileProvider = provider,
			Path = path,
			Optional = optional,
			ReloadOnChange = reloadOnChange
		};
		builder.Add(source);
		return builder;
	}
}

And that’s it! we can now the custom provider. When creating a WebHostBuilder directly, call UseConfiguration with the configuration:

var config = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddPhoneNumbersFile(@"C:\DEV\PHONES.TXT")
                .Build();

var host = new WebHostBuilder()
    .UseConfiguration(config)
    .UseKestrel()
    .UseStartup<Startup>();

Or, when building a host, we call ConfigureAppConfiguration to specify the application configuration:

public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build().Run();
    }

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((hostingContext, config) =>
        {
            config.SetBasePath(Directory.GetCurrentDirectory());
            config.AddPhoneNumbersFile(@"C:\DEV\PHONES.TXT")
        })
        .UseStartup<Startup>();

Leave a Reply

Your email address will not be published.