File: Utilities\ToolLocationHelper_ReferenceAssemblies.cs
Web Access
Project: Microsoft.Build.Framework.csproj (Microsoft.Build.Framework)
// 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.Text;
using System.Xml;
using Microsoft.Build.Framework;
using Microsoft.Build.Shared;
using Microsoft.Build.Shared.FileSystem;
using FrameworkNameVersioning = System.Runtime.Versioning.FrameworkName;

#nullable disable

namespace Microsoft.Build.Utilities;

public static partial class ToolLocationHelper
{
    /// <summary>
    /// Cache the results of reading the redist list so that we do not have to read the redist list over and over again to get the chains.
    /// </summary>
    private static Dictionary<string, string> s_chainedReferenceAssemblyPath;

    /// <summary>
    /// Cache the results of calling the GetPathToReferenceAssemblies so that we do not recalculate it every time we call the method
    /// </summary>
    private static Dictionary<string, IList<string>> s_cachedReferenceAssemblyPaths;

    /// <summary>
    /// Cache the display name for the TFM/FrameworkName, keyed by the target framework directory.
    /// This is generated by the "Name" attribute on the root tag of the primary matching redist list.
    /// Value is never an empty string or null: a name will be synthesized if necessary.
    /// </summary>
    private static Dictionary<string, string> s_cachedTargetFrameworkDisplayNames;

    /// <summary>
    /// Character used to separate search paths specified for MSBuildExtensionsPath* in
    /// the config file
    /// </summary>
    private static readonly char[] _separatorForFallbackSearchPaths = MSBuildConstants.SemicolonChar;

    /// <summary>
    /// Delegate to a method which takes a version enumeration and return a string path
    /// </summary>
    internal delegate string VersionToPath(TargetDotNetFrameworkVersion version);

    /// <summary>
    /// Returns the path to the reference assemblies location for the given target framework's standard libraries (i.e. mscorlib).
    /// This method will assume the requested ReferenceAssemblyRoot path will be the ProgramFiles directory specified by Environment.SpecialFolder.ProgramFiles
    /// In additon when the .NETFramework or .NET Framework targetFrameworkIdentifiers are seen and targetFrameworkVersion is 2.0, 3.0, 3.5 or 4.0 we will return the correctly chained reference assembly paths
    /// for the legacy .net frameworks. This chaining will use the existing GetPathToDotNetFrameworkReferenceAssemblies to build up the list of reference assembly paths.
    /// </summary>
    /// <param name="targetFrameworkIdentifier">Identifier being targeted</param>
    /// <param name="targetFrameworkVersion">Version being targeted</param>
    /// <param name="targetFrameworkProfile">Profile being targeted</param>
    /// <exception cref="ArgumentNullException">When the frameworkName is null</exception>
    /// <returns>Collection of reference assembly locations.</returns>
    public static string GetPathToStandardLibraries(string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile)
    {
        IList<string> referenceAssemblyDirectories = GetPathToReferenceAssemblies(targetFrameworkIdentifier, targetFrameworkVersion, targetFrameworkProfile);

        // Check each returned reference assembly directory for one containing mscorlib.dll
        // When we find it (most of the time it will be the first in the set) we'll
        // return that directory.
        foreach (string referenceAssemblyDirectory in referenceAssemblyDirectories)
        {
            if (FileSystems.Default.FileExists(Path.Combine(referenceAssemblyDirectory, "mscorlib.dll")))
            {
                // We found the framework reference assembly directory with mscorlib in it
                // that's our standard lib path, so return it, with no trailing slash.
                return FileUtilities.EnsureNoTrailingSlash(referenceAssemblyDirectory);
            }
        }

        // We didn't find a standard library path in our set, return empty.
        return string.Empty;
    }

    /// <summary>
    /// Returns the path to mscorlib and system.dll
    /// </summary>
    /// <param name="targetFrameworkIdentifier">Identifier being targeted</param>
    /// <param name="targetFrameworkVersion">Version being targeted</param>
    /// <param name="targetFrameworkProfile">Profile being targeted</param>
    /// <param name="platformTarget">What is the targeted platform, this is used to determine where we should look for the standard libraries. Note, this parameter is only used for .net frameworks less than 4.0</param>
    /// <exception cref="ArgumentNullException">When the frameworkName is null</exception>
    /// <returns>Collection of reference assembly locations.</returns>
    public static string GetPathToStandardLibraries(string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile, string platformTarget)
        => GetPathToStandardLibraries(targetFrameworkIdentifier, targetFrameworkVersion, targetFrameworkProfile, platformTarget, null);

