< Summary

Information
Class: ArturRios.Configuration.Loaders.ConfigurationLoader
Assembly: ArturRios.Configuration
File(s): /home/runner/work/dotnet-configuration/dotnet-configuration/src/Loaders/ConfigurationLoader.cs
Line coverage
100%
Covered lines: 52
Uncovered lines: 0
Coverable lines: 52
Total lines: 193
Line coverage: 100%
Branch coverage
95%
Covered branches: 23
Total branches: 24
Branch coverage: 95.8%
Method coverage
100%
Covered methods: 6
Fully covered methods: 6
Total methods: 6
Method coverage: 100%
Full method coverage: 100%

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()50%22100%
.ctor(...)100%11100%
.ctor(...)100%44100%
LoadEnvironment()100%44100%
LoadAppSettings()100%66100%
ResolveFile(...)100%88100%

File(s)

/home/runner/work/dotnet-configuration/dotnet-configuration/src/Loaders/ConfigurationLoader.cs

#LineLine coverage
 1using ArturRios.Configuration.Enums;
 2using DotNetEnv;
 3using Microsoft.Extensions.Configuration;
 4using Microsoft.Extensions.Logging;
 5
 6namespace ArturRios.Configuration.Loaders;
 7
 8/// <summary>
 9/// Provides helper methods to load environment variables from <c>.env</c> files and
 10/// application settings from <c>appsettings.&lt;Environment&gt;.json</c> files.
 11/// </summary>
 12/// <remarks>
 13/// <para>
 14/// By default, it looks for files under the <c>Environments</c> and <c>Settings</c> folders within the application base
 15/// If a specific environment file is not found, it falls back to the <see cref="EnvironmentType.Local"/> environment.
 16/// </para>
 17/// <para>
 18/// File names are matched without regard to case, so <c>.env.Development</c> and <c>.env.development</c> —
 19/// or <c>appsettings.Local.json</c> and <c>appsettings.local.json</c> — are equally acceptable on every
 20/// platform. Matching by exact case only would resolve on Windows and silently find nothing on Linux.
 21/// </para>
 22/// </remarks>
 23public class ConfigurationLoader
 24{
 25    /// <summary>
 26    /// Default environment name used when a specific environment file cannot be found.
 27    /// </summary>
 28    private const string DefaultEnvironmentName = nameof(EnvironmentType.Local);
 29    /// <summary>
 30    /// Default folder name that contains <c>.env</c> files.
 31    /// </summary>
 32    private const string DefaultEnvFileFolder = "Environments";
 33    /// <summary>
 34    /// Default folder name that contains <c>appsettings</c> files.
 35    /// </summary>
 36    private const string DefaultAppSettingsFolder = "Settings";
 37
 38    /// <summary>
 39    /// One console logger factory for the whole process. Creating a factory per loader leaked one provider
 40    /// per instance, since nothing ever disposed them.
 41    /// </summary>
 142    private static readonly Lazy<ILoggerFactory> FallbackLoggerFactory =
 343        new(() => LoggerFactory.Create(builder => builder.AddConsole()), isThreadSafe: true);
 44
 45    private readonly string _basePath;
 46    private readonly IConfigurationBuilder? _configurationBuilder;
 47    private readonly string _environmentName;
 48    private readonly ILogger<ConfigurationLoader> _logger;
 49
 50    /// <summary>
 51    /// Initializes a new instance of <see cref="ConfigurationLoader"/> using an existing <see cref="IConfigurationBuild
 52    /// </summary>
 53    /// <param name="configurationBuilder">The configuration builder where JSON files will be added.</param>
 54    /// <param name="environmentName">The environment name (e.g., <c>Local</c>, <c>Development</c>, <c>Staging</c>, <c>P
 55    /// <param name="basePath">Optional base path to search for files. If not provided, the current domain base director
 56    /// <param name="logger">Optional logger instance. If not provided, a shared console logger is used.</param>
 57    /// <exception cref="ArgumentNullException"><paramref name="configurationBuilder"/> is <c>null</c>.</exception>
 58    /// <exception cref="ArgumentException"><paramref name="environmentName"/> is <c>null</c> or whitespace.</exception>
 59    public ConfigurationLoader(IConfigurationBuilder configurationBuilder, string environmentName,
 60        string? basePath = null, ILogger<ConfigurationLoader>? logger = null)
 1561        : this(environmentName, basePath, logger)
 62    {
 1263        ArgumentNullException.ThrowIfNull(configurationBuilder);
 64
 1165        _configurationBuilder = configurationBuilder;
 1166    }
 67
 68    /// <summary>
 69    /// Initializes a new instance of <see cref="ConfigurationLoader"/> without a configuration builder.
 70    /// </summary>
 71    /// <param name="environmentName">The environment name (e.g., <c>Local</c>, <c>Development</c>, <c>Staging</c>, <c>P
 72    /// <param name="basePath">Optional base path to search for files. If not provided, the current domain base director
 73    /// <param name="logger">Optional logger instance. If not provided, a shared console logger is used.</param>
 74    /// <exception cref="ArgumentException"><paramref name="environmentName"/> is <c>null</c> or whitespace.</exception>
 75    /// <remarks>
 76    /// Use <see cref="LoadEnvironment"/> with this constructor. <see cref="LoadAppSettings"/> requires an <see cref="IC
 77    /// </remarks>
 2878    public ConfigurationLoader(string environmentName, string? basePath = null,
 2879        ILogger<ConfigurationLoader>? logger = null)
 80    {
 2881        ArgumentException.ThrowIfNullOrWhiteSpace(environmentName);
 82
 2283        _environmentName = environmentName;
 2284        _basePath = string.IsNullOrEmpty(basePath) ? AppDomain.CurrentDomain.BaseDirectory : basePath;
 2285        _logger = logger ?? FallbackLoggerFactory.Value.CreateLogger<ConfigurationLoader>();
 2286    }
 87
 88    /// <summary>
 89    /// Loads environment variables from a specific <c>.env</c> file based on the configured environment.
 90    /// </summary>
 91    /// <remarks>
 92    /// It looks for <c>Environments/.env.&lt;environment&gt;</c> and falls back to <c>Environments/.env.local</c> if no
 93    /// The file name is matched without regard to case.
 94    /// </remarks>
 95    public void LoadEnvironment()
 96    {
 997        var envFolder = Path.Combine(_basePath, DefaultEnvFileFolder);
 998        var envFile = ResolveFile(envFolder, $".env.{_environmentName}");
 999        var defaultEnvFile = ResolveFile(envFolder, $".env.{DefaultEnvironmentName}");
 100
 9101        if (envFile is not null)
 102        {
 3103            _logger.LogInformation("Loading variables for {EnvironmentName} environment...", _environmentName);
 104
 3105            Env.Load(envFile);
 106        }
 6107        else if (defaultEnvFile is not null)
 108        {
 4109            _logger.LogInformation(
 4110                "Could not find variables for {EnvironmentName} environment. Loading default environment {DefaultEnviron
 4111                _environmentName, DefaultEnvironmentName);
 112
 4113            Env.Load(defaultEnvFile);
 114        }
 115        else
 116        {
 2117            _logger.LogInformation("Could not find any environment variables");
 118        }
 2119    }
 120
 121    /// <summary>
 122    /// Loads JSON application settings for the configured environment into the provided <see cref="IConfigurationBuilde
 123    /// </summary>
 124    /// <exception cref="InvalidOperationException">Thrown when no <see cref="IConfigurationBuilder"/> was provided in t
 125    /// <remarks>
 126    /// It looks for <c>Settings/appsettings.&lt;environment&gt;.json</c> and falls back to <c>Settings/appsettings.loca
 127    /// The file name is matched without regard to case.
 128    /// </remarks>
 129    public void LoadAppSettings()
 130    {
 12131        if (_configurationBuilder is null)
 132        {
 1133            throw new InvalidOperationException(
 1134                "Cannot load appsettings.json if configuration builder is not provided on constructor");
 135        }
 136
 11137        var settingsFolder = Path.Combine(_basePath, DefaultAppSettingsFolder);
 11138        var envSettingsFile = ResolveFile(settingsFolder, $"appsettings.{_environmentName}.json");
 11139        var defaultSettingsFile = ResolveFile(settingsFolder, $"appsettings.{DefaultEnvironmentName}.json");
 140
 11141        if (envSettingsFile is not null)
 142        {
 5143            _logger.LogInformation("Loading app settings for {EnvironmentName} environment...", _environmentName);
 144
 5145            _configurationBuilder.AddJsonFile(envSettingsFile, false, true);
 146        }
 6147        else if (defaultSettingsFile is not null)
 148        {
 4149            _logger.LogInformation(
 4150                "Could not find app settings for {EnvironmentName} environment. Loading default environment {DefaultEnvi
 4151                _environmentName, DefaultEnvironmentName);
 152
 4153            _configurationBuilder.AddJsonFile(defaultSettingsFile, false, true);
 154        }
 155        else
 156        {
 2157            _logger.LogInformation("Could not find any app settings");
 158        }
 2159    }
 160
 161    /// <summary>
 162    /// Finds <paramref name="fileName"/> inside <paramref name="folder"/>, ignoring case.
 163    /// </summary>
 164    /// <returns>The full path of the matching file, or <c>null</c> when the folder or the file is absent.</returns>
 165    /// <remarks>
 166    /// The exact-case path is probed first, which is the common case and costs a single stat call. Only when
 167    /// that misses is the directory enumerated, so a case-insensitive file system never pays for the scan.
 168    /// </remarks>
 169    private static string? ResolveFile(string folder, string fileName)
 170    {
 40171        var exact = Path.Combine(folder, fileName);
 172
 40173        if (File.Exists(exact))
 174        {
 8175            return exact;
 176        }
 177
 32178        if (!Directory.Exists(folder))
 179        {
 8180            return null;
 181        }
 182
 87183        foreach (var candidate in Directory.EnumerateFiles(folder))
 184        {
 25185            if (string.Equals(Path.GetFileName(candidate), fileName, StringComparison.OrdinalIgnoreCase))
 186            {
 11187                return candidate;
 188            }
 189        }
 190
 13191        return null;
 11192    }
 193}