Repository: microsoft/MSBuildLocator
Pull request #329: Return SDKs from all dotnet location
Author: login: JoeRobich
Created: 2025-03-19T23:02:41Z | Merged: 2025-03-31T10:04:12Z

[Review with whitespace off](https://github.com/microsoft/MSBuildLocator/pull/329/files?diff=split&w=1) as the formatter removed trailing whitespace.

The method which finds all SDKs has an early exit which stops it from finding SDKs from more than one dotnet install. This PR removes the early exit and filters the returned instances by their reported version number. This should deduplicate the SDKs.


Diff:
diff --git a/src/MSBuildLocator/DotNetSdkLocationHelper.cs b/src/MSBuildLocator/DotNetSdkLocationHelper.cs
index f5398ff..58cb54d 100644
--- a/src/MSBuildLocator/DotNetSdkLocationHelper.cs
+++ b/src/MSBuildLocator/DotNetSdkLocationHelper.cs
@@ -56,8 +56,8 @@ internal static class DotNetSdkLocationHelper
             // in the .NET 5 SDK rely on the .NET 5.0 runtime. Assuming the runtime that shipped with a particular SDK has the same version,
             // this ensures that we don't choose an SDK that doesn't work with the runtime of the chosen application. This is not guaranteed
             // to always work but should work for now.
-            if (!allowQueryAllRuntimeVersions && 
-                (major > Environment.Version.Major || 
+            if (!allowQueryAllRuntimeVersions &&
+                (major > Environment.Version.Major ||
                  (major == Environment.Version.Major && minor > Environment.Version.Minor)))
             {
                 return null;
@@ -70,46 +70,104 @@ internal static class DotNetSdkLocationHelper
                 discoveryType: DiscoveryType.DotNetSdk);
         }
 
-        public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes)
-        {            
-            foreach (var basePath in GetDotNetBasePaths(workingDirectory))
+        public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes, bool allowAllDotnetLocations)
+        {
+            string? bestSdkPath;
+            string[] allAvailableSdks;
+            try
+            {
+                AddUnmanagedDllResolver();
+
+                bestSdkPath = GetSdkFromGlobalSettings(workingDirectory);
+                allAvailableSdks = GetAllAvailableSDKs(allowAllDotnetLocations).ToArray();
+            }
+            finally
+            {
+                RemoveUnmanagedDllResolver();
+            }
+
+            Dictionary<Version, VisualStudioInstance?> versionInstanceMap = new();
+            foreach (var basePath in allAvailableSdks)
             {
                 var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes);
                 if (dotnetSdk != null)
                 {
-                    yield return dotnetSdk;
+                    // We want to return the best SDK first
+                    if (dotnetSdk.VisualStudioRootPath == bestSdkPath)
+                    {
+                        // We will add a null entry to the map to ensure we do not add the same SDK from a different location.
+                        versionInstanceMap[dotnetSdk.Version] = null;
+                        yield return dotnetSdk;
+                    }
+
+                    // Only add an SDK once, even if it's installed in multiple locations.
+                    versionInstanceMap.TryAdd(dotnetSdk.Version, dotnetSdk);
                 }
             }
-        }
 
-        private static IEnumerable<string> GetDotNetBasePaths(string workingDirectory)
-        {
-            try
+            // We want to return the newest SDKs first. Using OfType will remove the null entry added if we found the best SDK.
+            var instances = versionInstanceMap.Values.OfType<VisualStudioInstance>().OrderByDescending(i => i.Version);
+            foreach (var instance in instances)
             {
-                AddUnmanagedDllResolver();
+                yield return instance;
+            }
 
-                string? bestSDK = GetSdkFromGlobalSettings(workingDirectory);
-                if (!string.IsNullOrEmpty(bestSDK))
+            // Returns the list of all available SDKs ordered by ascending version.
+            static IEnumerable<string> GetAllAvailableSDKs(bool allowAllDotnetLocations)
+            {
+                bool foundSdks = false;
+                string[]? resolvedPaths = null;
+                foreach (string dotnetPath in s_dotnetPathCandidates.Value)
                 {
-                    yield return bestSDK;
-                }
+                    int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);
 
-                string[] dotnetPaths = GetAllAvailableSDKs();
-                // We want to return the newest SDKs first, however, so iterate over the list in reverse order.
-                // If basePath is disqualified because it was later
-                // than the runtime version, this ensures that RegisterDefaults will return the latest valid
-                // SDK instead of the earliest installed.
-                for (int i = dotnetPaths.Length - 1; i >= 0; i--)
-                {
-                    if (dotnetPaths[i] != bestSDK)
+                    if (rc == 0 && resolvedPaths != null)
                     {
-                        yield return dotnetPaths[i];
+                        foundSdks = true;
+
+                        foreach (string path in resolvedPaths)
+                        {
+                            yield return path;
+                        }
+
+                        if (resolvedPaths.Length > 0 && !allowAllDotnetLocations)
+                        {
+                            break;
+                        }
                     }
                 }
+
+                // Errors are automatically printed to stderr. We should not continue to try to output anything if we failed.
+                if (!foundSdks)
+                {
+                    throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks)));
+                }
             }
-            finally
+
+            // Determines the directory location of the SDK accounting for global.json and multi-level lookup policy.
+            static string? GetSdkFromGlobalSettings(string workingDirectory)
             {
-                RemoveUnmanagedDllResolver();
+                string? resolvedSdk = null;
+                foreach (string dotnetPath in s_dotnetPathCandidates.Value)
+                {
+                    int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: dotnetPath, working_dir: workingDirectory, flags: 0, result: (key, value) =>
+                    {
+                        if (key == NativeMethods.hostfxr_resolve_sdk2_result_key_t.resolved_sdk_dir)
+                        {
+                            resolvedSdk = value;
+                        }
+                    });
+
+                    if (rc == 0)
+                    {
+                        SetEnvironmentVariableIfEmpty("DOTNET_HOST_PATH", Path.Combine(dotnetPath, ExeName));
+                        return resolvedSdk;
+                    }
+                }
+
+                return string.IsNullOrEmpty(resolvedSdk)
+                    ? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_resolve_sdk2)))
+                    : resolvedSdk;
             }
         }
 