    /// <summary>
    /// Returns the path to mscorlib and system.dll
    /// </summary>
    /// <param name="targetFrameworkIdentifier">Identifier being targeted</param>
    /// <param name="targetFrameworkVersion">Version being targeted</param>
    /// <param name="targetFrameworkProfile">Profile being targeted</param>
    /// <param name="platformTarget">What is the targeted platform, this is used to determine where we should look for the standard libraries. Note, this parameter is only used for .net frameworks less than 4.0</param>
    /// <param name="targetFrameworkRootPath">Root directory where the target framework will be looked for. Uses default path if this is null</param>
    /// <exception cref="ArgumentNullException">When the frameworkName is null</exception>
    /// <returns>Collection of reference assembly locations.</returns>
    public static string GetPathToStandardLibraries(string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile, string platformTarget, string targetFrameworkRootPath)
        => GetPathToStandardLibraries(targetFrameworkIdentifier, targetFrameworkVersion, targetFrameworkProfile, platformTarget, targetFrameworkRootPath, null);

    /// <summary>
    /// Returns the path to mscorlib and system.dll
    /// </summary>
    /// <param name="targetFrameworkIdentifier">Identifier being targeted</param>
    /// <param name="targetFrameworkVersion">Version being targeted</param>
    /// <param name="targetFrameworkProfile">Profile being targeted</param>
    /// <param name="platformTarget">What is the targeted platform, this is used to determine where we should look for the standard libraries. Note, this parameter is only used for .net frameworks less than 4.0</param>
    /// <param name="targetFrameworkRootPath">Root directory where the target framework will be looked for. Uses default path if this is null</param>
    /// <param name="targetFrameworkFallbackSearchPaths">';' separated list of paths that are looked up if the framework cannot be found in @targetFrameworkRootPath</param>
    /// <exception cref="ArgumentNullException">When the frameworkName is null</exception>
    /// <returns>Collection of reference assembly locations.</returns>
    public static string GetPathToStandardLibraries(string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile, string platformTarget, string targetFrameworkRootPath, string targetFrameworkFallbackSearchPaths)
    {
        ArgumentException.ThrowIfNullOrEmpty(targetFrameworkIdentifier);
        ArgumentException.ThrowIfNullOrEmpty(targetFrameworkVersion);

        Version frameworkVersion = ConvertTargetFrameworkVersionToVersion(targetFrameworkVersion);
        // For .net framework less than 4 the mscorlib should be found in the .net 2.0 directory
        if (targetFrameworkIdentifier.Equals(FrameworkLocationHelper.dotNetFrameworkIdentifier, StringComparison.OrdinalIgnoreCase) && frameworkVersion.Major < 4 && string.IsNullOrEmpty(targetFrameworkProfile))
        {
            // The default
            DotNetFrameworkArchitecture targetedArchitecture = DotNetFrameworkArchitecture.Current;

            if (NativeMethods.IsWindows && platformTarget != null)
            {
                // If we are a 32 bit operating system the we should always return the 32 bit directory, or we are targeting x86, arm is also 32 bit
                if (!Environment.Is64BitOperatingSystem || platformTarget.Equals("x86", StringComparison.OrdinalIgnoreCase) || platformTarget.Equals("arm", StringComparison.OrdinalIgnoreCase))
                {
                    targetedArchitecture = DotNetFrameworkArchitecture.Bitness32;
                }
                else if (platformTarget.Equals("x64", StringComparison.OrdinalIgnoreCase) || platformTarget.Equals("Itanium", StringComparison.OrdinalIgnoreCase) || platformTarget.Equals("arm64", StringComparison.OrdinalIgnoreCase))
                {
                    targetedArchitecture = DotNetFrameworkArchitecture.Bitness64;
                }
            }

            string legacyMsCorlib20Path = FrameworkLocationHelper.GetPathToDotNetFrameworkV20(targetedArchitecture);
            if (legacyMsCorlib20Path != null && FileSystems.Default.FileExists(Path.Combine(legacyMsCorlib20Path, "mscorlib.dll")))
            {
                // We found the framework reference assembly directory with mscorlib in it
                // that's our standard lib path, so return it, with no trailing slash.
                return FileUtilities.EnsureNoTrailingSlash(legacyMsCorlib20Path);
            }

            // If for some reason the 2.0 framework is not installed in its default location then maybe someone is using the ".net 4.0" reference assembly
            // location, if so then we can just use what ever version they passed in because it should be MSIL now and not bit specific.
        }

        IList<string> referenceAssemblyDirectories = GetPathToReferenceAssemblies(targetFrameworkIdentifier, targetFrameworkVersion, targetFrameworkProfile, targetFrameworkRootPath, targetFrameworkFallbackSearchPaths);
        // Check each returned reference assembly directory for one containing mscorlib.dll
        // When we find it (most of the time it will be the first in the set) we'll
        // return that directory.
        foreach (string referenceAssemblyDirectory in referenceAssemblyDirectories)
        {
            if (FileSystems.Default.FileExists(Path.Combine(referenceAssemblyDirectory, "mscorlib.dll")))
            {
                // We found the framework reference assembly directory with mscorlib in it
                // that's our standard lib path, so return it, with no trailing slash.
                return FileUtilities.EnsureNoTrailingSlash(referenceAssemblyDirectory);
            }
        }

        // We didn't find a standard library path in our set, return empty.
        return string.Empty;
    }

