File: FileConfigurationProvider.cs
Web Access
Project: src\src\libraries\Microsoft.Extensions.Configuration.FileExtensions\src\Microsoft.Extensions.Configuration.FileExtensions.csproj (Microsoft.Extensions.Configuration.FileExtensions)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Threading;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
 
namespace Microsoft.Extensions.Configuration
{
    /// <summary>
    /// Provides the base class for file-based <see cref="ConfigurationProvider"/> providers.
    /// </summary>
    public abstract class FileConfigurationProvider : ConfigurationProvider, IDisposable
    {
        private readonly IDisposable? _changeTokenRegistration;
 
        /// <summary>
        /// Initializes a new instance with the specified source.
        /// </summary>
        /// <param name="source">The source settings.</param>
        public FileConfigurationProvider(FileConfigurationSource source)
        {
            ThrowHelper.ThrowIfNull(source);
 
            Source = source;
 
            if (Source.ReloadOnChange && Source.FileProvider != null)
            {
                _changeTokenRegistration = ChangeToken.OnChange(
                    () => Source.FileProvider.Watch(Source.Path!),
                    () =>
                    {
                        Thread.Sleep(Source.ReloadDelay);
                        Load(reload: true);
                    });
            }
        }
 
        /// <summary>
        /// Gets the source settings for this provider.
        /// </summary>
        public FileConfigurationSource Source { get; }
 
        /// <summary>
        /// Generates a string representing this provider name and relevant details.
        /// </summary>
        /// <returns>The configuration name.</returns>
        public override string ToString()
            => $"{GetType().Name} for '{Source.Path}' ({(Source.Optional ? "Optional" : "Required")})";
 
        private void Load(bool reload)
        {
            IFileInfo? file = Source.FileProvider?.GetFileInfo(Source.Path ?? string.Empty);
            if (file == null || !file.Exists)
            {
                if (Source.Optional || reload) // Always optional on reload
                {
                    Data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
                }
                else
                {
                    var error = new StringBuilder(SR.Format(SR.Error_FileNotFound, Source.Path));
                    if (!string.IsNullOrEmpty(file?.PhysicalPath))
                    {
                        error.Append(SR.Format(SR.Error_ExpectedPhysicalPath, file.PhysicalPath));
                    }
                    HandleException(ExceptionDispatchInfo.Capture(new FileNotFoundException(error.ToString())));
                }
            }
            else
            {
                static Stream OpenRead(IFileInfo fileInfo)
                {
                    if (fileInfo.PhysicalPath != null)
                    {
                        // The default physical file info assumes asynchronous IO which results in unnecessary overhead
                        // especially since the configuration system is synchronous. This uses the same settings
                        // and disables async IO.
                        return new FileStream(
                            fileInfo.PhysicalPath,
                            FileMode.Open,
                            FileAccess.Read,
                            FileShare.ReadWrite,
                            bufferSize: 1,
                            FileOptions.SequentialScan);
                    }
 
                    return fileInfo.CreateReadStream();
                }
 
                using Stream stream = OpenRead(file);
                try
                {
                    Load(stream);
                }
                catch (Exception ex)
                {
                    if (reload)
                    {
                        Data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
                    }
                    var exception = new InvalidDataException(SR.Format(SR.Error_FailedToLoad, file.PhysicalPath), ex);
                    HandleException(ExceptionDispatchInfo.Capture(exception));
                }
            }
            // REVIEW: Should we raise this in the base as well / instead?
            OnReload();
        }
 
        /// <summary>
        /// Loads the contents of the file at <see cref="Path"/>.
        /// </summary>
        /// <exception cref="DirectoryNotFoundException">Optional is <c>false</c> on the source and a
        /// directory cannot be found at the specified Path.</exception>
        /// <exception cref="FileNotFoundException">Optional is <c>false</c> on the source and a
        /// file does not exist at specified Path.</exception>
        /// <exception cref="InvalidDataException">An exception was thrown by the concrete implementation of the
        /// <see cref="Load()"/> method. Use the source <see cref="FileConfigurationSource.OnLoadException"/> callback
        /// if you need more control over the exception.</exception>
        public override void Load()
        {
            Load(reload: false);
        }
 
        /// <summary>
        /// Loads this provider's data from a stream.
        /// </summary>
        /// <param name="stream">The stream to read.</param>
        public abstract void Load(Stream stream);
 
        private void HandleException(ExceptionDispatchInfo info)
        {
            bool ignoreException = false;
            if (Source.OnLoadException != null)
            {
                var exceptionContext = new FileLoadExceptionContext
                {
                    Provider = this,
                    Exception = info.SourceException
                };
                Source.OnLoadException.Invoke(exceptionContext);
                ignoreException = exceptionContext.Ignore;
            }
            if (!ignoreException)
            {
                info.Throw();
            }
        }
 
        /// <inheritdoc />
        public void Dispose() => Dispose(true);
 
        /// <summary>
        /// Disposes the provider.
        /// </summary>
        /// <param name="disposing"><c>true</c> if invoked from <see cref="IDisposable.Dispose"/>.</param>
        protected virtual void Dispose(bool disposing)
        {
            _changeTokenRegistration?.Dispose();
        }
    }
}