@@ -158,7 +216,7 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName)
                     };
 
                     var orderedVersions = fileEnumerable.Where(v => v != null).Select(v => v!).OrderByDescending(f => f).ToList();
-                    
+
                     foreach (SemanticVersion hostFxrVersion in orderedVersions)
                     {
                         string hostFxrAssembly = Path.Combine(hostFxrRoot, hostFxrVersion.OriginalValue, hostFxrLibName);
@@ -178,35 +236,6 @@ private static IntPtr HostFxrResolver(Assembly assembly, string libraryName)
         }
 
         private static string SdkResolutionExceptionMessage(string methodName) => $"Failed to find all versions of .NET Core MSBuild. Call to {methodName}. There may be more details in stderr.";
-        
-        /// <summary>
-        /// Determines the directory location of the SDK accounting for
-        /// global.json and multi-level lookup policy.
-        /// </summary>
-        private static string? GetSdkFromGlobalSettings(string workingDirectory)
-        {
-            string? resolvedSdk = null;
-            foreach (string dotnetPath in s_dotnetPathCandidates.Value)
-            {
-                int rc = NativeMethods.hostfxr_resolve_sdk2(exe_dir: dotnetPath, working_dir: workingDirectory, flags: 0, result: (key, value) =>
-                {
-                    if (key == NativeMethods.hostfxr_resolve_sdk2_result_key_t.resolved_sdk_dir)
-                    {
-                        resolvedSdk = value;
-                    }
-                });
-
-                if (rc == 0)
-                {
-                    SetEnvironmentVariableIfEmpty("DOTNET_HOST_PATH", Path.Combine(dotnetPath, ExeName));
-                    return resolvedSdk;
-                }
-            }
-
-            return string.IsNullOrEmpty(resolvedSdk)
-                ? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_resolve_sdk2)))
-                : resolvedSdk;
-        }
 
         private static IList<string> ResolveDotnetPathCandidates()
         {
@@ -256,7 +285,7 @@ void AddIfValid(string? path)
             // 32-bit architecture has (x86) suffix
             string envVarName = (IntPtr.Size == 4) ? "DOTNET_ROOT(x86)" : "DOTNET_ROOT";
             var dotnetPath = FindDotnetPathFromEnvVariable(envVarName);
-            
+
             return dotnetPath;
         }
 
@@ -285,26 +314,6 @@ void AddIfValid(string? path)
             return dotnetPath;
         }
 
-        /// <summary>
-        /// Returns the list of all available SDKs ordered by ascending version.
-        /// </summary>
-        private static string[] GetAllAvailableSDKs()
-        {
-            string[]? resolvedPaths = null;
-            foreach (string dotnetPath in s_dotnetPathCandidates.Value)
-            {
-                int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);
-
-                if (rc == 0 && resolvedPaths != null && resolvedPaths.Length > 0)
-                {
-                    break;
-                }
-            }
-
-            // Errors are automatically printed to stderr. We should not continue to try to output anything if we failed.
-            return resolvedPaths ?? throw new InvalidOperationException(SdkResolutionExceptionMessage(nameof(NativeMethods.hostfxr_get_available_sdks)));
-        }
-
         /// <summary>
         /// This native method call determines the actual location of path, including
         /// resolving symbolic links.
@@ -321,7 +330,7 @@ private static string[] GetAllAvailableSDKs()
         private static string? FindDotnetPathFromEnvVariable(string environmentVariable)
         {
             string? dotnetPath = Environment.GetEnvironmentVariable(environmentVariable);
-            
+
             return string.IsNullOrEmpty(dotnetPath) ? null : ValidatePath(dotnetPath);
         }
 
diff --git a/src/MSBuildLocator/MSBuildLocator.cs b/src/MSBuildLocator/MSBuildLocator.cs
index f501f49..9d76676 100644
--- a/src/MSBuildLocator/MSBuildLocator.cs
+++ b/src/MSBuildLocator/MSBuildLocator.cs
@@ -52,6 +52,14 @@ public static class MSBuildLocator
         /// </remarks.
         public static bool AllowQueryAllRuntimeVersions { get; set; } = false;
 
+        /// <summary>
+        ///     Allow discovery of .NET SDK versions from all discovered dotnet install locations.
+        /// </summary>
+        /// <remarks>
+        ///     Defaults to <see langword="false"/>. Set this to <see langword="true"/> only if you do not mind behaving differently than the dotnet muxer.
+        /// </remarks.
+        public static bool AllowQueryAllDotnetLocations { get; set; } = false;
+
         /// <summary>
         ///     Gets a value indicating whether an instance of MSBuild can be registered.
         /// </summary>
@@ -200,7 +208,7 @@ private static void RegisterMSBuildPathsInternally(string[] msbuildSearchPaths)
             {
                 if (string.IsNullOrWhiteSpace(msbuildSearchPaths[i]))
                 {
-                    nullOrWhiteSpaceExceptions.Add(new ArgumentException($"Value at position {i+1} may not be null or whitespace", nameof(msbuildSearchPaths)));
+                    nullOrWhiteSpaceExceptions.Add(new ArgumentException($"Value at position {i + 1} may not be null or whitespace", nameof(msbuildSearchPaths)));
                 }
             }
             if (nullOrWhiteSpaceExceptions.Count > 0)
@@ -266,7 +274,7 @@ private static void RegisterMSBuildPathsInternally(string[] msbuildSearchPaths)
 
             AppDomain.CurrentDomain.AssemblyResolve += s_registeredHandler;
 #else
-            s_registeredHandler = (_, assemblyName) => 
+            s_registeredHandler = (_, assemblyName) =>
             {
                 return TryLoadAssembly(assemblyName);
             };
@@ -377,7 +385,8 @@ private static IEnumerable<VisualStudioInstance> GetInstances(VisualStudioInstan
 #if NETCOREAPP
             // AllowAllRuntimeVersions was added to VisualStudioInstanceQueryOptions for fulfilling Roslyn's needs. One of the properties will be removed in v2.0.
             bool allowAllRuntimeVersions = AllowQueryAllRuntimeVersions || options.AllowAllRuntimeVersions;
-            foreach (var dotnetSdk in DotNetSdkLocationHelper.GetInstances(options.WorkingDirectory, allowAllRuntimeVersions))
+            bool allowAllDotnetLocations = AllowQueryAllDotnetLocations || options.AllowAllDotnetLocations;
+            foreach (var dotnetSdk in DotNetSdkLocationHelper.GetInstances(options.WorkingDirectory, allowAllRuntimeVersions, allowAllDotnetLocations))
                 yield return dotnetSdk;
 #endif
         }