    /// <summary>
    /// Returns the paths to the reference assemblies location for the given target framework.
    /// This method will assume the requested ReferenceAssemblyRoot path will be the ProgramFiles directory specified by Environment.SpecialFolder.ProgramFiles
    /// In additon when the .NETFramework or .NET Framework targetFrameworkIdentifiers are seen and targetFrameworkVersion is 2.0, 3.0, 3.5 or 4.0 we will return the correctly chained reference assembly paths
    /// for the legacy .net frameworks. This chaining will use the existing GetPathToDotNetFrameworkReferenceAssemblies to build up the list of reference assembly paths.
    /// </summary>
    /// <param name="targetFrameworkIdentifier">Identifier being targeted</param>
    /// <param name="targetFrameworkVersion">Version being targeted</param>
    /// <param name="targetFrameworkProfile">Profile being targeted</param>
    /// <exception cref="ArgumentNullException">When the frameworkName is null</exception>
    /// <returns>Collection of reference assembly locations.</returns>
    public static IList<string> GetPathToReferenceAssemblies(string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile)
        => GetPathToReferenceAssemblies(targetFrameworkIdentifier, targetFrameworkVersion, targetFrameworkProfile, null);

    /// <summary>
    /// Returns the paths to the reference assemblies location for the given target framework.
    /// This method will assume the requested ReferenceAssemblyRoot path will be the ProgramFiles directory specified by Environment.SpecialFolder.ProgramFiles
    /// In additon when the .NETFramework or .NET Framework targetFrameworkIdentifiers are seen and targetFrameworkVersion is 2.0, 3.0, 3.5 or 4.0 we will return the correctly chained reference assembly paths
    /// for the legacy .net frameworks. This chaining will use the existing GetPathToDotNetFrameworkReferenceAssemblies to build up the list of reference assembly paths.
    /// </summary>
    /// <param name="targetFrameworkIdentifier">Identifier being targeted</param>
    /// <param name="targetFrameworkVersion">Version being targeted</param>
    /// <param name="targetFrameworkProfile">Profile being targeted</param>
    /// <param name="targetFrameworkRootPath">Root directory which will be used to calculate the reference assembly path. The references assemblies will be
    /// generated in the following way TargetFrameworkRootPath\TargetFrameworkIdentifier\TargetFrameworkVersion\SubType\TargetFrameworkSubType.
    /// Uses the default path if this is null.
    /// </param>
    /// <exception cref="ArgumentNullException">When the frameworkName is null</exception>
    /// <returns>Collection of reference assembly locations.</returns>
    public static IList<string> GetPathToReferenceAssemblies(string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile, string targetFrameworkRootPath)
        => GetPathToReferenceAssemblies(targetFrameworkIdentifier, targetFrameworkVersion, targetFrameworkProfile, targetFrameworkRootPath, null);

