File: Settings\SettingsLoadingContext.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Configuration\NuGet.Configuration.csproj (NuGet.Configuration)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Concurrent;
using System.IO;
using NuGet.Common;

namespace NuGet.Configuration
{
    /// <summary>
    /// Represents a cache context based on file paths when loading <see cref="SettingsFile" /> objects so that they are only read once.
    /// If the file changes on disk, it is not reloaded.
    /// </summary>
    public sealed class SettingsLoadingContext : IDisposable
    {
        private readonly ConcurrentDictionary<string, Lazy<SettingsFile>> _cache = new ConcurrentDictionary<string, Lazy<SettingsFile>>(PathUtility.GetStringComparerBasedOnOS());

        private bool _isDisposed;

        /// <summary>
        /// Occurs when a file is read.
        /// </summary>
        internal event EventHandler<string>? FileRead;

        /// <inheritdoc cref="IDisposable.Dispose" />
        public void Dispose()
        {
            if (!_isDisposed)
            {
                _isDisposed = true;

                _cache.Clear();
            }

            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Gets or creates a settings file for the specified path.
        /// </summary>
        /// <param name="filePath">The file path to create a <see cref="SettingsFile" /> object for.</param>
        /// <param name="isMachineWide">An optional value indicating whether or not the settings file is machine-wide.</param>
        /// <param name="isReadOnly">An optional value indicating whether or not the settings file is read-only.</param>
        /// <returns></returns>
        /// <exception cref="ObjectDisposedException">When the current object has been disposed.</exception>
        /// <exception cref="ArgumentNullException">When <paramref name="filePath" /> is <see langword="null" />.</exception>
        internal SettingsFile GetOrCreateSettingsFile(string filePath, bool isMachineWide = false, bool isReadOnly = false)
        {
            if (_isDisposed)
            {
                throw new ObjectDisposedException(nameof(SettingsLoadingContext));
            }

            if (filePath == null)
            {
                throw new ArgumentNullException(nameof(filePath));
            }

            if (!Path.IsPathRooted(filePath))
            {
                throw new ArgumentException("SettingsLoadingContext requires a rooted path", nameof(filePath));
            }

            Lazy<SettingsFile> settingsLazy = _cache.GetOrAdd(
                filePath,
                (key) => new Lazy<SettingsFile>(() =>
                {
                    var fileInfo = new FileInfo(key);

                    // Load the settings file, this will throw an exception if something is wrong with the file
                    var settingsFile = new SettingsFile(fileInfo.DirectoryName!, fileInfo.Name, isMachineWide, isReadOnly);

                    // Fire the FileRead event so unit tests can detect when a file was actually read versus cached
                    FileRead?.Invoke(this, fileInfo.FullName);

                    if (ConfigurationEventSource.Instance.IsEnabled()) ConfigurationEventSource.Instance.SettingsLoadingContext_FileRead(fileInfo.FullName, isMachineWide ? 1 : 0, isReadOnly ? 1 : 0);

                    return settingsFile;
                }));

            return settingsLazy.Value;
        }
    }
}