@@ -404,7 +413,7 @@ private static VisualStudioInstance GetDevConsoleInstance()
                     Version.TryParse(versionString, out version);
                 }
 
-                if(version != null)
+                if (version != null)
                 {
                     return new VisualStudioInstance("DEVCONSOLE", path, version, DiscoveryType.DeveloperConsole);
                 }
diff --git a/src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs b/src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs
index c3e1660..116abb2 100644
--- a/src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs
+++ b/src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs
@@ -37,6 +37,14 @@ public class VisualStudioInstanceQueryOptions
         ///     Defaults to <see langword="false"/>. Set this to <see langword="true"/> only if your application has special logic to handle loading an incompatible SDK, such as launching a new process with the target SDK's runtime.
         /// </remarks.
         public bool AllowAllRuntimeVersions { get; set; } = false;
+
+        /// <summary>
+        ///     Allow discovery of .NET SDK versions from all discovered dotnet install locations.
+        /// </summary>
+        /// <remarks>
+        ///     Defaults to <see langword="false"/>. Set this to <see langword="true"/> only if you do not mind behaving differently than a command-line dotnet invocation.
+        /// </remarks.
+        public bool AllowAllDotnetLocations { get; set; } = false;
 #endif
 
         /// <summary>
diff --git a/version.json b/version.json
index 1d01149..99db79e 100644
--- a/version.json
+++ b/version.json
@@ -1,5 +1,5 @@
 {
-  "version": "1.7",
+  "version": "1.8",
   "assemblyVersion": "1.0.0.0",
   "publicReleaseRefSpec": [
     "^refs/heads/release/.*"


Reviews:
user: login: Forgind
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T23:42:23Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T00:03:12Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T00:03:59Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T00:09:58Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T00:12:03Z
user: login: Forgind
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T00:17:54Z
user: login: Forgind
body: I think this would work the way I'd expect! @baronfel mentioned a concern about diverging from dotnet.exe's behavior in not using other dotnet.exes, but I would want to better understand his reasoning, as I'd argue that MSBuildLocator is (and always has been) more flexible with respect to which MSBuild it ultimately loads than dotnet.exe is, so I see no reason it shouldn't also be more flexible as far as which it finds. That said, we should still solicit feedback from MSBuild, as I don't actually own this product 🙂
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-20T22:59:53Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T05:50:47Z
user: login: Forgind
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T20:31:39Z
user: login: rainersigwald
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T14:45:15Z
user: login: Forgind
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T18:03:06Z
user: login: rainersigwald
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T19:03:52Z
user: login: Forgind
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-28T19:45:08Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T20:43:04Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T20:44:43Z
user: login: rainersigwald
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T21:27:55Z
user: login: rainersigwald
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-28T21:29:25Z
user: login: rainersigwald
body: I guess this should be a minor version bump, for adding the feature to request this?
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-28T21:30:29Z
user: login: darikprescott
body: Seems very logical to provide a mechanism to allow faster delivery of sdks and being able to find them faster. 
state: APPROVED
author_association: NONE
submitted_at: 2025-03-29T17:51:02Z

Review comments:
diff_hunk: @@ -293,12 +298,7 @@ private static string[] GetAllAvailableSDKs()
             string[]? resolvedPaths = null;
             foreach (string dotnetPath in s_dotnetPathCandidates.Value)
             {
-                int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);
-
-                if (rc == 0 && resolvedPaths != null && resolvedPaths.Length > 0)
-                {
-                    break;
-                }
+                NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);
path: src/MSBuildLocator/DotNetSdkLocationHelper.cs
user: login: Forgind
body: How about this?

```suggestion
                int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);

                if (rc == 0 && resolvedPaths != null && resolvedPaths.Length > 0)
                {
                    foreach (string path in resolvedPaths)
                    {
                        yield return path;
                    }
                }
```
created_at: 2025-03-19T23:42:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,45 +71,87 @@ internal static class DotNetSdkLocationHelper
         }
 
         public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes)
-        {            
-            foreach (var basePath in GetDotNetBasePaths(workingDirectory))
+        {
+            string? bestSdkPath;
+            string[] allAvailableSdks;
+            try
path: src/MSBuildLocator/DotNetSdkLocationHelper.cs
user: login: JoeRobich
body: Inlined `GetDotNetBasePaths`
created_at: 2025-03-20T00:01:52Z
author_association: MEMBER
diff_hunk: @@ -71,45 +71,87 @@ internal static class DotNetSdkLocationHelper
         }
 
         public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes)
-        {            
-            foreach (var basePath in GetDotNetBasePaths(workingDirectory))
+        {
+            string? bestSdkPath;
+            string[] allAvailableSdks;
+            try
+            {
+                AddUnmanagedDllResolver();
+
+                bestSdkPath = GetSdkFromGlobalSettings(workingDirectory);
+                allAvailableSdks = GetAllAvailableSDKs();
+            }
+            finally
+            {
+                RemoveUnmanagedDllResolver();
+            }
+
+            Dictionary<Version, VisualStudioInstance?> versionInstanceMap = new();
+            foreach (var basePath in allAvailableSdks)
             {
                 var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes);
                 if (dotnetSdk != null)
                 {
-                    yield return dotnetSdk;
+                    // We want to return the best SDK first
+                    if (dotnetSdk.VisualStudioRootPath == bestSdkPath)
+                    {
+                        // We will add a null entry to the map to ensure we do not add the same SDK from a different location.
+                        versionInstanceMap[dotnetSdk.Version] = null;
+                        yield return dotnetSdk;
+                    }
+
+                    // Only add an SDK once, even if it's installed in multiple locations.
+                    if (!versionInstanceMap.ContainsKey(dotnetSdk.Version))
+                    {
+                        versionInstanceMap.Add(dotnetSdk.Version, dotnetSdk);
+                    }
                 }
             }
-        }
 