    /// <summary>
    /// Returns the paths to the reference assemblies location for the given target framework.
    /// This method will assume the requested ReferenceAssemblyRoot path will be the ProgramFiles directory specified by Environment.SpecialFolder.ProgramFiles
    /// In addition when the .NETFramework or .NET Framework targetFrameworkIdentifiers are seen and targetFrameworkVersion is 2.0, 3.0, 3.5 or 4.0 we will return the correctly chained reference assembly paths
    /// for the legacy .net frameworks. This chaining will use the existing GetPathToDotNetFrameworkReferenceAssemblies to build up the list of reference assembly paths.
    /// </summary>
    /// <param name="targetFrameworkIdentifier">Identifier being targeted</param>
    /// <param name="targetFrameworkVersion">Version being targeted</param>
    /// <param name="targetFrameworkProfile">Profile being targeted</param>
    /// <param name="targetFrameworkRootPath">Root directory which will be used to calculate the reference assembly path. The references assemblies will be
    /// generated in the following way TargetFrameworkRootPath\TargetFrameworkIdentifier\TargetFrameworkVersion\SubType\TargetFrameworkSubType.
    /// Uses the default path if this is null.
    /// </param>
    /// <param name="targetFrameworkFallbackSearchPaths">';' separated list of paths that are looked up if the framework cannot be found in <paramref name="targetFrameworkRootPath"/></param>
    /// <exception cref="ArgumentNullException">When the frameworkName is null</exception>
    /// <returns>Collection of reference assembly locations.</returns>
    public static IList<string> GetPathToReferenceAssemblies(string targetFrameworkIdentifier, string targetFrameworkVersion, string targetFrameworkProfile, string targetFrameworkRootPath, string targetFrameworkFallbackSearchPaths)
    {
        ArgumentException.ThrowIfNullOrEmpty(targetFrameworkVersion);
        ArgumentException.ThrowIfNullOrEmpty(targetFrameworkIdentifier);
        ArgumentNullException.ThrowIfNull(targetFrameworkProfile);

        Version frameworkVersion = ConvertTargetFrameworkVersionToVersion(targetFrameworkVersion);
        FrameworkNameVersioning targetFrameworkName = new FrameworkNameVersioning(targetFrameworkIdentifier, frameworkVersion, targetFrameworkProfile);

        return GetPathToReferenceAssemblies(targetFrameworkRootPath, targetFrameworkFallbackSearchPaths, targetFrameworkName);
    }

    /// <summary>
    /// Returns the paths to the reference assemblies location for the given target framework.
    /// This method will assume the requested ReferenceAssemblyRoot path will be the ProgramFiles directory specified by Environment.SpecialFolder.ProgramFiles
    /// In additon when the .NETFramework or .NET Framework targetFrameworkIdentifiers are seen and targetFrameworkVersion is 2.0, 3.0, 3.5 or 4.0 we will return the correctly chained reference assembly paths
    /// for the legacy .net frameworks. This chaining will use the existing GetPathToDotNetFrameworkReferenceAssemblies to build up the list of reference assembly paths.
    /// </summary>
    /// <param name="frameworkName">Framework required</param>
    /// <exception cref="ArgumentNullException">When the frameworkName is null</exception>
    /// <returns>Collection of reference assembly locations.</returns>
    public static IList<string> GetPathToReferenceAssemblies(FrameworkNameVersioning frameworkName)
    {
        // Verify the framework class passed in is not null. Other than being null the class will ensure the framework moniker is correct
        ArgumentNullException.ThrowIfNull(frameworkName);
        IList<string> paths =
            GetPathToReferenceAssemblies(
                FrameworkLocationHelper.programFilesReferenceAssemblyLocation,
                frameworkName);

        return paths;
    }

    /// <summary>
    /// Call either the static method or the delegate. This is done purely for performance as the delegate is only required for ease of unit testing and since
    /// the methods being called are static this will be a non 0 cost to use delegates vs the static methods directly.
    /// </summary>
    private static string VersionToDotNetFrameworkPath(VersionToPath PathToDotNetFramework, TargetDotNetFrameworkVersion version)
        => PathToDotNetFramework == null
            ? GetPathToDotNetFramework(version)
            : PathToDotNetFramework(version);

    /// <summary>
    /// Call either the static method or the delegate. This is done purely for performance as the delegate is only required for ease of unit testing and since
    /// the methods being called are static this will be a non 0 cost to use delegates vs the static methods directly.
    /// </summary>
    private static string VersionToDotNetReferenceAssemblies(VersionToPath PathToDotReferenceAssemblies, TargetDotNetFrameworkVersion version)
        => PathToDotReferenceAssemblies == null
            ? GetPathToDotNetFrameworkReferenceAssemblies(version)
            : PathToDotReferenceAssemblies(version);

