Базова конфігурація ASP.NET: додавання конфігурацій в один рядок коду

Протягом останніх кількох років я працював із Sitecore CMS і тепер повернувся до «чистої» розробки ASP.Net MVC. Мені дуже подобається, як Sitecore CMS працює з конфігураційними файлами.

Тут усі логічні частини конфігурації винесено в окремі (невеликі) конфігураційні файли, усі файли об'єднуються під час запуску додатка, патчі застосовують під час прикріплення файлів. На мій смак це чисте й зрозуміле рішення

Повертаючись до чистого ядра ASP.Net MVC — ви можете додавати файли в конфігурацію один за одним. Усі інші будуть об'єднані в остаточний конфіг. І виглядає це нормально, доти, доки ви не розробите застосунок із широкими можливостями налаштування (наприклад, CMS) і не отримаєте сотні конфігураційних файлів

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

Існує 3 типи файлів конфігурації, які ви можете додати в ASP.Net Core «з коробки»: .json , .xml та .ini . Давайте створимо методи розширення для IConfigurationBuilder, які дають змогу включати кілька файлів конфігурації цих типів одним рядком коду в Program.cs.

Основна ідея полягає у тому, щоб отримати список необхідних файлів (виконавши пошук у каталозі) і додати їх усі до IConfigurationBuilder, використовуючи необхідні стандартні методи в циклі.

Логіка буде приблизно однаковою для кожного розширення, єдина різниця полягає в методі, що викликається компоновщиком.

  1. Почнемо зі створення загальнодоступного статичного класу ConfigurationExtensions (звісно, підійде будь-яке ім'я). Клас можна створити в будь-якому проєкті вашого рішення. У разі, якщо це не тип проєкту ASP.Net Core, вам може знадобитися встановити додаткові пакети nuget для загальнодоступних методів, таких як Microsoft.Extensions.Configuration.Json тощо.
  2. Повторно використовуємо метод GetAvailableFiles().
  3. Додаємо  делегата для перенесення «однофайлових» методів  IConfigurationBuilder  ( AddConfigFileDelegate  )

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

4. Підключаємо загальну логіку додавання файлів у IConfigurationBuilder ( метод AddConfigFiles() )

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. Додаємо загальнодоступні методи для розширення функціональності IConfigurationBuilder  — AddJsonFiles, AddXmlFiles та AddIniFiles.filenamePattern за замовчуванням

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

Це дає нам змогу використовувати розширення таким чином:

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

Усі файли буде додано в алфавітному порядку. Вам також дозволено фільтрувати файли в каталогах не лише за розширенням.

Повний вихідний код представлено нижче:

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;
        }
    }
}