File: RestoreCommand\LockFileBuilderCache.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Commands\NuGet.Commands.csproj (NuGet.Commands)
// 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.

#nullable disable

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using NuGet.ContentModel;
using NuGet.Frameworks;
using NuGet.LibraryModel;
using NuGet.ProjectModel;
using NuGet.Repositories;
using NuGet.Shared;
using NuGet.Versioning;

namespace NuGet.Commands
{

    /// <summary>
    /// Cache objects used for building the lock file.
    /// </summary>
    public class LockFileBuilderCache
    {
        // Package files
        private readonly ConcurrentDictionary<(string Id, NuGetVersion Version, string sha512), ContentItemCollection> _contentItems
            = new();

        // OrderedCriteria is stored per target graph + override framework.
        private readonly ConcurrentDictionary<CriteriaKey, List<(List<SelectionCriteria>, bool)>> _criteriaSets =
            new();

        private readonly ConcurrentDictionary<(CriteriaKey, string path, string aliases, LibraryIncludeFlags, int dependencyCount), Lazy<(LockFileTargetLibrary, bool, NuGetFramework, NuGetFramework)>> _lockFileTargetLibraryCache =
            new();

        /// <summary>
        /// Get ordered selection criteria.
        /// <paramref name="graph">RestoreTargetGraph to be used. Must not be null.</paramref>
        /// <paramref name="framework">Framework to be used. Must not be null.</paramref>
        /// </summary>
        /// <remarks>
        /// For performance reasons(detecting AssetTargetFallback warnings), this is not used in the current restore code. <see cref="GetLabeledSelectionCriteria(RestoreTargetGraph, NuGetFramework)"/> is used instead.
        /// This method is not being marked as obsolete despite being unused in the NuGet product, as at this point there's no reason for the replacement method needs to be public.
        /// </remarks>
        public List<List<SelectionCriteria>> GetSelectionCriteria(RestoreTargetGraph graph, NuGetFramework framework)
        {
            _ = graph ?? throw new ArgumentNullException(nameof(graph));
            _ = framework ?? throw new ArgumentNullException(nameof(framework));
            // Criteria are unique on graph and framework override.
            var key = new CriteriaKey(graph.TargetGraphName, framework);
            List<(List<SelectionCriteria> selectionCriterias, bool fallbackUsed)> result = _criteriaSets.GetOrAdd(key, _ => LockFileUtils.CreateOrderedCriteriaSets(graph.Conventions, framework, runtimeIdentifier: graph.RuntimeIdentifier));
            return result.Select(e => e.selectionCriterias).ToList();
        }

        /// <summary>
        /// Get ordered selection criteria.
        /// Each boolean of the value tuple says whether the criteria itself is a fallback criteria.
        /// </summary>
        /// <paramref name="graph">RestoreTargetGraph to be used. Must not be null.</paramref>
        /// <paramref name="framework">Framework to be used. Must not be null.</paramref>
        /// <returns>Returns a list of ordered criteria, along with a boolean that says whether the criteria is generated for a fallback framework.</returns>
        /// <exception cref="ArgumentNullException">If graph or framework is null.</exception>
        /// <remarks>This method being internal is inconsistent with the rest of the methods in this class,
        /// but given that this class is only used in the current assembly it would have been the best if it was never public, but we can't turn back time.
        /// </remarks>
        internal List<(List<SelectionCriteria>, bool)> GetLabeledSelectionCriteria(RestoreTargetGraph graph, NuGetFramework framework)
        {
            _ = graph ?? throw new ArgumentNullException(nameof(graph));
            _ = framework ?? throw new ArgumentNullException(nameof(framework));
            // Criteria are unique on graph and framework override.
            var key = new CriteriaKey(graph.TargetGraphName, framework);
            return _criteriaSets.GetOrAdd(key, _ => LockFileUtils.CreateOrderedCriteriaSets(graph.Conventions, framework, runtimeIdentifier: graph.RuntimeIdentifier));
        }

        /// <summary>
        /// Get a ContentItemCollection of the package files.
        /// </summary>
        /// <remarks>Library is optional.</remarks>
        public ContentItemCollection GetContentItems(LockFileLibrary library, LocalPackageInfo package)
        {
            if (package == null)
            {
                throw new ArgumentNullException(nameof(package));
            }

            return _contentItems.GetOrAdd((package.Id, package.Version, package.Sha512), _ =>
            {
                var collection = new ContentItemCollection();

                if (library == null)
                {
                    // Read folder
                    collection.Load(package.Files);
                }
                else
                {
                    // Use existing library
                    collection.Load(library.Files);
                }

                return collection;
            });
        }

        /// <summary>
        /// Try to get a LockFileTargetLibrary from the cache.
        /// </summary>
        internal (LockFileTargetLibrary, bool, NuGetFramework, NuGetFramework) GetLockFileTargetLibrary(RestoreTargetGraph graph, NuGetFramework framework, LocalPackageInfo localPackageInfo, string aliases, LibraryIncludeFlags libraryIncludeFlags, List<LibraryDependency> dependencies, Func<(LockFileTargetLibrary, bool, NuGetFramework, NuGetFramework)> valueFactory)
        {
            // Comparing RuntimeGraph for equality is very expensive,
            // so in case of a request where the RuntimeGraph is not empty we avoid using the cache.
            if (!string.IsNullOrEmpty(graph.RuntimeIdentifier))
                return valueFactory();

            localPackageInfo = localPackageInfo ?? throw new ArgumentNullException(nameof(localPackageInfo));
            var criteriaKey = new CriteriaKey(graph.TargetGraphName, framework);
            var packagePath = localPackageInfo.ExpandedPath;
            return _lockFileTargetLibraryCache.GetOrAdd((criteriaKey, packagePath, aliases, libraryIncludeFlags, dependencies.Count),
                key => new Lazy<(LockFileTargetLibrary, bool, NuGetFramework, NuGetFramework)>(valueFactory)).Value;
        }

        private class CriteriaKey : IEquatable<CriteriaKey>
        {
            public string TargetGraphName { get; }

            public NuGetFramework Framework { get; }

            public AssetTargetFallbackFramework AssetTargetFallbackFramework { get; }

            public CriteriaKey(string targetGraphName, NuGetFramework frameworkOverride)
            {
                TargetGraphName = targetGraphName;
                if (frameworkOverride is AssetTargetFallbackFramework assetTargetFallbackFramework)
                {
                    Framework = null;
                    AssetTargetFallbackFramework = assetTargetFallbackFramework;
                }
                else
                {
                    Framework = frameworkOverride;
                    AssetTargetFallbackFramework = null;
                }
            }

            public bool Equals(CriteriaKey other)
            {
                if (ReferenceEquals(this, other))
                {
                    return true;
                }

                if (ReferenceEquals(other, null))
                {
                    return false;
                }

                return StringComparer.Ordinal.Equals(TargetGraphName, other.TargetGraphName)
                       && NuGetFramework.Comparer.Equals(Framework, other.Framework)
                       && (AssetTargetFallbackFramework == null && other.AssetTargetFallbackFramework == null ||
                           AssetTargetFallbackFramework != null && AssetTargetFallbackFramework.Equals(other.AssetTargetFallbackFramework));
            }

            public override bool Equals(object obj)
            {
                return Equals(obj as CriteriaKey);
            }

            public override int GetHashCode()
            {
                var combiner = new HashCodeCombiner();

                combiner.AddObject(StringComparer.Ordinal.GetHashCode(TargetGraphName));
                combiner.AddObject(Framework);
                combiner.AddObject(AssetTargetFallbackFramework);

                return combiner.CombinedHash;
            }
        }
    }
}