    /// <summary>
    /// Generate the list of reference assembly paths for well known .net framework versions
    /// </summary>
    /// <param name="frameworkName">Target framework moniker class which contains the targetframeworkVersion</param>
    /// <param name="PathToDotNetFramework"></param>
    /// <param name="PathToReferenceAssemblies"></param>
    /// <returns>A collection of strings which list the chained reference assembly paths with the highest version being first</returns>
    internal static IList<string> HandleLegacyDotNetFrameworkReferenceAssemblyPaths(VersionToPath PathToDotNetFramework, VersionToPath PathToReferenceAssemblies, FrameworkNameVersioning frameworkName)
    {
        if (frameworkName.Version == FrameworkLocationHelper.dotNetFrameworkVersion20)
        {
            return HandleLegacy20(PathToDotNetFramework);
        }
        else if (frameworkName.Version == FrameworkLocationHelper.dotNetFrameworkVersion30)
        {
            return HandleLegacy30(PathToDotNetFramework, PathToReferenceAssemblies);
        }
        else if (frameworkName.Version == FrameworkLocationHelper.dotNetFrameworkVersion35)
        {
            return HandleLegacy35(PathToDotNetFramework, PathToReferenceAssemblies);
        }

        // Don't know the framework send back an empty list because it does not exist
        return new List<string>();
    }

    /// <summary>
    /// Generate the key which will be used for the reference assembly cache so that multiple static methods will generate it in the same way.
    /// </summary>
    private static string GenerateReferenceAssemblyCacheKey(string targetFrameworkRootPath, FrameworkNameVersioning frameworkName) => targetFrameworkRootPath + "|" + frameworkName.FullName;

    /// <summary>
    /// Create the shared cache if it is not null
    /// </summary>
    private static void CreateReferenceAssemblyPathsCache()
    {
        lock (s_locker)
        {
            s_cachedReferenceAssemblyPaths ??= new Dictionary<string, IList<string>>(StringComparer.OrdinalIgnoreCase);
        }
    }

    /// <summary>
    /// Do the correct chaining of .net 3.5, 3.0 and 2.0. Throw an exception if any of the chain is missing
    /// </summary>
    private static IList<string> HandleLegacy35(VersionToPath PathToDotNetFramework, VersionToPath PathToReferenceAssemblies)
    {
        var referencePaths = new List<string>();
        string referenceAssemblyPath = VersionToDotNetReferenceAssemblies(PathToReferenceAssemblies, TargetDotNetFrameworkVersion.Version35);
        string dotNetFrameworkPath = VersionToDotNetFrameworkPath(PathToDotNetFramework, TargetDotNetFrameworkVersion.Version35);

        if (referenceAssemblyPath != null && dotNetFrameworkPath != null)
        {
            referencePaths.Add(referenceAssemblyPath);
            referencePaths.Add(dotNetFrameworkPath);
        }
        else
        {
            return referencePaths;
        }

        // This method will return either an empty list or a list with elements in the order  3.0 2.0
        IList<string> referenceAssembly30Paths = HandleLegacy30(PathToDotNetFramework, PathToReferenceAssemblies);
        referencePaths.AddRange(referenceAssembly30Paths);

        return referencePaths;
    }

    /// <summary>
    /// Do the correct chaining of .net 3.5, 3.0 and 2.0. Throw an exception if any of the chain is missing
    /// </summary>
    private static IList<string> HandleLegacy30(VersionToPath PathToDotNetFramework, VersionToPath PathToReferenceAssemblies)
    {
        var referencePaths = new List<string>();
        string referenceAssemblyPath = VersionToDotNetReferenceAssemblies(PathToReferenceAssemblies, TargetDotNetFrameworkVersion.Version30);
        string dotNetFrameworkPath = VersionToDotNetFrameworkPath(PathToDotNetFramework, TargetDotNetFrameworkVersion.Version30);

        if (referenceAssemblyPath != null && dotNetFrameworkPath != null)
        {
            referencePaths.Add(referenceAssemblyPath);
            referencePaths.Add(dotNetFrameworkPath);
        }
        else
        {
            return referencePaths;
        }

        IList<string> referenceAssembly20Paths = HandleLegacy20(PathToDotNetFramework);
        referencePaths.AddRange(referenceAssembly20Paths);

        return referencePaths;
    }

    /// <summary>
    /// Check to see if .net 2.0 is installed
    /// </summary>
    private static IList<string> HandleLegacy20(VersionToPath PathToDotNetFramework)
    {
        var referencePaths = new List<string>();
        string referencePath = VersionToDotNetFrameworkPath(PathToDotNetFramework, TargetDotNetFrameworkVersion.Version20);

        if (referencePath != null)
        {
            referencePaths.Add(referencePath);
        }

        return referencePaths;
    }