-        private static IEnumerable<string> GetDotNetBasePaths(string workingDirectory)
-        {
-            try
+            // We want to return the newest SDKs first. Using OfType will remove the null entry added if we found the best SDK.
+            var instances = versionInstanceMap.Values.OfType<VisualStudioInstance>().OrderByDescending(i => i.Version);
+            foreach (var instance in instances)
             {
-                AddUnmanagedDllResolver();
+                yield return instance;
+            }
 
-                string? bestSDK = GetSdkFromGlobalSettings(workingDirectory);
-                if (!string.IsNullOrEmpty(bestSDK))
+            // Returns the list of all available SDKs ordered by ascending version.
+            static string[] GetAllAvailableSDKs()
path: src/MSBuildLocator/DotNetSdkLocationHelper.cs
user: login: JoeRobich
body: This method and `GetSdkFromGlobalSettings` were only referenced from the inlined `GetDotNetBasePaths` so I made them local functions.
created_at: 2025-03-20T00:03:02Z
author_association: MEMBER
diff_hunk: @@ -293,12 +298,7 @@ private static string[] GetAllAvailableSDKs()
             string[]? resolvedPaths = null;
             foreach (string dotnetPath in s_dotnetPathCandidates.Value)
             {
-                int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);
-
-                if (rc == 0 && resolvedPaths != null && resolvedPaths.Length > 0)
-                {
-                    break;
-                }
+                NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);
path: src/MSBuildLocator/DotNetSdkLocationHelper.cs
user: login: JoeRobich
body: Ah, thanks in my head it was accumulating them but I see now it isn't. Will update.
created_at: 2025-03-20T00:03:59Z
author_association: MEMBER
diff_hunk: @@ -293,12 +298,7 @@ private static string[] GetAllAvailableSDKs()
             string[]? resolvedPaths = null;
             foreach (string dotnetPath in s_dotnetPathCandidates.Value)
             {
-                int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);
-
-                if (rc == 0 && resolvedPaths != null && resolvedPaths.Length > 0)
-                {
-                    break;
-                }
+                NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);
path: src/MSBuildLocator/DotNetSdkLocationHelper.cs
user: login: JoeRobich
body: Updated.
created_at: 2025-03-20T00:09:58Z
author_association: MEMBER
diff_hunk: @@ -71,45 +71,101 @@ internal static class DotNetSdkLocationHelper
         }
 
         public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes)
-        {            
-            foreach (var basePath in GetDotNetBasePaths(workingDirectory))
+        {
+            string? bestSdkPath;
+            string[] allAvailableSdks;
+            try
+            {
+                AddUnmanagedDllResolver();
+
+                bestSdkPath = GetSdkFromGlobalSettings(workingDirectory);
+                allAvailableSdks = GetAllAvailableSDKs().ToArray();
+            }
+            finally
+            {
+                RemoveUnmanagedDllResolver();
+            }
+
+            Dictionary<Version, VisualStudioInstance?> versionInstanceMap = new();
+            foreach (var basePath in allAvailableSdks)
             {
                 var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes);
                 if (dotnetSdk != null)
                 {
-                    yield return dotnetSdk;
+                    // We want to return the best SDK first
+                    if (dotnetSdk.VisualStudioRootPath == bestSdkPath)
+                    {
+                        // We will add a null entry to the map to ensure we do not add the same SDK from a different location.
+                        versionInstanceMap[dotnetSdk.Version] = null;
+                        yield return dotnetSdk;
+                    }
+
+                    // Only add an SDK once, even if it's installed in multiple locations.
+                    if (!versionInstanceMap.ContainsKey(dotnetSdk.Version))
+                    {
+                        versionInstanceMap.Add(dotnetSdk.Version, dotnetSdk);
+                    }
                 }
             }
-        }
 
-        private static IEnumerable<string> GetDotNetBasePaths(string workingDirectory)
-        {
-            try
+            // We want to return the newest SDKs first. Using OfType will remove the null entry added if we found the best SDK.
+            var instances = versionInstanceMap.Values.OfType<VisualStudioInstance>().OrderByDescending(i => i.Version);
+            foreach (var instance in instances)
             {
-                AddUnmanagedDllResolver();
+                yield return instance;
+            }
 
-                string? bestSDK = GetSdkFromGlobalSettings(workingDirectory);
-                if (!string.IsNullOrEmpty(bestSDK))
+            // Returns the list of all available SDKs ordered by ascending version.
+            static IEnumerable<string> GetAllAvailableSDKs()
+            {
+                bool foundSdks = false;
+                string[]? resolvedPaths = null;
+                foreach (string dotnetPath in s_dotnetPathCandidates.Value)
                 {
-                    yield return bestSDK;
-                }
+                    int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);
 
-                string[] dotnetPaths = GetAllAvailableSDKs();
-                // We want to return the newest SDKs first, however, so iterate over the list in reverse order.
-                // If basePath is disqualified because it was later
-                // than the runtime version, this ensures that RegisterDefaults will return the latest valid
-                // SDK instead of the earliest installed.
-                for (int i = dotnetPaths.Length - 1; i >= 0; i--)
-                {
-                    if (dotnetPaths[i] != bestSDK)
+                    if (rc == 0 && resolvedPaths != null)
path: src/MSBuildLocator/DotNetSdkLocationHelper.cs
user: login: JoeRobich
body: Previous behavior was that we would not throw if `resolvedPaths` was an empty array.
created_at: 2025-03-20T00:12:03Z
author_association: MEMBER
diff_hunk: @@ -293,12 +298,7 @@ private static string[] GetAllAvailableSDKs()
             string[]? resolvedPaths = null;
             foreach (string dotnetPath in s_dotnetPathCandidates.Value)
             {
-                int rc = NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);
-
-                if (rc == 0 && resolvedPaths != null && resolvedPaths.Length > 0)
-                {
-                    break;
-                }
+                NativeMethods.hostfxr_get_available_sdks(exe_dir: dotnetPath, result: (key, value) => resolvedPaths = value);
path: src/MSBuildLocator/DotNetSdkLocationHelper.cs
user: login: Forgind
body: Definitely a bit confusing with how the native APIs work 🙂 I struggled with that for a bit when I wrote the first version of this.
created_at: 2025-03-20T00:17:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,45 +71,101 @@ internal static class DotNetSdkLocationHelper
         }
 
         public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes)
