Basic ASP.NET configuration: adding configurations in one line of code

I've been working with Sitecore CMS for the past few years and now I'm back to “pure” ASP.Net MVC development. I like the way Sitecore CMS handles configuration files

Here all logical parts of the configuration are put into separate (small) configuration files, all files are merged at application startup, and patches are applied when attaching files. To my taste, this is a clean and clear solution.

Back to pure ASP.Net MVC core - you can add files to the config one by one. All the others will be merged into the final config. And this seems fine until you develop an application with extensive customization capabilities (like a CMS) and have hundreds of configuration files.

var builder = WebApplication.Create(args);
builder.Configuration
     .AddJsonFiles("Configs\\config1.json")
     .AddJsonFiles("Configs\\config2.json")

There are 3 types of configuration files that you can add to ASP.Net Core out of the box: .json, .xml и .ini Let's create extension methods for IConfigurationBuilder that allow you to include multiple configuration files of these types with a single line of code in Program.cs.

The basic idea is to get a list of the necessary files (by searching the directory) and add them all to the IConfigurationBuilder by using the necessary standard methods in a loop.

The logic will be roughly the same for each extension, the only difference is the method called by the linker.

  1. Let's start by creating a public static class ConfigurationExtensions (of course, any name will do). The class can be created in any project of your solution. In case it is not an ASP.Net Core project type, you may need to install additional nuget packages for publicly available methods such as Microsoft.Extensions.Configuration.Json, etc.
  2. Re-use the GetAvailableFiles() method.
  3. Add a delegate to carry the “single file” methods of IConfigurationBuilder ( AddConfigFileDelegate )

private delegate IConfigurationBuilder AddConfigFileDelegate(string filename, bool optional, bool reloadOnChange);

4. Connect the general logic for adding files to IConfigurationBuilder ( AddConfigFiles() method )

private static void AddConfigFiles(string  relativePath, string filenamePattern, AddConfigFileDelegate addConfigFile)
{
    var appAssembly = System.Reflection.Assembly.GetEntryAssembly();
    if (appAssembly != null)   
      {
           var appRoot = System.IO.Path.GetDirectoryName(appAssembly.Location);
           if (!string.IsNullOrEmpty(appRoot))
             {
                  var configsFolder = System.IO.Path.Combine(appRoot, relativePath);
                  var files = GetAvailableFiles(configsFolder, filenamePattern, includeSubdirectories: true);
                  foreach (var fileName in files)
                      {
                          addConfigFile(filename: fileName, optional: true, reloadOnChange: rtue);
                      }
             }
      }
}

5. Add public methods to extend the functionality of IConfigurationBuilder - AddJsonFiles, AddXmlFiles and AddIniFiles.filenamePattern by default

public static IConfigurationBuilder AddJsonFiles(this IConfigurationBuilder builder, string relativePath = "", string filenamePattern = "*.json"
{
     var addJsonFileDelegate = new AddConfigFileDelegate(builder.AddJsonFile);
     AddConfigFiles(relativePath, filenamePattern, AddJsonFileDelegate);
     return builder;
}

This allows us to use the extension in the following way:

builder.Configuration
     .AddJsonFiles("Configs", filenamePattern: "*.json");

All files will be added in alphabetical order. You are also allowed to filter files in directories not only by extension.

The full source code is presented below:

using Microsoft.Extensions.Configuration;

namespace Website.Extensions
{
    public static class ConfigurationExtensions
    {
        /// <summary>
        /// Lists all files in directory and subdirectories
        /// </summary>
        /// <param name="directory">Directory path to search in</param>
        /// <param name="searchPattern">File search pattern</param>
        /// <param name="includeSubdirectories"></param>
        /// <returns>List of file paths</returns>
        private static IEnumerable<string> GetAvailableFiles(string directory, string searchPattern, bool includeSubdirectories = true)
        {
            var result = new List<string>();
            try
            {
                if (includeSubdirectories)
                {
                    var subDirs = System.IO.Directory.GetDirectories(directory);
                    foreach (var subDir in subDirs)
                    {
                        var subDirFiles = GetAvailableFiles(subDir, searchPattern, includeSubdirectories);
                        result.AddRange(subDirFiles);
                    }
                }
                var dirFiles = System.IO.Directory.GetFiles(directory, searchPattern, SearchOption.TopDirectoryOnly);
                result.AddRange(dirFiles);
            }
            catch (UnauthorizedAccessException)
            {
                // Unable to list directory
            }
            return result;
        }

        /// <summary>
        /// Delegate to pass one of IConfigurationBuilder.AddJsonFile, IConfigurationBuilder.AddXmlFile, IConfigurationBuilder.AddIniFile to AddConfigFiles method
        /// </summary>
        private delegate IConfigurationBuilder AddConfigFileDelegate(string filename, bool optional, bool reloadOnChange);

        /// <summary>
        /// Private method with shared logic of config adding
        /// </summary>
        private static void AddConfigFiles(string relativePath, string filenamePattern, AddConfigFileDelegate addConfigFile)
        {
            var appAssembly = System.Reflection.Assembly.GetEntryAssembly();
            if (appAssembly != null)
            {
                var appRoot = System.IO.Path.GetDirectoryName(appAssembly.Location);
                if (!string.IsNullOrEmpty(appRoot))
                {
                    var configsFolder = System.IO.Path.Combine(appRoot, relativePath);
                    var files = GetAvailableFiles(configsFolder, filenamePattern, includeSubdirectories: true);
                    foreach (var fileName in files)
                    {
                        addConfigFile(filename: fileName, optional: true, reloadOnChange: true);
                    }
                }
            }
        }

        /// <summary>
        /// Adds all config files to ConfigurationBuilder as JSON config source
        /// </summary>
        /// <param name="relativePath">Directory to find. Root app directory if null or empty.</param>
        /// <param name="filenamePattern">Filename pattern to search.</param>
        public static IConfigurationBuilder AddJsonFiles(this IConfigurationBuilder builder, string relativePath = "", string filenamePattern = "*.json")
        {
            var addJsonFileDelegate = new AddConfigFileDelegate(builder.AddJsonFile);
            AddConfigFiles(relativePath, filenamePattern, addJsonFileDelegate);
            return builder;
        }

        /// <summary>
        /// Adds all config files to ConfigurationBuilder as XML config source
        /// </summary>
        /// <param name="relativePath">Directory to find. Root app directory if null or empty.</param>
        /// <param name="filenamePattern">Filename pattern to search.</param>
        public static IConfigurationBuilder AddXmlFiles(this IConfigurationBuilder builder, string relativePath = "", string filenamePattern = "*.xml")
        {
            var addXmlFileDelegate = new AddConfigFileDelegate(builder.AddXmlFile);
            AddConfigFiles(relativePath, filenamePattern, addXmlFileDelegate);
            return builder;
        }

        /// <summary>
        /// Adds all config files to ConfigurationBuilder as INI config source
        /// </summary>
        /// <param name="relativePath">Directory to find. Root app directory if null or empty.</param>
        /// <param name="filenamePattern">Filename pattern to search.</param>
        public static IConfigurationBuilder AddIniFiles(this IConfigurationBuilder builder, string relativePath = "", string filenamePattern = "*.ini")
        {
            var addIniFileDelegate = new AddConfigFileDelegate(builder.AddIniFile);
            AddConfigFiles(relativePath, filenamePattern, addIniFileDelegate);
            return builder;
        }
    }
}