    /// <summary>
    /// Returns the paths to the reference assemblies location for the given framework version relative to a given targetFrameworkRoot.
    /// The method will not check to see if the path exists or not.
    /// </summary>
    /// <param name="targetFrameworkRootPath">Root directory which will be used to calculate the reference assembly path. The references assemblies will be
    /// generated in the following way TargetFrameworkRootPath\TargetFrameworkIdentifier\TargetFrameworkVersion\SubType\TargetFrameworkSubType.
    /// </param>
    /// <param name="targetFrameworkFallbackSearchPaths">';' separated list of paths that are looked up if the framework cannot be found in @targetFrameworkRootPath</param>
    /// <param name="frameworkName">A frameworkName class which represents a TargetFrameworkMoniker. This cannot be null.</param>
    /// <returns>Collection of reference assembly locations.</returns>
    public static IList<string> GetPathToReferenceAssemblies(string targetFrameworkRootPath, string targetFrameworkFallbackSearchPaths, FrameworkNameVersioning frameworkName)
    {
        IList<string> pathsList = string.IsNullOrEmpty(targetFrameworkRootPath)
            ? GetPathToReferenceAssemblies(frameworkName)
            : GetPathToReferenceAssemblies(targetFrameworkRootPath, frameworkName);

        if (pathsList?.Count > 0)
        {
            return pathsList;
        }

        if (!string.IsNullOrEmpty(targetFrameworkFallbackSearchPaths))
        {
            foreach (string rootPath in targetFrameworkFallbackSearchPaths.Split(_separatorForFallbackSearchPaths, StringSplitOptions.RemoveEmptyEntries))
            {
                pathsList = GetPathToReferenceAssemblies(rootPath, frameworkName);
                if (pathsList?.Count > 0)
                {
                    return pathsList;
                }
            }
        }

        return pathsList ?? [];
    }

    /// <summary>
    /// Returns the paths to the reference assemblies location for the given framework version relative to a given targetFrameworkRoot.
    /// The method will not check to see if the path exists or not.
    /// </summary>
    /// <param name="targetFrameworkRootPath">Root directory which will be used to calculate the reference assembly path. The references assemblies will be
    /// generated in the following way TargetFrameworkRootPath\TargetFrameworkIdentifier\TargetFrameworkVersion\SubType\TargetFrameworkSubType.
    /// </param>
    /// <param name="frameworkName">A frameworkName class which represents a TargetFrameworkMoniker. This cannot be null.</param>
    /// <returns>Collection of reference assembly locations.</returns>
    public static IList<string> GetPathToReferenceAssemblies(string targetFrameworkRootPath, FrameworkNameVersioning frameworkName)
    {
        // Verify the root path is not null throw an ArgumentNullException if the given string parameter is null and ArgumentException if it has zero length.
        ArgumentException.ThrowIfNullOrEmpty(targetFrameworkRootPath);

        // Verify the framework class passed in is not null. Other than being null the class will ensure it is consistent and the internal state is correct
        ArgumentNullException.ThrowIfNull(frameworkName);

        string referenceAssemblyCacheKey = GenerateReferenceAssemblyCacheKey(targetFrameworkRootPath, frameworkName);
        CreateReferenceAssemblyPathsCache();

        lock (s_locker)
        {
            if (s_cachedReferenceAssemblyPaths.TryGetValue(referenceAssemblyCacheKey, out IList<string> referenceAssemblies))
            {
                return referenceAssemblies;
            }
        }

        // Try and find the reference assemblies using the reference assembly path generation algorithm
        IList<string> dotNetFrameworkReferenceAssemblies = GetPathAndChainReferenceAssemblyLocations(targetFrameworkRootPath, frameworkName, true);

        // We have not found any reference assembly locations, if we are the .net framework we can try and fallback to the old legacy tool location helper methods
        if (string.Equals(frameworkName.Identifier, ".NETFramework", StringComparison.OrdinalIgnoreCase)
            && dotNetFrameworkReferenceAssemblies.Count == 0)
        {
            if (string.IsNullOrEmpty(frameworkName.Profile)) // profiles are always in new locations
            {
                // If the identifier is ".NET Framework" and the version is a well know legacy version. Manually generate the list of reference assembly paths
                // based on the known chaining order. Pass null in for the two delegates so we call the static methods rather than require the creation and calling
                // of two delegates
                dotNetFrameworkReferenceAssemblies = HandleLegacyDotNetFrameworkReferenceAssemblyPaths(
                    PathToDotNetFramework: null,
                    PathToReferenceAssemblies: null,
                    frameworkName);
            }
        }

        for (int i = 0; i < dotNetFrameworkReferenceAssemblies.Count; i++)
        {
            if (
                !dotNetFrameworkReferenceAssemblies[i].EndsWith(
                    Path.DirectorySeparatorChar.ToString(),
                    StringComparison.Ordinal))
            {
                dotNetFrameworkReferenceAssemblies[i] += Path.DirectorySeparatorChar;
            }
        }

        lock (s_locker)
        {
            s_cachedReferenceAssemblyPaths[referenceAssemblyCacheKey] = dotNetFrameworkReferenceAssemblies;
        }

        return dotNetFrameworkReferenceAssemblies;
    }