-        {            
-            foreach (var basePath in GetDotNetBasePaths(workingDirectory))
+        {
+            string? bestSdkPath;
+            string[] allAvailableSdks;
+            try
+            {
+                AddUnmanagedDllResolver();
+
+                bestSdkPath = GetSdkFromGlobalSettings(workingDirectory);
+                allAvailableSdks = GetAllAvailableSDKs().ToArray();
+            }
+            finally
+            {
+                RemoveUnmanagedDllResolver();
+            }
+
+            Dictionary<Version, VisualStudioInstance?> versionInstanceMap = new();
+            foreach (var basePath in allAvailableSdks)
             {
                 var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes);
                 if (dotnetSdk != null)
                 {
-                    yield return dotnetSdk;
+                    // We want to return the best SDK first
+                    if (dotnetSdk.VisualStudioRootPath == bestSdkPath)
+                    {
+                        // We will add a null entry to the map to ensure we do not add the same SDK from a different location.
+                        versionInstanceMap[dotnetSdk.Version] = null;
+                        yield return dotnetSdk;
+                    }
+
+                    // Only add an SDK once, even if it's installed in multiple locations.
+                    if (!versionInstanceMap.ContainsKey(dotnetSdk.Version))
+                    {
+                        versionInstanceMap.Add(dotnetSdk.Version, dotnetSdk);
+                    }
path: src/MSBuildLocator/DotNetSdkLocationHelper.cs
user: login: Forgind
body: Tiny nit:
I think we can skip to:
```suggestion
                    // Only add an SDK once, even if it's installed in multiple locations.
                    versionInstanceMap[dotnetSdk.Version] = dotnetSdk;
```

(One fewer dictionary access)
created_at: 2025-03-20T22:52:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,45 +71,101 @@ internal static class DotNetSdkLocationHelper
         }
 
         public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes)
-        {            
-            foreach (var basePath in GetDotNetBasePaths(workingDirectory))
+        {
+            string? bestSdkPath;
+            string[] allAvailableSdks;
+            try
+            {
+                AddUnmanagedDllResolver();
+
+                bestSdkPath = GetSdkFromGlobalSettings(workingDirectory);
+                allAvailableSdks = GetAllAvailableSDKs().ToArray();
+            }
+            finally
+            {
+                RemoveUnmanagedDllResolver();
+            }
+
+            Dictionary<Version, VisualStudioInstance?> versionInstanceMap = new();
+            foreach (var basePath in allAvailableSdks)
             {
                 var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes);
                 if (dotnetSdk != null)
                 {
-                    yield return dotnetSdk;
+                    // We want to return the best SDK first
+                    if (dotnetSdk.VisualStudioRootPath == bestSdkPath)
+                    {
+                        // We will add a null entry to the map to ensure we do not add the same SDK from a different location.
+                        versionInstanceMap[dotnetSdk.Version] = null;
+                        yield return dotnetSdk;
+                    }
+
+                    // Only add an SDK once, even if it's installed in multiple locations.
+                    if (!versionInstanceMap.ContainsKey(dotnetSdk.Version))
+                    {
+                        versionInstanceMap.Add(dotnetSdk.Version, dotnetSdk);
+                    }
path: src/MSBuildLocator/DotNetSdkLocationHelper.cs
user: login: JoeRobich
body: So I am unsure if it is a benefit or not, by keeping the first SDK for each discovered version we maintain a preference for SDKs in the following install location ordering.

1. DOTNET_ROOT
2. Current process path if MSBuild.Locator is called from dotnet.exe
3. DOTNET_HOST_PATH
4. DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR
5. PATH
created_at: 2025-03-25T05:50:47Z
author_association: MEMBER
diff_hunk: @@ -71,45 +71,101 @@ internal static class DotNetSdkLocationHelper
         }
 
         public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes)
-        {            
-            foreach (var basePath in GetDotNetBasePaths(workingDirectory))
+        {
+            string? bestSdkPath;
+            string[] allAvailableSdks;
+            try
+            {
+                AddUnmanagedDllResolver();
+
+                bestSdkPath = GetSdkFromGlobalSettings(workingDirectory);
+                allAvailableSdks = GetAllAvailableSDKs().ToArray();
+            }
+            finally
+            {
+                RemoveUnmanagedDllResolver();
+            }
+
+            Dictionary<Version, VisualStudioInstance?> versionInstanceMap = new();
+            foreach (var basePath in allAvailableSdks)
             {
                 var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes);
                 if (dotnetSdk != null)
                 {
-                    yield return dotnetSdk;
+                    // We want to return the best SDK first
+                    if (dotnetSdk.VisualStudioRootPath == bestSdkPath)
+                    {
+                        // We will add a null entry to the map to ensure we do not add the same SDK from a different location.
+                        versionInstanceMap[dotnetSdk.Version] = null;
+                        yield return dotnetSdk;
+                    }
+
+                    // Only add an SDK once, even if it's installed in multiple locations.
+                    if (!versionInstanceMap.ContainsKey(dotnetSdk.Version))
+                    {
+                        versionInstanceMap.Add(dotnetSdk.Version, dotnetSdk);
+                    }
path: src/MSBuildLocator/DotNetSdkLocationHelper.cs
user: login: Forgind
body: I'm trying to figure out if it matters or not, and I haven't come up with an answer, so playing it safe is probably a good idea.
created_at: 2025-03-25T20:31:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,6 +37,14 @@ public class VisualStudioInstanceQueryOptions
         ///     Defaults to <see langword="false"/>. Set this to <see langword="true"/> only if your application has special logic to handle loading an incompatible SDK, such as launching a new process with the target SDK's runtime.
         /// </remarks.
         public bool AllowAllRuntimeVersions { get; set; } = false;
+
+        /// <summary>
+        ///     Allow discovery of .NET SDK versions from all discovered dotnet install locations.
+        /// </summary>
+        /// <remarks>
+        ///     Defaults to <see langword="false"/>. Set this to <see langword="true"/> only if you do not mind behaving differently than the dotnet muxer.
path: src/MSBuildLocator/VisualStudioInstanceQueryOptions.cs
user: login: rainersigwald
body: ```suggestion
        ///     Defaults to <see langword="false"/>. Set this to <see langword="true"/> only if you do not mind behaving differently than a command-line dotnet invocation.
```
created_at: 2025-03-28T14:05:14Z
author_association: MEMBER
diff_hunk: @@ -71,45 +71,101 @@ internal static class DotNetSdkLocationHelper
         }
 
         public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes)
-        {            
-            foreach (var basePath in GetDotNetBasePaths(workingDirectory))
+        {
+            string? bestSdkPath;
+            string[] allAvailableSdks;
+            try
+            {
+                AddUnmanagedDllResolver();
+
+                bestSdkPath = GetSdkFromGlobalSettings(workingDirectory);
+                allAvailableSdks = GetAllAvailableSDKs().ToArray();
+            }
+            finally
+            {
+                RemoveUnmanagedDllResolver();
+            }
+
+            Dictionary<Version, VisualStudioInstance?> versionInstanceMap = new();
+            foreach (var basePath in allAvailableSdks)
             {
                 var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes);
                 if (dotnetSdk != null)
                 {
-                    yield return dotnetSdk;
+                    // We want to return the best SDK first
+                    if (dotnetSdk.VisualStudioRootPath == bestSdkPath)
+                    {
+                        // We will add a null entry to the map to ensure we do not add the same SDK from a different location.
+                        versionInstanceMap[dotnetSdk.Version] = null;
+                        yield return dotnetSdk;
+                    }
+
+                    // Only add an SDK once, even if it's installed in multiple locations.
+                    if (!versionInstanceMap.ContainsKey(dotnetSdk.Version))
+                    {
+                        versionInstanceMap.Add(dotnetSdk.Version, dotnetSdk);
+                    }
path: src/MSBuildLocator/DotNetSdkLocationHelper.cs
user: login: rainersigwald
body: `Dictionary<>.TryAdd()` would have that behavior right?
created_at: 2025-03-28T14:39:15Z
author_association: MEMBER
diff_hunk: @@ -70,46 +70,107 @@ internal static class DotNetSdkLocationHelper
                 discoveryType: DiscoveryType.DotNetSdk);
         }
 
-        public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes)
-        {            
-            foreach (var basePath in GetDotNetBasePaths(workingDirectory))
+        public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes, bool allowAllDotnetLocations)
+        {
+            string? bestSdkPath;
+            string[] allAvailableSdks;
+            try
+            {
+                AddUnmanagedDllResolver();
+
+                bestSdkPath = GetSdkFromGlobalSettings(workingDirectory);
+                allAvailableSdks = GetAllAvailableSDKs(allowAllDotnetLocations).ToArray();
+            }
+            finally
+            {
+                RemoveUnmanagedDllResolver();
+            }
+
+            Dictionary<Version, VisualStudioInstance?> versionInstanceMap = new();
+            foreach (var basePath in allAvailableSdks)
             {
                 var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes);
                 if (dotnetSdk != null)
                 {
-                    yield return dotnetSdk;
+                    // We want to return the best SDK first
+                    if (dotnetSdk.VisualStudioRootPath == bestSdkPath)
+                    {
+                        // We will add a null entry to the map to ensure we do not add the same SDK from a different location.
+                        versionInstanceMap[dotnetSdk.Version] = null;
+                        yield return dotnetSdk;
+                    }
+
+                    // Only add an SDK once, even if it's installed in multiple locations.
+                    if (!versionInstanceMap.ContainsKey(dotnetSdk.Version))
+                    {
+                        versionInstanceMap.Add(dotnetSdk.Version, dotnetSdk);
+                    }
                 }
             }
-        }
 
-        private static IEnumerable<string> GetDotNetBasePaths(string workingDirectory)
-        {
-            try
+            // We want to return the newest SDKs first. Using OfType will remove the null entry added if we found the best SDK.
+            var instances = versionInstanceMap.Values.OfType<VisualStudioInstance>().OrderByDescending(i => i.Version);
path: src/MSBuildLocator/DotNetSdkLocationHelper.cs
user: login: rainersigwald
body: I am finding this sort confusing with respect to your comment above about keeping the precedence order. That is just per-version, but you'd prefer the overall results sorted by version?

Can you go into more detail on your use case?
created_at: 2025-03-28T14:43:10Z
author_association: MEMBER
diff_hunk: @@ -71,45 +71,101 @@ internal static class DotNetSdkLocationHelper
         }
 
         public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes)
-        {            
-            foreach (var basePath in GetDotNetBasePaths(workingDirectory))
+        {
+            string? bestSdkPath;
+            string[] allAvailableSdks;
+            try
+            {
+                AddUnmanagedDllResolver();
+
+                bestSdkPath = GetSdkFromGlobalSettings(workingDirectory);
+                allAvailableSdks = GetAllAvailableSDKs().ToArray();
+            }
+            finally
+            {
+                RemoveUnmanagedDllResolver();
+            }
+
+            Dictionary<Version, VisualStudioInstance?> versionInstanceMap = new();
+            foreach (var basePath in allAvailableSdks)
             {
                 var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes);
                 if (dotnetSdk != null)
                 {
-                    yield return dotnetSdk;
+                    // We want to return the best SDK first
+                    if (dotnetSdk.VisualStudioRootPath == bestSdkPath)
+                    {
+                        // We will add a null entry to the map to ensure we do not add the same SDK from a different location.
+                        versionInstanceMap[dotnetSdk.Version] = null;
+                        yield return dotnetSdk;
+                    }
+
+                    // Only add an SDK once, even if it's installed in multiple locations.
+                    if (!versionInstanceMap.ContainsKey(dotnetSdk.Version))
+                    {
+                        versionInstanceMap.Add(dotnetSdk.Version, dotnetSdk);
+                    }
path: src/MSBuildLocator/DotNetSdkLocationHelper.cs
user: login: Forgind
body: That isn't available on net46, though
created_at: 2025-03-28T18:03:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -71,45 +71,101 @@ internal static class DotNetSdkLocationHelper
         }
 
         public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes)
-        {            
-            foreach (var basePath in GetDotNetBasePaths(workingDirectory))
+        {
+            string? bestSdkPath;
+            string[] allAvailableSdks;
+            try
+            {
+                AddUnmanagedDllResolver();
+
+                bestSdkPath = GetSdkFromGlobalSettings(workingDirectory);
+                allAvailableSdks = GetAllAvailableSDKs().ToArray();
+            }
+            finally
+            {
+                RemoveUnmanagedDllResolver();
+            }
+
+            Dictionary<Version, VisualStudioInstance?> versionInstanceMap = new();
+            foreach (var basePath in allAvailableSdks)
             {
                 var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes);
                 if (dotnetSdk != null)
                 {
-                    yield return dotnetSdk;
+                    // We want to return the best SDK first
+                    if (dotnetSdk.VisualStudioRootPath == bestSdkPath)
+                    {
+                        // We will add a null entry to the map to ensure we do not add the same SDK from a different location.
+                        versionInstanceMap[dotnetSdk.Version] = null;
+                        yield return dotnetSdk;
+                    }
+
+                    // Only add an SDK once, even if it's installed in multiple locations.
+                    if (!versionInstanceMap.ContainsKey(dotnetSdk.Version))
+                    {
+                        versionInstanceMap.Add(dotnetSdk.Version, dotnetSdk);
+                    }
path: src/MSBuildLocator/DotNetSdkLocationHelper.cs
user: login: rainersigwald
body: Isn't this code used only on core?
created_at: 2025-03-28T19:03:52Z
author_association: MEMBER
diff_hunk: @@ -71,45 +71,101 @@ internal static class DotNetSdkLocationHelper
         }
 
         public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes)
-        {            
-            foreach (var basePath in GetDotNetBasePaths(workingDirectory))
+        {
+            string? bestSdkPath;
+            string[] allAvailableSdks;
+            try
+            {
+                AddUnmanagedDllResolver();
+
+                bestSdkPath = GetSdkFromGlobalSettings(workingDirectory);
+                allAvailableSdks = GetAllAvailableSDKs().ToArray();
+            }
+            finally
+            {
+                RemoveUnmanagedDllResolver();
+            }
+
+            Dictionary<Version, VisualStudioInstance?> versionInstanceMap = new();
+            foreach (var basePath in allAvailableSdks)
             {
                 var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes);
                 if (dotnetSdk != null)
                 {
-                    yield return dotnetSdk;
+                    // We want to return the best SDK first
+                    if (dotnetSdk.VisualStudioRootPath == bestSdkPath)
+                    {
+                        // We will add a null entry to the map to ensure we do not add the same SDK from a different location.
+                        versionInstanceMap[dotnetSdk.Version] = null;
+                        yield return dotnetSdk;
+                    }
+
+                    // Only add an SDK once, even if it's installed in multiple locations.
+                    if (!versionInstanceMap.ContainsKey(dotnetSdk.Version))
+                    {
+                        versionInstanceMap.Add(dotnetSdk.Version, dotnetSdk);
+                    }
path: src/MSBuildLocator/DotNetSdkLocationHelper.cs
user: login: Forgind
body: Good point 👍
created_at: 2025-03-28T19:45:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,46 +70,107 @@ internal static class DotNetSdkLocationHelper
                 discoveryType: DiscoveryType.DotNetSdk);
         }
 
-        public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes)
-        {            
-            foreach (var basePath in GetDotNetBasePaths(workingDirectory))
+        public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes, bool allowAllDotnetLocations)
+        {
+            string? bestSdkPath;
+            string[] allAvailableSdks;
+            try
+            {
+                AddUnmanagedDllResolver();
+
+                bestSdkPath = GetSdkFromGlobalSettings(workingDirectory);
+                allAvailableSdks = GetAllAvailableSDKs(allowAllDotnetLocations).ToArray();
+            }
+            finally
+            {
+                RemoveUnmanagedDllResolver();
+            }
+
+            Dictionary<Version, VisualStudioInstance?> versionInstanceMap = new();
+            foreach (var basePath in allAvailableSdks)
             {
                 var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes);
                 if (dotnetSdk != null)
                 {
-                    yield return dotnetSdk;
+                    // We want to return the best SDK first
+                    if (dotnetSdk.VisualStudioRootPath == bestSdkPath)
+                    {
+                        // We will add a null entry to the map to ensure we do not add the same SDK from a different location.
+                        versionInstanceMap[dotnetSdk.Version] = null;
+                        yield return dotnetSdk;
+                    }
+
+                    // Only add an SDK once, even if it's installed in multiple locations.
+                    if (!versionInstanceMap.ContainsKey(dotnetSdk.Version))
+                    {
+                        versionInstanceMap.Add(dotnetSdk.Version, dotnetSdk);
+                    }
                 }
             }
-        }
 
-        private static IEnumerable<string> GetDotNetBasePaths(string workingDirectory)
-        {
-            try
+            // We want to return the newest SDKs first. Using OfType will remove the null entry added if we found the best SDK.
+            var instances = versionInstanceMap.Values.OfType<VisualStudioInstance>().OrderByDescending(i => i.Version);
path: src/MSBuildLocator/DotNetSdkLocationHelper.cs
user: login: JoeRobich
body: I was trying to preserve the behavior from this comment:
```
// We want to return the newest SDKs first, however, so iterate over the list in reverse order.
```
But you are right. I cannot guarantee this will maintain the precedence ordering.

We intend to use this from the MSBuildWorkspace buildhost where we are looking for an SDK and we don't really care where it comes from. If they have pinned a version then we would like to find it, otherwise we would take the SDK with the highest version from any install location.
created_at: 2025-03-28T20:43:04Z
author_association: MEMBER
diff_hunk: @@ -71,45 +71,101 @@ internal static class DotNetSdkLocationHelper
         }
 
         public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes)
-        {            
-            foreach (var basePath in GetDotNetBasePaths(workingDirectory))
+        {
+            string? bestSdkPath;
+            string[] allAvailableSdks;
+            try
+            {
+                AddUnmanagedDllResolver();
+
+                bestSdkPath = GetSdkFromGlobalSettings(workingDirectory);
+                allAvailableSdks = GetAllAvailableSDKs().ToArray();
+            }
+            finally
+            {
+                RemoveUnmanagedDllResolver();
+            }
+
+            Dictionary<Version, VisualStudioInstance?> versionInstanceMap = new();
+            foreach (var basePath in allAvailableSdks)
             {
                 var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes);
                 if (dotnetSdk != null)
                 {
-                    yield return dotnetSdk;
+                    // We want to return the best SDK first
+                    if (dotnetSdk.VisualStudioRootPath == bestSdkPath)
+                    {
+                        // We will add a null entry to the map to ensure we do not add the same SDK from a different location.
+                        versionInstanceMap[dotnetSdk.Version] = null;
+                        yield return dotnetSdk;
+                    }
+
+                    // Only add an SDK once, even if it's installed in multiple locations.
+                    if (!versionInstanceMap.ContainsKey(dotnetSdk.Version))
+                    {
+                        versionInstanceMap.Add(dotnetSdk.Version, dotnetSdk);
+                    }
path: src/MSBuildLocator/DotNetSdkLocationHelper.cs
user: login: JoeRobich
body: I had thought about TryAdd and talked myself out of it without realizing this code only builds under .NET.
created_at: 2025-03-28T20:44:43Z
author_association: MEMBER
diff_hunk: @@ -71,45 +71,101 @@ internal static class DotNetSdkLocationHelper
         }
 
         public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes)
-        {            
-            foreach (var basePath in GetDotNetBasePaths(workingDirectory))
+        {
+            string? bestSdkPath;
+            string[] allAvailableSdks;
+            try
+            {
+                AddUnmanagedDllResolver();
+
+                bestSdkPath = GetSdkFromGlobalSettings(workingDirectory);
+                allAvailableSdks = GetAllAvailableSDKs().ToArray();
+            }
+            finally
+            {
+                RemoveUnmanagedDllResolver();
+            }
+
+            Dictionary<Version, VisualStudioInstance?> versionInstanceMap = new();
+            foreach (var basePath in allAvailableSdks)
             {
                 var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes);
                 if (dotnetSdk != null)
                 {
-                    yield return dotnetSdk;
+                    // We want to return the best SDK first
+                    if (dotnetSdk.VisualStudioRootPath == bestSdkPath)
+                    {
+                        // We will add a null entry to the map to ensure we do not add the same SDK from a different location.
+                        versionInstanceMap[dotnetSdk.Version] = null;
+                        yield return dotnetSdk;
+                    }
+
+                    // Only add an SDK once, even if it's installed in multiple locations.
+                    if (!versionInstanceMap.ContainsKey(dotnetSdk.Version))
+                    {
+                        versionInstanceMap.Add(dotnetSdk.Version, dotnetSdk);
+                    }
path: src/MSBuildLocator/DotNetSdkLocationHelper.cs
user: login: rainersigwald
body: In your defense the repo has "MSBuild" in the name so thinking everything targets v.OldAndBusted is not a bad assumption!
created_at: 2025-03-28T21:27:55Z
author_association: MEMBER
diff_hunk: @@ -70,46 +70,107 @@ internal static class DotNetSdkLocationHelper
                 discoveryType: DiscoveryType.DotNetSdk);
         }
 
-        public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes)
-        {            
-            foreach (var basePath in GetDotNetBasePaths(workingDirectory))
+        public static IEnumerable<VisualStudioInstance> GetInstances(string workingDirectory, bool allowQueryAllRuntimes, bool allowAllDotnetLocations)
+        {
+            string? bestSdkPath;
+            string[] allAvailableSdks;
+            try
+            {
+                AddUnmanagedDllResolver();
+
+                bestSdkPath = GetSdkFromGlobalSettings(workingDirectory);
+                allAvailableSdks = GetAllAvailableSDKs(allowAllDotnetLocations).ToArray();
+            }
+            finally
+            {
+                RemoveUnmanagedDllResolver();
+            }
+
+            Dictionary<Version, VisualStudioInstance?> versionInstanceMap = new();
+            foreach (var basePath in allAvailableSdks)
             {
                 var dotnetSdk = GetInstance(basePath, allowQueryAllRuntimes);
                 if (dotnetSdk != null)
                 {
-                    yield return dotnetSdk;
+                    // We want to return the best SDK first
+                    if (dotnetSdk.VisualStudioRootPath == bestSdkPath)
+                    {
+                        // We will add a null entry to the map to ensure we do not add the same SDK from a different location.
+                        versionInstanceMap[dotnetSdk.Version] = null;
+                        yield return dotnetSdk;
+                    }
+
+                    // Only add an SDK once, even if it's installed in multiple locations.
+                    if (!versionInstanceMap.ContainsKey(dotnetSdk.Version))
+                    {
+                        versionInstanceMap.Add(dotnetSdk.Version, dotnetSdk);
+                    }
                 }
             }
-        }
 
-        private static IEnumerable<string> GetDotNetBasePaths(string workingDirectory)
-        {
-            try
+            // We want to return the newest SDKs first. Using OfType will remove the null entry added if we found the best SDK.
+            var instances = versionInstanceMap.Values.OfType<VisualStudioInstance>().OrderByDescending(i => i.Version);
path: src/MSBuildLocator/DotNetSdkLocationHelper.cs
user: login: rainersigwald
body: Yeah that makes sense I think. The question I have is whether we should embed the sort here, or push it to the caller's layer. I don't think I have super strong opinions so this is fine.
created_at: 2025-03-28T21:29:25Z
author_association: MEMBER

PR comments:
user: login: YuliiaKovalova
created_at: 2025-03-28T09:04:01Z
body: it looks good to me, @rainersigwald do you have any concerns? 
author_association: CONTRIBUTOR
user: login: JoeRobich
created_at: 2025-03-28T22:29:44Z
body: > I guess this should be a minor version bump, for adding the feature to request this?

Want me to do that in this PR? or as a follow up?
author_association: MEMBER
user: login: YuliiaKovalova
created_at: 2025-03-31T10:01:51Z
body: > > I guess this should be a minor version bump, for adding the feature to request this?
> 
> Want me to do that in this PR? or as a follow up?

I have bumped the version , no worries!
Thank you for the contribution.
author_association: CONTRIBUTOR
user: login: rainersigwald
created_at: 2025-03-31T14:09:02Z
body: @darikprescott Note that being able to _locate_ the new SDKs does not do anything to make them more loadable; if you wanted to take advantage of this you'd need a big bag of tricks to find and load a newer runtime to load the newer SDK.
author_association: MEMBER
user: login: YuliiaKovalova
created_at: 2025-03-31T15:56:06Z
body: It has been released:https://www.nuget.org/packages/Microsoft.Build.Locator/1.8.1 :)
author_association: CONTRIBUTOR