    /// <summary>
    /// Figures out a display name given the target framework details.
    /// This is the equivalent of the target framework moniker, but for display.
    /// If one cannot be found from the redist list file, a synthesized one is returned, so there is always a display name.
    /// </summary>
    public static string GetDisplayNameForTargetFrameworkDirectory(string targetFrameworkDirectory, FrameworkNameVersioning frameworkName)
    {
        string displayName;
        lock (s_locker)
        {
            if (s_cachedTargetFrameworkDisplayNames != null
                && s_cachedTargetFrameworkDisplayNames.TryGetValue(targetFrameworkDirectory, out displayName))
            {
                return displayName;
            }
        }

        // Not in the cache, try to find it and if so cache it
        ChainReferenceAssemblyPath(targetFrameworkDirectory);

        lock (s_locker)
        {
            if (s_cachedTargetFrameworkDisplayNames.TryGetValue(targetFrameworkDirectory, out displayName))
            {
                return displayName;
            }
        }

        // Still don't have one.
        // Probably it's 3.5 or earlier: make something reasonable.
        // VS uses the same algorithm to find something to display
        StringBuilder displayNameBuilder = new StringBuilder();

        displayNameBuilder.Append(frameworkName.Identifier);
        displayNameBuilder.Append(' ');
        displayNameBuilder.Append('v').Append(frameworkName.Version.ToString());

        if (!string.IsNullOrEmpty(frameworkName.Profile))
        {
            displayNameBuilder.Append(' ');
            displayNameBuilder.Append(frameworkName.Profile);
        }

        displayName = displayNameBuilder.ToString();

        // Cache it
        lock (s_locker)
        {
            s_cachedTargetFrameworkDisplayNames[targetFrameworkDirectory] = displayName;
        }

        return displayName;
    }

    /// <summary>
    /// Returns the paths to the reference assemblies location for the given framework version and properly chains the reference assemblies if required.
    /// </summary>
    /// <returns>Collection of reference assembly locations.</returns>
    private static IList<string> GetPathAndChainReferenceAssemblyLocations(string targetFrameworkRootPath, FrameworkNameVersioning frameworkName, bool chain)
    {
        var referencePaths = new List<string>();

        string path = FrameworkLocationHelper.GenerateReferenceAssemblyPath(targetFrameworkRootPath, frameworkName);
        if (FileSystems.Default.DirectoryExists(path))
        {
            referencePaths.Add(path);

            if (chain)
            {
                while (!string.IsNullOrEmpty(path))
                {
                    // Will return String.Empty when there are no longer any paths to chain to
                    // We will return null if the chain is invalid and we need to return an empty chain.
                    path = ChainReferenceAssemblyPath(path);

                    if (!string.IsNullOrEmpty(path))
                    {
                        if (referencePaths.Contains(path))
                        {
                            break;
                        }

                        referencePaths.Add(path);
                    }
                    else if (path == null)
                    {
                        // We have an invalid chain, we need to clear out any reference paths we have already added.
                        referencePaths.Clear();
                        break;
                    }
                }
            }
        }

        return referencePaths;
    }

    /// <summary>
    /// Take the path to a reference assembly directory which contains a RedistList folder which then contains a FrameworkList.xml file.
    /// We will read in the xml file and determine if it has an IncludedFramework element in the redist list. If it does it will calculate
    /// the path where the next link in the chain should be and return that path.
    /// Also, when reading the redist list, if any display name is found it will be cached, keyed off the path passed in.
    /// </summary>
    /// <returns>Return null if we could not chain due to an error or the path not being found. return String.Empty if there is no next element in the chain</returns>
    internal static string ChainReferenceAssemblyPath(string targetFrameworkDirectory)
    {
        string path = Path.GetFullPath(targetFrameworkDirectory);

        lock (s_locker)
        {
            // Cache the results of the chain search so that we do not have to do an expensive read more than once per process per redist list.
            s_chainedReferenceAssemblyPath ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
            s_cachedTargetFrameworkDisplayNames ??= new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

            if (s_chainedReferenceAssemblyPath.TryGetValue(path, out string cachedPath))
            {
                return cachedPath;
            }
        }

        // Read in the redist list at the specified path, and return
        // the display name and the "include framework" value for chaining.
        // If display name is not available, returns empty string.
        // If include framework is not available, returns null.
        // Caches the display name keyed by the path.

        // Make sure we have a directory with a redist list folder and a FrameworkList.xml file in there as this is what we will use for chaining.
        string redistListFolder = Path.Combine(path, "RedistList");
        string redistFilePath = Path.Combine(redistListFolder, "FrameworkList.xml");

        // If the redist list does not exist then the entire chain is incorrect.
        if (!FileSystems.Default.FileExists(redistFilePath))
        {
            lock (s_locker)
            {
                s_chainedReferenceAssemblyPath[path] = null;
                s_cachedTargetFrameworkDisplayNames[path] = null;
            }

            return null;
        }

        string includeFramework = null;
        string displayName = null;
        string redirectPath = null;

        try
        {
            // Read in the xml file looking for the includeFramework inorder to chain.
            XmlReaderSettings readerSettings = new XmlReaderSettings { DtdProcessing = DtdProcessing.Ignore, CloseInput = true };
            FileStream fs = File.OpenRead(redistFilePath);
            using (XmlReader reader = XmlReader.Create(fs, readerSettings))
            {
                while (reader.Read())
                {
                    if (reader.NodeType == XmlNodeType.Element)
                    {
                        if (string.Equals(reader.Name, "FileList", StringComparison.OrdinalIgnoreCase))
                        {
                            reader.MoveToFirstAttribute();
                            do
                            {
                                if (string.Equals(reader.Name, "IncludeFramework", StringComparison.OrdinalIgnoreCase))
                                {
                                    includeFramework = reader.Value;
                                    continue;
                                }

                                if (string.Equals(reader.Name, "Name", StringComparison.OrdinalIgnoreCase))
                                {
                                    displayName = reader.Value;
                                    continue;
                                }
                            }
                            while (reader.MoveToNextAttribute());
                            reader.MoveToElement();
                            break;
                        }
                    }
                }
            }
        }
        catch (XmlException ex)
        {
            throw new InvalidOperationException(SR.FormatInvalidRedistFile(redistFilePath, ex.Message));
        }
        catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
        {
            throw new InvalidOperationException(SR.FormatInvalidRedistFile(redistFilePath, ex.Message));
        }

        // Cache the display name if we have one
        if (displayName != null)
        {
            lock (s_locker)
            {
                s_cachedTargetFrameworkDisplayNames[path] = displayName;
            }
        }

        string pathToReturn = string.Empty;

        try
        {
            // The IncludeFramework element could not be found so our chain is done.
            if (!string.IsNullOrEmpty(includeFramework))
            {
                // Take the path which should point to something like  c:\ProgramFiles\ReferenceAssemblies\Framework\.NETFramework\v4.1
                // We will take the path, to "up" a directory then append the name found in the redist. For example if the redist list had v4.0
                // the path which would be expected would be c:\ProgramFiles\ReferenceAssemblies\Framework\.NETFramework\v4.0
                pathToReturn = path;
                pathToReturn = Directory.GetParent(pathToReturn).FullName;
                pathToReturn = Path.Combine(pathToReturn, includeFramework);
                pathToReturn = Path.GetFullPath(pathToReturn);

                // The directory which we are chaining to does not exist, return null indicating the chain is incorrect.
                if (!FileSystems.Default.DirectoryExists(pathToReturn))
                {
                    pathToReturn = null;
                }
            }
            // We may also have a redirect path
            else if (!string.IsNullOrEmpty(redirectPath) && FileSystems.Default.DirectoryExists(redirectPath))
            {
                pathToReturn = redirectPath;
            }

            lock (s_locker)
            {
                s_chainedReferenceAssemblyPath[path] = pathToReturn;
            }

            return pathToReturn;
        }
        catch (Exception e) when (!ExceptionHandling.IsCriticalException(e))
        {
            throw new InvalidOperationException(SR.FormatCouldNotCreateChain(path, pathToReturn, e.Message));
        }
    }

    /// <summary>
    /// Will return the root location for the reference assembly directory under the program files directory.
    /// </summary>
    /// <returns></returns>
    public static string GetProgramFilesReferenceAssemblyRoot()
        => FrameworkLocationHelper.programFilesReferenceAssemblyLocation;
}