Repository: dotnet/roslyn
Pull request #78647: Update Workspace.MSBuild to reference Microsoft.Build.Framework.
Author: login: JoeRobich
Labels: name: Area-IDE
name: VSCode
Created: 2025-05-19T20:41:14Z | Merged: 2025-05-23T13:51:21Z

Workspace.MSBuild requires a MSBuild dependency because our API references their `ILogger` interface. This PR changes the MSBuild references to be Microsoft.Build.Framework, which is where the interface is defined.

In order to fully drop the Microsoft.Build dependency I also pulled in the VS-SolutionPersister changes which bring solution parsing back in-proc.


Diff:
diff --git a/eng/Directory.Packages.props b/eng/Directory.Packages.props
index 682905fac91fa..bb3170ec57059 100644
--- a/eng/Directory.Packages.props
+++ b/eng/Directory.Packages.props
@@ -146,6 +146,11 @@
     <PackageVersion Include="Microsoft.TestPlatform.TranslationLayer" Version="$(MicrosoftNETTestSdkVersion)" />
     <PackageVersion Include="Microsoft.TestPlatform.ObjectModel" Version="$(MicrosoftNETTestSdkVersion)" />
 
+    <!--
+      MSBuildWorkspace
+    -->
+    <PackageVersion Include="Microsoft.VisualStudio.SolutionPersistence" Version="1.0.52" />
+
     <!--
       Analyzers
     -->
diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs
index 64d3082ef258f..0730d4b501e9c 100644
--- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs
+++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs
@@ -57,19 +57,7 @@ public async Task OpenSolutionAsync(string solutionFilePath)
         _logger.LogInformation(string.Format(LanguageServerResources.Loading_0, solutionFilePath));
         ProjectFactory.SolutionPath = solutionFilePath;
 
-        // We'll load solutions out-of-proc, since it's possible we might be running on a runtime that doesn't have a matching SDK installed,
-        // and we don't want any MSBuild registration to set environment variables in our process that might impact child processes.
-        await using var buildHostProcessManager = new BuildHostProcessManager(globalMSBuildProperties: AdditionalProperties, loggerFactory: LoggerFactory);
-        var buildHost = await buildHostProcessManager.GetBuildHostAsync(BuildHostProcessKind.NetCore, CancellationToken.None);
-
-        // If we don't have a .NET Core SDK on this machine at all, try .NET Framework
-        if (!await buildHost.HasUsableMSBuildAsync(solutionFilePath, CancellationToken.None))
-        {
-            var kind = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? BuildHostProcessKind.NetFramework : BuildHostProcessKind.Mono;
-            buildHost = await buildHostProcessManager.GetBuildHostAsync(kind, CancellationToken.None);
-        }
-
-        var projects = await buildHost.GetProjectsInSolutionAsync(solutionFilePath, CancellationToken.None);
+        var (_, projects) = await SolutionFileReader.ReadSolutionFileAsync(solutionFilePath, CancellationToken.None);
         foreach (var (path, guid) in projects)
         {
             await BeginLoadingProjectAsync(path, guid);
diff --git a/src/LanguageServer/ProtocolUnitTests/Hover/HoverTests.cs b/src/LanguageServer/ProtocolUnitTests/Hover/HoverTests.cs
index af452f1de5f70..7c51e7df39f74 100644
--- a/src/LanguageServer/ProtocolUnitTests/Hover/HoverTests.cs
+++ b/src/LanguageServer/ProtocolUnitTests/Hover/HoverTests.cs
@@ -7,7 +7,6 @@
 using System.Linq;
 using System.Threading;
 using System.Threading.Tasks;
-using Microsoft.Build.Evaluation;
 using Microsoft.CodeAnalysis.PooledObjects;
 using Roslyn.Test.Utilities;
 using Roslyn.Text.Adornments;
diff --git a/src/Workspaces/MSBuild/BuildHost/BuildHost.cs b/src/Workspaces/MSBuild/BuildHost/BuildHost.cs
index 86c59f00d115f..92abd8997af61 100644
--- a/src/Workspaces/MSBuild/BuildHost/BuildHost.cs
+++ b/src/Workspaces/MSBuild/BuildHost/BuildHost.cs
@@ -9,7 +9,6 @@
 using System.Runtime.CompilerServices;
 using System.Threading;
 using System.Threading.Tasks;
-using Microsoft.Build.Construction;
 using Microsoft.Build.Locator;
 using Microsoft.Build.Logging;
 using Roslyn.Utilities;
@@ -133,31 +132,6 @@ private void EnsureMSBuildLoaded(string projectFilePath)
         Contract.ThrowIfFalse(TryEnsureMSBuildLoaded(projectFilePath), $"We don't have an MSBuild to use; {nameof(HasUsableMSBuild)} should have been called first to check.");
     }
 
-    public ImmutableArray<(string ProjectPath, string ProjectGuid)> GetProjectsInSolution(string solutionFilePath)
-    {
-        EnsureMSBuildLoaded(solutionFilePath);
-        return GetProjectsInSolutionCore(solutionFilePath);
-    }
-
-    [MethodImpl(MethodImplOptions.NoInlining)] // Do not inline this, since this uses MSBuild types which are being loaded by the caller
-    private static ImmutableArray<(string ProjectPath, string ProjectGuid)> GetProjectsInSolutionCore(string solutionFilePath)
-    {
-        // WARNING: do not use a lambda in this function, as it internally will be put in a class that contains other lambdas used in
-        // TryEnsureMSBuildLoaded; on Mono this causes type load errors.
-
-        var builder = ImmutableArray.CreateBuilder<(string ProjectPath, string ProjectGuid)>();
-
-        foreach (var project in SolutionFile.Parse(solutionFilePath).ProjectsInOrder)
-        {
-            if (project.ProjectType != SolutionProjectType.SolutionFolder)
-            {
-                builder.Add((project.AbsolutePath, project.ProjectGuid));
-            }
-        }
-
-        return builder.ToImmutableAndClear();
-    }
-
     /// <summary>
     /// Returns the target ID of the <see cref="ProjectFile"/> object created for this.
     /// </summary>
diff --git a/src/Workspaces/MSBuild/BuildHost/Rpc/Contracts/IBuildHost.cs b/src/Workspaces/MSBuild/BuildHost/Rpc/Contracts/IBuildHost.cs
index 4bde82f6a5a19..8724cc7c991c6 100644
--- a/src/Workspaces/MSBuild/BuildHost/Rpc/Contracts/IBuildHost.cs
+++ b/src/Workspaces/MSBuild/BuildHost/Rpc/Contracts/IBuildHost.cs
@@ -2,7 +2,6 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 // See the LICENSE file in the project root for more information.
 
-using System.Collections.Immutable;
 using System.Threading;
 using System.Threading.Tasks;
 
@@ -14,7 +13,6 @@ namespace Microsoft.CodeAnalysis.MSBuild;
 internal interface IBuildHost
 {
     bool HasUsableMSBuild(string projectOrSolutionFilePath);
-    ImmutableArray<(string ProjectPath, string ProjectGuid)> GetProjectsInSolution(string solutionFilePath);
     Task<int> LoadProjectFileAsync(string projectFilePath, string languageName, CancellationToken cancellationToken);
 
     /// <summary>Permits loading a project file which only exists in-memory, for example, for file-based program scenarios.</summary>
diff --git a/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.cs b/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.cs
index 3b87ffde7b420..f2d8c7d4db940 100644
--- a/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.cs
+++ b/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.cs
@@ -167,51 +167,20 @@ public async Task<SolutionInfo> LoadSolutionInfoAsync(
             throw new ArgumentNullException(nameof(solutionFilePath));
         }
 
-        if (!_pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath))
-        {
-            // TryGetAbsoluteSolutionPath should throw before we get here.
-            return null!;
-        }
-
-        var projectFilter = ImmutableHashSet<string>.Empty;
-        if (SolutionFilterReader.IsSolutionFilterFilename(absoluteSolutionPath) &&
-            !SolutionFilterReader.TryRead(absoluteSolutionPath, _pathResolver, out absoluteSolutionPath, out projectFilter))
-        {
-            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_filter_0, solutionFilePath));
-        }
+        var (absoluteSolutionPath, projects) = await SolutionFileReader.ReadSolutionFileAsync(solutionFilePath, _pathResolver, cancellationToken).ConfigureAwait(false);
+        var projectPaths = projects.SelectAsArray(p => p.ProjectPath);
 
         using (_dataGuard.DisposableWait(cancellationToken))
         {
-            this.SetSolutionProperties(absoluteSolutionPath);
+            SetSolutionProperties(absoluteSolutionPath);
         }
 
-        var solutionFile = MSB.Construction.SolutionFile.Parse(absoluteSolutionPath);
         var reportingMode = GetReportingModeForUnrecognizedProjects();
 
         var reportingOptions = new DiagnosticReportingOptions(
             onPathFailure: reportingMode,
             onLoaderFailure: reportingMode);
 
-        var projectPaths = ImmutableArray.CreateBuilder<string>();
-
-        // load all the projects
-        foreach (var project in solutionFile.ProjectsInOrder)
-        {
-            cancellationToken.ThrowIfCancellationRequested();
-
-            if (project.ProjectType == MSB.Construction.SolutionProjectType.SolutionFolder)
-            {
-                continue;
-            }
-
-            // Load project if we have an empty project filter and the project path is present.
-            if (projectFilter.IsEmpty ||
-                projectFilter.Contains(project.AbsolutePath))
-            {
-                projectPaths.Add(project.RelativePath);
-            }
-        }
-
         var buildHostProcessManager = new BuildHostProcessManager(Properties, loggerFactory: _loggerFactory);
         await using var _ = buildHostProcessManager.ConfigureAwait(false);
 
@@ -221,7 +190,7 @@ public async Task<SolutionInfo> LoadSolutionInfoAsync(
             _pathResolver,
             _projectFileExtensionRegistry,
             buildHostProcessManager,
-            projectPaths.ToImmutable(),
+            projectPaths,
             // TryGetAbsoluteSolutionPath should not return an invalid path
             baseDirectory: Path.GetDirectoryName(absoluteSolutionPath)!,
             Properties,
@@ -231,14 +200,14 @@ public async Task<SolutionInfo> LoadSolutionInfoAsync(
             discoveredProjectOptions: reportingOptions,
             preferMetadataForReferencesOfDiscoveredProjects: false);
 
-        var projects = await worker.LoadAsync(cancellationToken).ConfigureAwait(false);
+        var projectInfos = await worker.LoadAsync(cancellationToken).ConfigureAwait(false);
 
         // construct workspace from loaded project infos
         return SolutionInfo.Create(
             SolutionId.CreateNewId(debugName: absoluteSolutionPath),
             version: default,
             absoluteSolutionPath,
-            projects);
+            projectInfos);
     }
 
     /// <summary>
diff --git a/src/Workspaces/MSBuild/Core/MSBuild/PathResolver.cs b/src/Workspaces/MSBuild/Core/MSBuild/PathResolver.cs
index 9786b03d0a0b8..b8ec8a763b0f3 100644
--- a/src/Workspaces/MSBuild/Core/MSBuild/PathResolver.cs
+++ b/src/Workspaces/MSBuild/Core/MSBuild/PathResolver.cs
@@ -11,9 +11,9 @@ namespace Microsoft.CodeAnalysis.MSBuild;
 
 internal sealed class PathResolver
 {
-    private readonly DiagnosticReporter _diagnosticReporter;
+    private readonly DiagnosticReporter? _diagnosticReporter;
 
-    public PathResolver(DiagnosticReporter diagnosticReporter)
+    public PathResolver(DiagnosticReporter? diagnosticReporter)
     {
         _diagnosticReporter = diagnosticReporter;
     }
@@ -26,14 +26,14 @@ public bool TryGetAbsoluteSolutionPath(string path, string baseDirectory, Diagno
         }
         catch (Exception)
         {
-            _diagnosticReporter.Report(reportingMode, string.Format(WorkspacesResources.Invalid_solution_file_path_colon_0, path));
+            _diagnosticReporter?.Report(reportingMode, string.Format(WorkspacesResources.Invalid_solution_file_path_colon_0, path));
             absolutePath = null;
             return false;
         }
 
         if (!File.Exists(absolutePath))
         {
-            _diagnosticReporter.Report(
+            _diagnosticReporter?.Report(
                 reportingMode,
                 string.Format(WorkspacesResources.Solution_file_not_found_colon_0, absolutePath),
                 msg => new FileNotFoundException(msg));
@@ -51,14 +51,14 @@ public bool TryGetAbsoluteProjectPath(string path, string baseDirectory, Diagnos
         }
         catch (Exception)
         {
-            _diagnosticReporter.Report(reportingMode, string.Format(WorkspacesResources.Invalid_project_file_path_colon_0, path));
+            _diagnosticReporter?.Report(reportingMode, string.Format(WorkspacesResources.Invalid_project_file_path_colon_0, path));
             absolutePath = null;
             return false;
         }
 
         if (!File.Exists(absolutePath))
         {
-            _diagnosticReporter.Report(
+            _diagnosticReporter?.Report(
                 reportingMode,
                 string.Format(WorkspacesResources.Project_file_not_found_colon_0, absolutePath),
                 msg => new FileNotFoundException(msg));
diff --git a/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.SolutionFilterReader.cs b/src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.SolutionFilterReader.cs
similarity index 80%
rename from src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.SolutionFilterReader.cs
rename to src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.SolutionFilterReader.cs
index 4cfa80dae6205..d62ca9af643aa 100644
--- a/src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.SolutionFilterReader.cs
+++ b/src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.SolutionFilterReader.cs
@@ -11,7 +11,7 @@
 
 namespace Microsoft.CodeAnalysis.MSBuild;
 
-public partial class MSBuildProjectLoader
+internal partial class SolutionFileReader
 {
     private static class SolutionFilterReader
     {
@@ -35,13 +35,7 @@ public static bool TryRead(string filterFilename, PathResolver pathResolver, [No
                     return false;
                 }
 
-                if (!pathResolver.TryGetAbsoluteSolutionPath(solutionPath, baseDirectory, DiagnosticReportingMode.Throw, out solutionFilename))
-                {
-                    // TryGetAbsoluteSolutionPath should throw before we get here.
-                    solutionFilename = string.Empty;
-                    projectFilter = [];
-                    return false;
-                }
+                Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteSolutionPath(solutionPath, baseDirectory, DiagnosticReportingMode.Throw, out solutionFilename));
 
                 if (!File.Exists(solutionFilename))
                 {
@@ -64,10 +58,8 @@ public static bool TryRead(string filterFilename, PathResolver pathResolver, [No
                     }
 
                     // Fill the filter with the absolute project paths.
-                    if (pathResolver.TryGetAbsoluteProjectPath(projectPath, baseDirectory, DiagnosticReportingMode.Throw, out var absoluteProjectPath))
-                    {
-                        filterProjects.Add(absoluteProjectPath);
-                    }
+                    Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteProjectPath(projectPath, baseDirectory, DiagnosticReportingMode.Throw, out var absoluteProjectPath));
+                    filterProjects.Add(absoluteProjectPath);
                 }
 
                 projectFilter = filterProjects.ToImmutable();
diff --git a/src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.cs b/src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.cs
new file mode 100644
index 0000000000000..ebad2a17130a1
--- /dev/null
+++ b/src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.cs
@@ -0,0 +1,75 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal partial class SolutionFileReader
+{
+    public static Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, CancellationToken cancellationToken)
+    {
+        return ReadSolutionFileAsync(solutionFilePath, new PathResolver(diagnosticReporter: null), cancellationToken);
+    }
+
+    public static async Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, CancellationToken cancellationToken)
+    {
+        Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath));
+
+        // When passed a solution filter, we need to read the filter file to get the solution path and included project paths.
+        var projectFilter = ImmutableHashSet<string>.Empty;
+        if (SolutionFilterReader.IsSolutionFilterFilename(absoluteSolutionPath) &&
+            !SolutionFilterReader.TryRead(absoluteSolutionPath, pathResolver, out absoluteSolutionPath, out projectFilter))
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_filter_0, solutionFilePath));
+        }
+
+        var projects = await TryReadSolutionFileAsync(absoluteSolutionPath, pathResolver, projectFilter, cancellationToken).ConfigureAwait(false);
+        if (!projects.HasValue)
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_0, absoluteSolutionPath));
+        }
+
+        return (absoluteSolutionPath, projects.Value);
+    }
+
+    private static async Task<ImmutableArray<(string ProjectPath, string ProjectGuid)>?> TryReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, ImmutableHashSet<string> projectFilter, CancellationToken cancellationToken)
+    {
+        var serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
+        if (serializer == null)
+        {
+            return null;
+        }
+
+        // The solution folder is the base directory for project paths.
+        var baseDirectory = Path.GetDirectoryName(solutionFilePath);
+        RoslynDebug.AssertNotNull(baseDirectory);
+
+        var solutionModel = await serializer.OpenAsync(solutionFilePath, cancellationToken).ConfigureAwait(false);
+
+        var builder = ImmutableArray.CreateBuilder<(string ProjectPath, string ProjectGuid)>();
+        foreach (var projectModel in solutionModel.SolutionProjects)
+        {
+            // If we are filtering based on a solution filter, then we need to verify the project is included.
+            if (!projectFilter.IsEmpty)
+            {
+                Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteProjectPath(projectModel.FilePath, baseDirectory, DiagnosticReportingMode.Throw, out var absoluteProjectPath));
+                if (!projectFilter.Contains(absoluteProjectPath))
+                {
+                    continue;
+                }
+            }
+
+            builder.Add((projectModel.FilePath, projectModel.Id.ToString()));
+        }
+
+        return builder.ToImmutable();
+    }
+}
diff --git a/src/Workspaces/MSBuild/Core/Microsoft.CodeAnalysis.Workspaces.MSBuild.csproj b/src/Workspaces/MSBuild/Core/Microsoft.CodeAnalysis.Workspaces.MSBuild.csproj
index 0b4d11ad2192c..fb336b31260da 100644
--- a/src/Workspaces/MSBuild/Core/Microsoft.CodeAnalysis.Workspaces.MSBuild.csproj
+++ b/src/Workspaces/MSBuild/Core/Microsoft.CodeAnalysis.Workspaces.MSBuild.csproj
@@ -22,10 +22,17 @@
     </PackageDescription>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.Build" />
-    <PackageReference Include="Microsoft.Build.Tasks.Core" />
+    <PackageReference Include="Microsoft.Build.Framework" />
     <PackageReference Include="Microsoft.Extensions.Logging" />
+    <PackageReference Include="Microsoft.VisualStudio.SolutionPersistence" />
     <PackageReference Include="Newtonsoft.Json" />
+    <!--
+      Since System.Text.Json is part of the .NET BCL we do not want to add
+      it as a package reference. Doing so will pin the version to what we use
+      in our build instead of allowing it to use the version from the .NET Runtime
+      we are running against.
+    -->
+    <PackageReference Include="System.Text.Json" Condition="'$(TargetFramework)' == 'net472'" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\Core\Portable\Microsoft.CodeAnalysis.Workspaces.csproj" />
diff --git a/src/Workspaces/MSBuild/Core/Rpc/RemoteBuildHost.cs b/src/Workspaces/MSBuild/Core/Rpc/RemoteBuildHost.cs
index eecafbfcbefda..1d6aeb03de599 100644
--- a/src/Workspaces/MSBuild/Core/Rpc/RemoteBuildHost.cs
+++ b/src/Workspaces/MSBuild/Core/Rpc/RemoteBuildHost.cs
@@ -23,9 +23,6 @@ public RemoteBuildHost(RpcClient client)
     public Task<bool> HasUsableMSBuildAsync(string projectOrSolutionFilePath, CancellationToken cancellationToken)
         => _client.InvokeAsync<bool>(BuildHostTargetObject, nameof(IBuildHost.HasUsableMSBuild), parameters: [projectOrSolutionFilePath], cancellationToken);
 
-    public Task<ImmutableArray<(string ProjectPath, string ProjectGuid)>> GetProjectsInSolutionAsync(string solutionFilePath, CancellationToken cancellationToken)
-        => _client.InvokeAsync<ImmutableArray<(string ProjectPath, string ProjectGuid)>>(BuildHostTargetObject, nameof(IBuildHost.GetProjectsInSolution), parameters: [solutionFilePath], cancellationToken);
-
     public async Task<RemoteProjectFile> LoadProjectFileAsync(string projectFilePath, string languageName, CancellationToken cancellationToken)
     {
         var remoteProjectFileTargetObject = await _client.InvokeAsync<int>(BuildHostTargetObject, nameof(IBuildHost.LoadProjectFileAsync), parameters: [projectFilePath, languageName], cancellationToken).ConfigureAwait(false);
diff --git a/src/Workspaces/MSBuild/Core/WorkspaceMSBuildResources.resx b/src/Workspaces/MSBuild/Core/WorkspaceMSBuildResources.resx
index 7bf2389e18ce7..a4f7ffbd318fe 100644
--- a/src/Workspaces/MSBuild/Core/WorkspaceMSBuildResources.resx
+++ b/src/Workspaces/MSBuild/Core/WorkspaceMSBuildResources.resx
@@ -123,6 +123,9 @@
   <data name="Failed_to_load_solution_filter_0" xml:space="preserve">
     <value>Failed to load solution filter: '{0}'</value>
   </data>
+  <data name="Failed_to_load_solution_0" xml:space="preserve">
+    <value>Failed to load solution: '{0}'</value>
+  </data>
   <data name="Found_project_reference_without_a_matching_metadata_reference_0" xml:space="preserve">
     <value>Found project reference without a matching metadata reference: {0}</value>
   </data>
diff --git a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.cs.xlf b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.cs.xlf
index a2069b38f5d33..5d46e4c8b7c56 100644
--- a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.cs.xlf
+++ b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.cs.xlf
@@ -2,6 +2,11 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="cs" original="../WorkspaceMSBuildResources.resx">
     <body>
+      <trans-unit id="Failed_to_load_solution_0">
+        <source>Failed to load solution: '{0}'</source>
+        <target state="new">Failed to load solution: '{0}'</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Failed_to_load_solution_filter_0">
         <source>Failed to load solution filter: '{0}'</source>
         <target state="translated">Nepovedlo se načíst filtr řešení: {0}</target>
diff --git a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.de.xlf b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.de.xlf
index 6125fa5918100..d21f9e68d2db9 100644
--- a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.de.xlf
+++ b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.de.xlf
@@ -2,6 +2,11 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="de" original="../WorkspaceMSBuildResources.resx">
     <body>
+      <trans-unit id="Failed_to_load_solution_0">
+        <source>Failed to load solution: '{0}'</source>
+        <target state="new">Failed to load solution: '{0}'</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Failed_to_load_solution_filter_0">
         <source>Failed to load solution filter: '{0}'</source>
         <target state="translated">Fehler beim Laden des Projektmappenfilters: "{0}"</target>
diff --git a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.es.xlf b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.es.xlf
index 9b38ece6f434a..17951e391426f 100644
--- a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.es.xlf
+++ b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.es.xlf
@@ -2,6 +2,11 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="es" original="../WorkspaceMSBuildResources.resx">
     <body>
+      <trans-unit id="Failed_to_load_solution_0">
+        <source>Failed to load solution: '{0}'</source>
+        <target state="new">Failed to load solution: '{0}'</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Failed_to_load_solution_filter_0">
         <source>Failed to load solution filter: '{0}'</source>
         <target state="translated">No se pudo cargar el filtro de la solución: "{0}"</target>
diff --git a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.fr.xlf b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.fr.xlf
index 1ea565215356e..d3a9a407cb32a 100644
--- a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.fr.xlf
+++ b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.fr.xlf
@@ -2,6 +2,11 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="fr" original="../WorkspaceMSBuildResources.resx">
     <body>
+      <trans-unit id="Failed_to_load_solution_0">
+        <source>Failed to load solution: '{0}'</source>
+        <target state="new">Failed to load solution: '{0}'</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Failed_to_load_solution_filter_0">
         <source>Failed to load solution filter: '{0}'</source>
         <target state="translated">Échec du chargement du filtre de solution : '{0}'</target>
diff --git a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.it.xlf b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.it.xlf
index 4996c28f4ab66..0a3319a83ac16 100644
--- a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.it.xlf
+++ b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.it.xlf
@@ -2,6 +2,11 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="it" original="../WorkspaceMSBuildResources.resx">
     <body>
+      <trans-unit id="Failed_to_load_solution_0">
+        <source>Failed to load solution: '{0}'</source>
+        <target state="new">Failed to load solution: '{0}'</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Failed_to_load_solution_filter_0">
         <source>Failed to load solution filter: '{0}'</source>
         <target state="translated">Non è stato possibile caricare il filtro soluzione: '{0}'</target>
diff --git a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.ja.xlf b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.ja.xlf
index 6813cc30aeb2e..b2e4c85fed7aa 100644
--- a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.ja.xlf
+++ b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.ja.xlf
@@ -2,6 +2,11 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="ja" original="../WorkspaceMSBuildResources.resx">
     <body>
+      <trans-unit id="Failed_to_load_solution_0">
+        <source>Failed to load solution: '{0}'</source>
+        <target state="new">Failed to load solution: '{0}'</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Failed_to_load_solution_filter_0">
         <source>Failed to load solution filter: '{0}'</source>
         <target state="translated">ソリューション フィルター '{0}' を読み込めませんでした</target>
diff --git a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.ko.xlf b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.ko.xlf
index 88c99e8b9aa5c..fcbdb046c77d1 100644
--- a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.ko.xlf
+++ b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.ko.xlf
@@ -2,6 +2,11 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="ko" original="../WorkspaceMSBuildResources.resx">
     <body>
+      <trans-unit id="Failed_to_load_solution_0">
+        <source>Failed to load solution: '{0}'</source>
+        <target state="new">Failed to load solution: '{0}'</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Failed_to_load_solution_filter_0">
         <source>Failed to load solution filter: '{0}'</source>
         <target state="translated">솔루션 필터 '{0}'을(를) 로드하지 못했습니다.</target>
diff --git a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.pl.xlf b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.pl.xlf
index a46d4b3f5b141..f17c499fd4fd1 100644
--- a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.pl.xlf
+++ b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.pl.xlf
@@ -2,6 +2,11 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="pl" original="../WorkspaceMSBuildResources.resx">
     <body>
+      <trans-unit id="Failed_to_load_solution_0">
+        <source>Failed to load solution: '{0}'</source>
+        <target state="new">Failed to load solution: '{0}'</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Failed_to_load_solution_filter_0">
         <source>Failed to load solution filter: '{0}'</source>
         <target state="translated">Nie można załadować filtru rozwiązań: „{0}”</target>
diff --git a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.pt-BR.xlf b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.pt-BR.xlf
index edf0d856b64b9..10270549602d0 100644
--- a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.pt-BR.xlf
+++ b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.pt-BR.xlf
@@ -2,6 +2,11 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="pt-BR" original="../WorkspaceMSBuildResources.resx">
     <body>
+      <trans-unit id="Failed_to_load_solution_0">
+        <source>Failed to load solution: '{0}'</source>
+        <target state="new">Failed to load solution: '{0}'</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Failed_to_load_solution_filter_0">
         <source>Failed to load solution filter: '{0}'</source>
         <target state="translated">Falha ao carregar o filtro da solução: '{0}'</target>
diff --git a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.ru.xlf b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.ru.xlf
index 1380789379b15..33966c61b3ab5 100644
--- a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.ru.xlf
+++ b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.ru.xlf
@@ -2,6 +2,11 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="ru" original="../WorkspaceMSBuildResources.resx">
     <body>
+      <trans-unit id="Failed_to_load_solution_0">
+        <source>Failed to load solution: '{0}'</source>
+        <target state="new">Failed to load solution: '{0}'</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Failed_to_load_solution_filter_0">
         <source>Failed to load solution filter: '{0}'</source>
         <target state="translated">Не удалось загрузить фильтр решений: "{0}"</target>
diff --git a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.tr.xlf b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.tr.xlf
index 3c9da2a38945e..ba5a8b0d12468 100644
--- a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.tr.xlf
+++ b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.tr.xlf
@@ -2,6 +2,11 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="tr" original="../WorkspaceMSBuildResources.resx">
     <body>
+      <trans-unit id="Failed_to_load_solution_0">
+        <source>Failed to load solution: '{0}'</source>
+        <target state="new">Failed to load solution: '{0}'</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Failed_to_load_solution_filter_0">
         <source>Failed to load solution filter: '{0}'</source>
         <target state="translated">Çözüm filtresi yüklenemedi: '{0}'</target>
diff --git a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.zh-Hans.xlf b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.zh-Hans.xlf
index e0f9b88692ee0..74803708ffe0d 100644
--- a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.zh-Hans.xlf
+++ b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.zh-Hans.xlf
@@ -2,6 +2,11 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="zh-Hans" original="../WorkspaceMSBuildResources.resx">
     <body>
+      <trans-unit id="Failed_to_load_solution_0">
+        <source>Failed to load solution: '{0}'</source>
+        <target state="new">Failed to load solution: '{0}'</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Failed_to_load_solution_filter_0">
         <source>Failed to load solution filter: '{0}'</source>
         <target state="translated">未能加载解决方案筛选器:“{0}”</target>
diff --git a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.zh-Hant.xlf b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.zh-Hant.xlf
index 36b7d71732fb7..4910b9b3ba020 100644
--- a/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.zh-Hant.xlf
+++ b/src/Workspaces/MSBuild/Core/xlf/WorkspaceMSBuildResources.zh-Hant.xlf
@@ -2,6 +2,11 @@
 <xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
   <file datatype="xml" source-language="en" target-language="zh-Hant" original="../WorkspaceMSBuildResources.resx">
     <body>
+      <trans-unit id="Failed_to_load_solution_0">
+        <source>Failed to load solution: '{0}'</source>
+        <target state="new">Failed to load solution: '{0}'</target>
+        <note />
+      </trans-unit>
       <trans-unit id="Failed_to_load_solution_filter_0">
         <source>Failed to load solution filter: '{0}'</source>
         <target state="translated">無法載入解決方案篩選: '{0}'</target>
diff --git a/src/Workspaces/MSBuild/Test/Resources/XmlSolutionFiles/CSharpXmlSolution.slnx b/src/Workspaces/MSBuild/Test/Resources/XmlSolutionFiles/CSharpXmlSolution.slnx
new file mode 100644
index 0000000000000..3ee85b410a01d
--- /dev/null
+++ b/src/Workspaces/MSBuild/Test/Resources/XmlSolutionFiles/CSharpXmlSolution.slnx
@@ -0,0 +1,3 @@
+<Solution>
+    <Project Path="CSharpProject/CSharpProject.csproj" />
+</Solution>
\ No newline at end of file
diff --git a/src/Workspaces/MSBuild/Test/Resources/XmlSolutionFiles/InvalidXmlSolution.slnx b/src/Workspaces/MSBuild/Test/Resources/XmlSolutionFiles/InvalidXmlSolution.slnx
new file mode 100644
index 0000000000000..481b67acdee62
--- /dev/null
+++ b/src/Workspaces/MSBuild/Test/Resources/XmlSolutionFiles/InvalidXmlSolution.slnx
@@ -0,0 +1 @@
+This is an invalid SLNX
\ No newline at end of file
diff --git a/src/Workspaces/MSBuild/Test/TestFiles/Resources.cs b/src/Workspaces/MSBuild/Test/TestFiles/Resources.cs
index 9dad5d24ad129..b725379b92e8f 100644
--- a/src/Workspaces/MSBuild/Test/TestFiles/Resources.cs
+++ b/src/Workspaces/MSBuild/Test/TestFiles/Resources.cs
@@ -72,6 +72,12 @@ public static class SolutionFilters
         public static string CSharp => GetText("SolutionFilters.CSharpSolutionFilter.slnf");
     }
 
+    public static class XmlSolutionFiles
+    {
+        public static string CSharp => GetText("XmlSolutionFiles.CSharpXmlSolution.slnx");
+        public static string Invalid => GetText("XmlSolutionFiles.InvalidXmlSolution.slnx");
+    }
+
     public static class SolutionFiles
     {
         public static string AnalyzerReference => GetText("SolutionFiles.AnalyzerReference.sln");
@@ -165,6 +171,11 @@ public static class CSharp
             public static string WithoutPrefer32Bit => GetText("ProjectFiles.CSharp.WithoutPrefer32Bit.csproj");
             public static string VBNetCoreAppWithGlobalImportAndLibrary_Library => GetText("VBNetCoreAppWithGlobalImportAndLibrary.Library.csproj");
         }
+        public static class XmlSolutions
+        {
+            public static string CSharp => GetText("XmlSolutions.CSharpXmlSolution.slnx");
+            public static string Invalid => GetText("XmlSolutions.InvalidXmlSolution.slnx");
+        }
 
         public static class FSharp
         {
diff --git a/src/Workspaces/MSBuild/Test/VisualStudioMSBuildWorkspaceTests.cs b/src/Workspaces/MSBuild/Test/VisualStudioMSBuildWorkspaceTests.cs
index 9128c40c08781..4ea3f6b48e83b 100644
--- a/src/Workspaces/MSBuild/Test/VisualStudioMSBuildWorkspaceTests.cs
+++ b/src/Workspaces/MSBuild/Test/VisualStudioMSBuildWorkspaceTests.cs
@@ -3150,7 +3150,7 @@ public async Task TestOpenSolution_ProjectReferencesWithUnconventionalOutputPath
 
         var solutionFilePath = GetSolutionFileName(@"TestVB2.sln");
 
-        // The reference assemblies for .NETFramework,Version=v3.5 were not found To resolve this, install the Developer Pack 
+        // The reference assemblies for .NETFramework,Version=v3.5 were not found To resolve this, install the Developer Pack
         using var workspace = CreateMSBuildWorkspace(throwOnWorkspaceFailed: false);
         var solution = await workspace.OpenSolutionAsync(solutionFilePath);
 
@@ -3276,6 +3276,34 @@ public async Task TestInvalidSolutionFilterDoesNotLoad()
         Assert.Equal(0, workspace.CurrentSolution.ProjectIds.Count);
     }
 
+    [ConditionalFact(typeof(VisualStudioMSBuildInstalled))]
+    public async Task TestValidXmlSolutionSupport()
+    {
+        CreateFiles(GetMultiProjectSolutionFiles()
+            .WithFile(@"CSharpXmlSolution.slnx", Resources.XmlSolutionFiles.CSharp));
+        var solutionFilePath = GetSolutionFileName(@"CSharpXmlSolution.slnx");
+
+        using var workspace = CreateMSBuildWorkspace();
+        var solution = await workspace.OpenSolutionAsync(solutionFilePath);
+        var csharpProject = Assert.Single(solution.Projects);
+
+        Assert.Equal(LanguageNames.CSharp, csharpProject.Language);
+        Assert.Equal("CSharpProject.csproj", Path.GetFileName(csharpProject.FilePath));
+    }
+
+    [ConditionalFact(typeof(VisualStudioMSBuildInstalled))]
+    public async Task TestInvalidXmlSolutionSupport()
+    {
+        CreateFiles(GetMultiProjectSolutionFiles()
+            .WithFile(@"InvalidXmlSolution.slnx", Resources.XmlSolutionFiles.Invalid));
+        var solutionFilePath = GetSolutionFileName(@"InvalidXmlSolution.slnx");
+
+        using var workspace = CreateMSBuildWorkspace();
+        var exception = await Assert.ThrowsAnyAsync<Exception>(() => workspace.OpenSolutionAsync(solutionFilePath));
+
+        Assert.Equal(0, workspace.CurrentSolution.ProjectIds.Count);
+    }
+
     // On .NET Core this tests fails with "CodePage Not Found"
     [ConditionalFact(typeof(VisualStudioMSBuildInstalled), typeof(DesktopClrOnly))]
     [WorkItem("http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/991528")]


Reviews:
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T22:14:40Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T22:16:06Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T22:17:40Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T22:21:39Z
user: login: RikkiGibson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T22:30:17Z
user: login: RikkiGibson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T22:35:34Z
user: login: RikkiGibson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T22:35:57Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T22:38:19Z
user: login: RikkiGibson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T22:39:37Z
user: login: RikkiGibson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T22:41:37Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T22:41:45Z
user: login: RikkiGibson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T22:42:36Z
user: login: RikkiGibson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T22:43:47Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T22:45:08Z
user: login: RikkiGibson
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T22:45:24Z
user: login: RikkiGibson
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-21T22:45:37Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T00:09:42Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T00:10:10Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T00:10:30Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T00:12:04Z
user: login: RikkiGibson
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-22T21:24:29Z
user: login: dibarbet
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T22:17:14Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T22:30:38Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T22:31:15Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T22:31:34Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T22:33:44Z
user: login: JoeRobich
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T22:34:21Z
user: login: dibarbet
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-22T23:22:30Z
user: login: jasonmalinowski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-28T23:25:09Z
user: login: jasonmalinowski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-28T23:25:28Z
user: login: jasonmalinowski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-28T23:37:24Z
user: login: jasonmalinowski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-28T23:38:47Z
user: login: jasonmalinowski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-28T23:45:00Z
user: login: jasonmalinowski
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-30T20:29:19Z

Review comments:
diff_hunk: @@ -57,19 +57,7 @@ public async Task OpenSolutionAsync(string solutionFilePath)
         _logger.LogInformation(string.Format(LanguageServerResources.Loading_0, solutionFilePath));
         ProjectFactory.SolutionPath = solutionFilePath;
 
-        // We'll load solutions out-of-proc, since it's possible we might be running on a runtime that doesn't have a matching SDK installed,
-        // and we don't want any MSBuild registration to set environment variables in our process that might impact child processes.
-        await using var buildHostProcessManager = new BuildHostProcessManager(globalMSBuildProperties: AdditionalProperties, loggerFactory: LoggerFactory);
-        var buildHost = await buildHostProcessManager.GetBuildHostAsync(BuildHostProcessKind.NetCore, CancellationToken.None);
-
-        // If we don't have a .NET Core SDK on this machine at all, try .NET Framework
-        if (!await buildHost.HasUsableMSBuildAsync(solutionFilePath, CancellationToken.None))
-        {
-            var kind = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? BuildHostProcessKind.NetFramework : BuildHostProcessKind.Mono;
-            buildHost = await buildHostProcessManager.GetBuildHostAsync(kind, CancellationToken.None);
-        }
-
-        var projects = await buildHost.GetProjectsInSolutionAsync(solutionFilePath, CancellationToken.None);
+        var (_, projects) = await SolutionFileReader.ReadSolutionFileAsync(solutionFilePath, CancellationToken.None);
path: src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs
user: login: JoeRobich
body: In the case that `solutionFilePath` points to a .slnf `ReadSolutionFileAsync` returns the filtered solution's file path. Should we be assigning that path to the `ProjectFactory.SolutionPath`?
created_at: 2025-05-21T22:14:39Z
author_association: MEMBER
diff_hunk: @@ -167,51 +167,20 @@ public async Task<SolutionInfo> LoadSolutionInfoAsync(
             throw new ArgumentNullException(nameof(solutionFilePath));
         }
 
-        if (!_pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath))
path: src/Workspaces/MSBuild/Core/MSBuild/MSBuildProjectLoader.cs
user: login: JoeRobich
body: Moved this bit into a new `SolutionFileReader` class.
created_at: 2025-05-21T22:16:06Z
author_association: MEMBER
diff_hunk: @@ -11,9 +11,9 @@ namespace Microsoft.CodeAnalysis.MSBuild;
 
 internal sealed class PathResolver
 {
-    private readonly DiagnosticReporter _diagnosticReporter;
+    private readonly DiagnosticReporter? _diagnosticReporter;
 
-    public PathResolver(DiagnosticReporter diagnosticReporter)
+    public PathResolver(DiagnosticReporter? diagnosticReporter)
path: src/Workspaces/MSBuild/Core/MSBuild/PathResolver.cs
user: login: JoeRobich
body: Made the `DiagnosticReporter` optional since this will also be indirectly used by the `LanguageServerProjectSystem` which does not use a reporter.
created_at: 2025-05-21T22:17:39Z
author_association: MEMBER
diff_hunk: @@ -22,10 +22,11 @@
     </PackageDescription>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.Build" />
-    <PackageReference Include="Microsoft.Build.Tasks.Core" />
+    <PackageReference Include="Microsoft.Build.Framework" />
     <PackageReference Include="Microsoft.Extensions.Logging" />
+    <PackageReference Include="Microsoft.VisualStudio.SolutionPersistence" />
     <PackageReference Include="Newtonsoft.Json" />
+    <PackageReference Include="System.Text.Json" Condition="'$(TargetFramework)' == 'net472'" />
path: src/Workspaces/MSBuild/Core/Microsoft.CodeAnalysis.Workspaces.MSBuild.csproj
user: login: JoeRobich
body: Originally brought in by `Microsoft.Build` and is already available as part of .NET.
created_at: 2025-05-21T22:21:39Z
author_association: MEMBER
diff_hunk: @@ -57,19 +57,7 @@ public async Task OpenSolutionAsync(string solutionFilePath)
         _logger.LogInformation(string.Format(LanguageServerResources.Loading_0, solutionFilePath));
         ProjectFactory.SolutionPath = solutionFilePath;
 
-        // We'll load solutions out-of-proc, since it's possible we might be running on a runtime that doesn't have a matching SDK installed,
-        // and we don't want any MSBuild registration to set environment variables in our process that might impact child processes.
-        await using var buildHostProcessManager = new BuildHostProcessManager(globalMSBuildProperties: AdditionalProperties, loggerFactory: LoggerFactory);
-        var buildHost = await buildHostProcessManager.GetBuildHostAsync(BuildHostProcessKind.NetCore, CancellationToken.None);
-
-        // If we don't have a .NET Core SDK on this machine at all, try .NET Framework
-        if (!await buildHost.HasUsableMSBuildAsync(solutionFilePath, CancellationToken.None))
-        {
-            var kind = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? BuildHostProcessKind.NetFramework : BuildHostProcessKind.Mono;
-            buildHost = await buildHostProcessManager.GetBuildHostAsync(kind, CancellationToken.None);
-        }
-
-        var projects = await buildHost.GetProjectsInSolutionAsync(solutionFilePath, CancellationToken.None);
+        var (_, projects) = await SolutionFileReader.ReadSolutionFileAsync(solutionFilePath, CancellationToken.None);
path: src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs
user: login: RikkiGibson
body: Are you basically asking whether to use the path to the `slnf` or the path to the associated `sln`? I think we would want to use the `slnf`, which this PR appears to be doing right now.
created_at: 2025-05-21T22:30:16Z
author_association: MEMBER
diff_hunk: @@ -11,7 +11,7 @@
 
 namespace Microsoft.CodeAnalysis.MSBuild;
 
-public partial class MSBuildProjectLoader
+internal partial class SolutionFileReader
path: src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.SolutionFilterReader.cs
user: login: RikkiGibson
body: nit: line 40 mentions that TryGetAbsoluteSolutionPath "should throw", but I don't see why it would ever do that.
created_at: 2025-05-21T22:35:33Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,79 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal partial class SolutionFileReader
+{
+    public static Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, CancellationToken cancellationToken)
+    {
+        return ReadSolutionFileAsync(solutionFilePath, new PathResolver(diagnosticReporter: null), cancellationToken);
+    }
+
+    public static async Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, CancellationToken cancellationToken)
+    {
+        if (!pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath))
+        {
+            // TryGetAbsoluteSolutionPath should throw before we get here.
path: src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.cs
user: login: RikkiGibson
body: Why would it throw?
created_at: 2025-05-21T22:35:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,79 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal partial class SolutionFileReader
+{
+    public static Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, CancellationToken cancellationToken)
+    {
+        return ReadSolutionFileAsync(solutionFilePath, new PathResolver(diagnosticReporter: null), cancellationToken);
+    }
+
+    public static async Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, CancellationToken cancellationToken)
+    {
+        if (!pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath))
+        {
+            // TryGetAbsoluteSolutionPath should throw before we get here.
path: src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.cs
user: login: JoeRobich
body: Yeah, this is a funky thing. When there are Path issues the `PathResolver` will report a diagnostic via the DiagnosticReporter it is instantiated with. By passing `DiagnosticReportingMode.Throw` when calling `TryGetAbsoluteSolutionPath` the reporter will throw on our behalf.
created_at: 2025-05-21T22:38:19Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,79 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal partial class SolutionFileReader
+{
+    public static Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, CancellationToken cancellationToken)
+    {
+        return ReadSolutionFileAsync(solutionFilePath, new PathResolver(diagnosticReporter: null), cancellationToken);
+    }
+
+    public static async Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, CancellationToken cancellationToken)
+    {
+        if (!pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath))
+        {
+            // TryGetAbsoluteSolutionPath should throw before we get here.
+            return (solutionFilePath, []);
+        }
+
+        // When passed a solution filter, we need to read the filter file to get the solution path and included project paths.
+        var projectFilter = ImmutableHashSet<string>.Empty;
+        if (SolutionFilterReader.IsSolutionFilterFilename(absoluteSolutionPath) &&
+            !SolutionFilterReader.TryRead(absoluteSolutionPath, pathResolver, out absoluteSolutionPath, out projectFilter))
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_filter_0, solutionFilePath));
+        }
+
+        var projects = await TryReadSolutionFileAsync(absoluteSolutionPath, pathResolver, projectFilter, cancellationToken).ConfigureAwait(false);
+        if (!projects.HasValue)
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_0, absoluteSolutionPath));
+        }
+
+        return (absoluteSolutionPath, projects.Value);
+    }
+
+    private static async Task<ImmutableArray<(string ProjectPath, string ProjectGuid)>?> TryReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, ImmutableHashSet<string> projectFilter, CancellationToken cancellationToken)
+    {
+        var serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
+        if (serializer == null)
+        {
+            return null;
+        }
+
+        // The solution folder is the base directory for project paths.
+        var baseDirectory = Path.GetDirectoryName(solutionFilePath);
+        RoslynDebug.AssertNotNull(baseDirectory);
+
+        var solutionModel = await serializer.OpenAsync(solutionFilePath, cancellationToken).ConfigureAwait(false);
+
+        var builder = ImmutableArray.CreateBuilder<(string ProjectPath, string ProjectGuid)>();
+        foreach (var projectModel in solutionModel.SolutionProjects)
+        {
+            // If we are filtering based on a solution filter, then we need to verify the project is included.
+            if (!projectFilter.IsEmpty)
+            {
+                if (!pathResolver.TryGetAbsoluteProjectPath(projectModel.FilePath, baseDirectory, DiagnosticReportingMode.Throw, out var absoluteProjectPath)
path: src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.cs
user: login: RikkiGibson
body: Does this mean we will throw if we fail to get a project path here? In that case, can we just do a contract assertion that this call will succeed?
created_at: 2025-05-21T22:39:36Z
author_association: MEMBER
diff_hunk: @@ -23,9 +23,6 @@ public RemoteBuildHost(RpcClient client)
     public Task<bool> HasUsableMSBuildAsync(string projectOrSolutionFilePath, CancellationToken cancellationToken)
         => _client.InvokeAsync<bool>(BuildHostTargetObject, nameof(IBuildHost.HasUsableMSBuild), parameters: [projectOrSolutionFilePath], cancellationToken);
 
-    public Task<ImmutableArray<(string ProjectPath, string ProjectGuid)>> GetProjectsInSolutionAsync(string solutionFilePath, CancellationToken cancellationToken)
-        => _client.InvokeAsync<ImmutableArray<(string ProjectPath, string ProjectGuid)>>(BuildHostTargetObject, nameof(IBuildHost.GetProjectsInSolution), parameters: [solutionFilePath], cancellationToken);
path: src/Workspaces/MSBuild/Core/Rpc/RemoteBuildHost.cs
user: login: RikkiGibson
body: It looks like this PR means that we will be getting the projects in a solution in-proc again, instead of thru an RPC call? Is that right?
created_at: 2025-05-21T22:41:36Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,79 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal partial class SolutionFileReader
+{
+    public static Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, CancellationToken cancellationToken)
+    {
+        return ReadSolutionFileAsync(solutionFilePath, new PathResolver(diagnosticReporter: null), cancellationToken);
+    }
+
+    public static async Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, CancellationToken cancellationToken)
+    {
+        if (!pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath))
+        {
+            // TryGetAbsoluteSolutionPath should throw before we get here.
path: src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.cs
user: login: JoeRobich
body: It may make sense to throw regardless. The LanguageServerProjectSystem does not use a DiagnosticReporter and would simply report opening an empty solution.
created_at: 2025-05-21T22:41:45Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,3 @@
+<Solution>
+    <Project Path="CSharpProject/CSharpProject.csproj" />
+</Solution>
path: src/Workspaces/MSBuild/Test/Resources/XmlSolutionFiles/CSharpXmlSolution.slnx
user: login: RikkiGibson
body: really looking forward to being able to use these.
created_at: 2025-05-21T22:42:35Z
author_association: MEMBER
diff_hunk: @@ -3276,6 +3276,33 @@ public async Task TestInvalidSolutionFilterDoesNotLoad()
         Assert.Equal(0, workspace.CurrentSolution.ProjectIds.Count);
     }
 
+    [ConditionalFact(typeof(VisualStudioMSBuildInstalled))]
+    public async Task TestValidXmlSolutionSupport()
+    {
+        CreateFiles(GetMultiProjectSolutionFiles()
+            .WithFile(@"CSharpXmlSolution.slnx", Resources.XmlSolutionFiles.CSharp));
+        var solutionFilePath = GetSolutionFileName(@"CSharpXmlSolution.slnx");
+
+        using var workspace = CreateMSBuildWorkspace();
+        var solution = await workspace.OpenSolutionAsync(solutionFilePath);
+        var csharpProject = solution.Projects.Single();
+
+        Assert.Equal(LanguageNames.CSharp, csharpProject.Language);
path: src/Workspaces/MSBuild/Test/VisualStudioMSBuildWorkspaceTests.cs
user: login: RikkiGibson
body: nit: might be nice to assert on its name/path also. Up to you.
created_at: 2025-05-21T22:43:45Z
author_association: MEMBER
diff_hunk: @@ -23,9 +23,6 @@ public RemoteBuildHost(RpcClient client)
     public Task<bool> HasUsableMSBuildAsync(string projectOrSolutionFilePath, CancellationToken cancellationToken)
         => _client.InvokeAsync<bool>(BuildHostTargetObject, nameof(IBuildHost.HasUsableMSBuild), parameters: [projectOrSolutionFilePath], cancellationToken);
 
-    public Task<ImmutableArray<(string ProjectPath, string ProjectGuid)>> GetProjectsInSolutionAsync(string solutionFilePath, CancellationToken cancellationToken)
-        => _client.InvokeAsync<ImmutableArray<(string ProjectPath, string ProjectGuid)>>(BuildHostTargetObject, nameof(IBuildHost.GetProjectsInSolution), parameters: [solutionFilePath], cancellationToken);
path: src/Workspaces/MSBuild/Core/Rpc/RemoteBuildHost.cs
user: login: JoeRobich
body: Yes, I should update the description. I ended up needing to pull in the SLNX work after realizing that we needed the Microsoft.Build dependency for solution parsing
created_at: 2025-05-21T22:45:07Z
author_association: MEMBER
diff_hunk: @@ -11,7 +11,7 @@
 
 namespace Microsoft.CodeAnalysis.MSBuild;
 
-public partial class MSBuildProjectLoader
+internal partial class SolutionFileReader
path: src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.SolutionFilterReader.cs
user: login: RikkiGibson
body: I see, it's all in the DiagnosticReportingMode.Throw. It feels like when we call the method in this way, it would be reasonable to ignore the result or assert on it.
created_at: 2025-05-21T22:45:23Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,79 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal partial class SolutionFileReader
+{
+    public static Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, CancellationToken cancellationToken)
+    {
+        return ReadSolutionFileAsync(solutionFilePath, new PathResolver(diagnosticReporter: null), cancellationToken);
+    }
+
+    public static async Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, CancellationToken cancellationToken)
+    {
+        if (!pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath))
+        {
+            // TryGetAbsoluteSolutionPath should throw before we get here.
+            return (solutionFilePath, []);
+        }
+
+        // When passed a solution filter, we need to read the filter file to get the solution path and included project paths.
+        var projectFilter = ImmutableHashSet<string>.Empty;
+        if (SolutionFilterReader.IsSolutionFilterFilename(absoluteSolutionPath) &&
+            !SolutionFilterReader.TryRead(absoluteSolutionPath, pathResolver, out absoluteSolutionPath, out projectFilter))
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_filter_0, solutionFilePath));
+        }
+
+        var projects = await TryReadSolutionFileAsync(absoluteSolutionPath, pathResolver, projectFilter, cancellationToken).ConfigureAwait(false);
+        if (!projects.HasValue)
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_0, absoluteSolutionPath));
+        }
+
+        return (absoluteSolutionPath, projects.Value);
+    }
+
+    private static async Task<ImmutableArray<(string ProjectPath, string ProjectGuid)>?> TryReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, ImmutableHashSet<string> projectFilter, CancellationToken cancellationToken)
+    {
+        var serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
+        if (serializer == null)
+        {
+            return null;
+        }
+
+        // The solution folder is the base directory for project paths.
+        var baseDirectory = Path.GetDirectoryName(solutionFilePath);
+        RoslynDebug.AssertNotNull(baseDirectory);
+
+        var solutionModel = await serializer.OpenAsync(solutionFilePath, cancellationToken).ConfigureAwait(false);
+
+        var builder = ImmutableArray.CreateBuilder<(string ProjectPath, string ProjectGuid)>();
+        foreach (var projectModel in solutionModel.SolutionProjects)
+        {
+            // If we are filtering based on a solution filter, then we need to verify the project is included.
+            if (!projectFilter.IsEmpty)
+            {
+                if (!pathResolver.TryGetAbsoluteProjectPath(projectModel.FilePath, baseDirectory, DiagnosticReportingMode.Throw, out var absoluteProjectPath)
path: src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.cs
user: login: JoeRobich
body: Updated all use of `pathResolver.TryGetAbsol...` to use `Contract.ThrowIfFalse`
created_at: 2025-05-22T00:09:42Z
author_association: MEMBER
diff_hunk: @@ -3276,6 +3276,33 @@ public async Task TestInvalidSolutionFilterDoesNotLoad()
         Assert.Equal(0, workspace.CurrentSolution.ProjectIds.Count);
     }
 
+    [ConditionalFact(typeof(VisualStudioMSBuildInstalled))]
+    public async Task TestValidXmlSolutionSupport()
+    {
+        CreateFiles(GetMultiProjectSolutionFiles()
+            .WithFile(@"CSharpXmlSolution.slnx", Resources.XmlSolutionFiles.CSharp));
+        var solutionFilePath = GetSolutionFileName(@"CSharpXmlSolution.slnx");
+
+        using var workspace = CreateMSBuildWorkspace();
+        var solution = await workspace.OpenSolutionAsync(solutionFilePath);
+        var csharpProject = solution.Projects.Single();
+
+        Assert.Equal(LanguageNames.CSharp, csharpProject.Language);
path: src/Workspaces/MSBuild/Test/VisualStudioMSBuildWorkspaceTests.cs
user: login: JoeRobich
body: Added a few more assertions.
created_at: 2025-05-22T00:10:10Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,79 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal partial class SolutionFileReader
+{
+    public static Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, CancellationToken cancellationToken)
+    {
+        return ReadSolutionFileAsync(solutionFilePath, new PathResolver(diagnosticReporter: null), cancellationToken);
+    }
+
+    public static async Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, CancellationToken cancellationToken)
+    {
+        if (!pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath))
+        {
+            // TryGetAbsoluteSolutionPath should throw before we get here.
path: src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.cs
user: login: JoeRobich
body: Updated to throw on false.
created_at: 2025-05-22T00:10:30Z
author_association: MEMBER
diff_hunk: @@ -57,19 +57,7 @@ public async Task OpenSolutionAsync(string solutionFilePath)
         _logger.LogInformation(string.Format(LanguageServerResources.Loading_0, solutionFilePath));
         ProjectFactory.SolutionPath = solutionFilePath;
 
-        // We'll load solutions out-of-proc, since it's possible we might be running on a runtime that doesn't have a matching SDK installed,
-        // and we don't want any MSBuild registration to set environment variables in our process that might impact child processes.
-        await using var buildHostProcessManager = new BuildHostProcessManager(globalMSBuildProperties: AdditionalProperties, loggerFactory: LoggerFactory);
-        var buildHost = await buildHostProcessManager.GetBuildHostAsync(BuildHostProcessKind.NetCore, CancellationToken.None);
-
-        // If we don't have a .NET Core SDK on this machine at all, try .NET Framework
-        if (!await buildHost.HasUsableMSBuildAsync(solutionFilePath, CancellationToken.None))
-        {
-            var kind = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? BuildHostProcessKind.NetFramework : BuildHostProcessKind.Mono;
-            buildHost = await buildHostProcessManager.GetBuildHostAsync(kind, CancellationToken.None);
-        }
-
-        var projects = await buildHost.GetProjectsInSolutionAsync(solutionFilePath, CancellationToken.None);
+        var (_, projects) = await SolutionFileReader.ReadSolutionFileAsync(solutionFilePath, CancellationToken.None);
path: src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs
user: login: JoeRobich
body: True, it is working today without this change and is currently using the .slnf path.
created_at: 2025-05-22T00:12:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal partial class SolutionFileReader
+{
+    public static Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, CancellationToken cancellationToken)
+    {
+        return ReadSolutionFileAsync(solutionFilePath, new PathResolver(diagnosticReporter: null), cancellationToken);
+    }
+
+    public static async Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, CancellationToken cancellationToken)
+    {
+        Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath));
+
+        // When passed a solution filter, we need to read the filter file to get the solution path and included project paths.
+        var projectFilter = ImmutableHashSet<string>.Empty;
+        if (SolutionFilterReader.IsSolutionFilterFilename(absoluteSolutionPath) &&
+            !SolutionFilterReader.TryRead(absoluteSolutionPath, pathResolver, out absoluteSolutionPath, out projectFilter))
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_filter_0, solutionFilePath));
+        }
+
+        var projects = await TryReadSolutionFileAsync(absoluteSolutionPath, pathResolver, projectFilter, cancellationToken).ConfigureAwait(false);
+        if (!projects.HasValue)
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_0, absoluteSolutionPath));
+        }
+
+        return (absoluteSolutionPath, projects.Value);
+    }
+
+    private static async Task<ImmutableArray<(string ProjectPath, string ProjectGuid)>?> TryReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, ImmutableHashSet<string> projectFilter, CancellationToken cancellationToken)
+    {
+        var serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
+        if (serializer == null)
+        {
+            return null;
path: src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.cs
user: login: dibarbet
body: what does the serializer being null here mean?  Are there different serializers for sln vs slnx?
created_at: 2025-05-22T22:14:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal partial class SolutionFileReader
+{
+    public static Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, CancellationToken cancellationToken)
+    {
+        return ReadSolutionFileAsync(solutionFilePath, new PathResolver(diagnosticReporter: null), cancellationToken);
+    }
+
+    public static async Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, CancellationToken cancellationToken)
+    {
+        Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath));
+
+        // When passed a solution filter, we need to read the filter file to get the solution path and included project paths.
+        var projectFilter = ImmutableHashSet<string>.Empty;
+        if (SolutionFilterReader.IsSolutionFilterFilename(absoluteSolutionPath) &&
+            !SolutionFilterReader.TryRead(absoluteSolutionPath, pathResolver, out absoluteSolutionPath, out projectFilter))
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_filter_0, solutionFilePath));
+        }
+
+        var projects = await TryReadSolutionFileAsync(absoluteSolutionPath, pathResolver, projectFilter, cancellationToken).ConfigureAwait(false);
+        if (!projects.HasValue)
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_0, absoluteSolutionPath));
+        }
+
+        return (absoluteSolutionPath, projects.Value);
+    }
+
+    private static async Task<ImmutableArray<(string ProjectPath, string ProjectGuid)>?> TryReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, ImmutableHashSet<string> projectFilter, CancellationToken cancellationToken)
+    {
+        var serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
+        if (serializer == null)
+        {
+            return null;
+        }
+
+        // The solution folder is the base directory for project paths.
+        var baseDirectory = Path.GetDirectoryName(solutionFilePath);
+        RoslynDebug.AssertNotNull(baseDirectory);
+
+        var solutionModel = await serializer.OpenAsync(solutionFilePath, cancellationToken).ConfigureAwait(false);
+
+        var builder = ImmutableArray.CreateBuilder<(string ProjectPath, string ProjectGuid)>();
+        foreach (var projectModel in solutionModel.SolutionProjects)
+        {
+            // If we are filtering based on a solution filter, then we need to verify the project is included.
+            if (!projectFilter.IsEmpty)
+            {
+                Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteProjectPath(projectModel.FilePath, baseDirectory, DiagnosticReportingMode.Throw, out var absoluteProjectPath));
+                if (!projectFilter.Contains(absoluteProjectPath))
path: src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.cs
user: login: dibarbet
body: interesting, does the solution parsing library not give us a way to get the subset of projects out of a solution filter?  We have to do that ourselves?
created_at: 2025-05-22T22:16:24Z
author_association: MEMBER
diff_hunk: @@ -22,10 +22,11 @@
     </PackageDescription>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.Build" />
-    <PackageReference Include="Microsoft.Build.Tasks.Core" />
+    <PackageReference Include="Microsoft.Build.Framework" />
     <PackageReference Include="Microsoft.Extensions.Logging" />
+    <PackageReference Include="Microsoft.VisualStudio.SolutionPersistence" />
     <PackageReference Include="Newtonsoft.Json" />
+    <PackageReference Include="System.Text.Json" Condition="'$(TargetFramework)' == 'net472'" />
path: src/Workspaces/MSBuild/Core/Microsoft.CodeAnalysis.Workspaces.MSBuild.csproj
user: login: dibarbet
body: why do we only need this for net472?
created_at: 2025-05-22T22:16:58Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal partial class SolutionFileReader
+{
+    public static Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, CancellationToken cancellationToken)
+    {
+        return ReadSolutionFileAsync(solutionFilePath, new PathResolver(diagnosticReporter: null), cancellationToken);
+    }
+
+    public static async Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, CancellationToken cancellationToken)
+    {
+        Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath));
+
+        // When passed a solution filter, we need to read the filter file to get the solution path and included project paths.
+        var projectFilter = ImmutableHashSet<string>.Empty;
+        if (SolutionFilterReader.IsSolutionFilterFilename(absoluteSolutionPath) &&
+            !SolutionFilterReader.TryRead(absoluteSolutionPath, pathResolver, out absoluteSolutionPath, out projectFilter))
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_filter_0, solutionFilePath));
+        }
+
+        var projects = await TryReadSolutionFileAsync(absoluteSolutionPath, pathResolver, projectFilter, cancellationToken).ConfigureAwait(false);
+        if (!projects.HasValue)
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_0, absoluteSolutionPath));
+        }
+
+        return (absoluteSolutionPath, projects.Value);
+    }
+
+    private static async Task<ImmutableArray<(string ProjectPath, string ProjectGuid)>?> TryReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, ImmutableHashSet<string> projectFilter, CancellationToken cancellationToken)
+    {
+        var serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
+        if (serializer == null)
+        {
+            return null;
+        }
+
+        // The solution folder is the base directory for project paths.
+        var baseDirectory = Path.GetDirectoryName(solutionFilePath);
+        RoslynDebug.AssertNotNull(baseDirectory);
+
+        var solutionModel = await serializer.OpenAsync(solutionFilePath, cancellationToken).ConfigureAwait(false);
+
+        var builder = ImmutableArray.CreateBuilder<(string ProjectPath, string ProjectGuid)>();
+        foreach (var projectModel in solutionModel.SolutionProjects)
+        {
+            // If we are filtering based on a solution filter, then we need to verify the project is included.
+            if (!projectFilter.IsEmpty)
+            {
+                Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteProjectPath(projectModel.FilePath, baseDirectory, DiagnosticReportingMode.Throw, out var absoluteProjectPath));
+                if (!projectFilter.Contains(absoluteProjectPath))
path: src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.cs
user: login: JoeRobich
body: Not presently. However, there is an open feature request https://github.com/microsoft/vs-solutionpersistence/issues/85
created_at: 2025-05-22T22:30:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal partial class SolutionFileReader
+{
+    public static Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, CancellationToken cancellationToken)
+    {
+        return ReadSolutionFileAsync(solutionFilePath, new PathResolver(diagnosticReporter: null), cancellationToken);
+    }
+
+    public static async Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, CancellationToken cancellationToken)
+    {
+        Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath));
+
+        // When passed a solution filter, we need to read the filter file to get the solution path and included project paths.
+        var projectFilter = ImmutableHashSet<string>.Empty;
+        if (SolutionFilterReader.IsSolutionFilterFilename(absoluteSolutionPath) &&
+            !SolutionFilterReader.TryRead(absoluteSolutionPath, pathResolver, out absoluteSolutionPath, out projectFilter))
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_filter_0, solutionFilePath));
+        }
+
+        var projects = await TryReadSolutionFileAsync(absoluteSolutionPath, pathResolver, projectFilter, cancellationToken).ConfigureAwait(false);
+        if (!projects.HasValue)
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_0, absoluteSolutionPath));
+        }
+
+        return (absoluteSolutionPath, projects.Value);
+    }
+
+    private static async Task<ImmutableArray<(string ProjectPath, string ProjectGuid)>?> TryReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, ImmutableHashSet<string> projectFilter, CancellationToken cancellationToken)
+    {
+        var serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
+        if (serializer == null)
+        {
+            return null;
path: src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.cs
user: login: JoeRobich
body: Yes! And I imagine when solution filter support is add it will be a separate serializer.
created_at: 2025-05-22T22:31:15Z
author_association: MEMBER
diff_hunk: @@ -22,10 +22,11 @@
     </PackageDescription>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.Build" />
-    <PackageReference Include="Microsoft.Build.Tasks.Core" />
+    <PackageReference Include="Microsoft.Build.Framework" />
     <PackageReference Include="Microsoft.Extensions.Logging" />
+    <PackageReference Include="Microsoft.VisualStudio.SolutionPersistence" />
     <PackageReference Include="Newtonsoft.Json" />
+    <PackageReference Include="System.Text.Json" Condition="'$(TargetFramework)' == 'net472'" />
path: src/Workspaces/MSBuild/Core/Microsoft.CodeAnalysis.Workspaces.MSBuild.csproj
user: login: JoeRobich
body: System.Text.Json is part of the .NET BCL.
created_at: 2025-05-22T22:31:34Z
author_association: MEMBER
diff_hunk: @@ -22,10 +22,11 @@
     </PackageDescription>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.Build" />
-    <PackageReference Include="Microsoft.Build.Tasks.Core" />
+    <PackageReference Include="Microsoft.Build.Framework" />
     <PackageReference Include="Microsoft.Extensions.Logging" />
+    <PackageReference Include="Microsoft.VisualStudio.SolutionPersistence" />
     <PackageReference Include="Newtonsoft.Json" />
+    <PackageReference Include="System.Text.Json" Condition="'$(TargetFramework)' == 'net472'" />
path: src/Workspaces/MSBuild/Core/Microsoft.CodeAnalysis.Workspaces.MSBuild.csproj
user: login: JoeRobich
body: Having it as a PackageReference for .NET TFM will pin the version to whatever we use in the build instead of allowing it to use the version included with newer runtimes.
created_at: 2025-05-22T22:33:44Z
author_association: MEMBER
diff_hunk: @@ -22,10 +22,11 @@
     </PackageDescription>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.Build" />
-    <PackageReference Include="Microsoft.Build.Tasks.Core" />
+    <PackageReference Include="Microsoft.Build.Framework" />
     <PackageReference Include="Microsoft.Extensions.Logging" />
+    <PackageReference Include="Microsoft.VisualStudio.SolutionPersistence" />
     <PackageReference Include="Newtonsoft.Json" />
+    <PackageReference Include="System.Text.Json" Condition="'$(TargetFramework)' == 'net472'" />
path: src/Workspaces/MSBuild/Core/Microsoft.CodeAnalysis.Workspaces.MSBuild.csproj
user: login: JoeRobich
body: I will add all that as a comment.
created_at: 2025-05-22T22:34:21Z
author_association: MEMBER
diff_hunk: @@ -57,19 +57,7 @@ public async Task OpenSolutionAsync(string solutionFilePath)
         _logger.LogInformation(string.Format(LanguageServerResources.Loading_0, solutionFilePath));
         ProjectFactory.SolutionPath = solutionFilePath;
 
-        // We'll load solutions out-of-proc, since it's possible we might be running on a runtime that doesn't have a matching SDK installed,
-        // and we don't want any MSBuild registration to set environment variables in our process that might impact child processes.
-        await using var buildHostProcessManager = new BuildHostProcessManager(globalMSBuildProperties: AdditionalProperties, loggerFactory: LoggerFactory);
-        var buildHost = await buildHostProcessManager.GetBuildHostAsync(BuildHostProcessKind.NetCore, CancellationToken.None);
-
-        // If we don't have a .NET Core SDK on this machine at all, try .NET Framework
-        if (!await buildHost.HasUsableMSBuildAsync(solutionFilePath, CancellationToken.None))
-        {
-            var kind = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? BuildHostProcessKind.NetFramework : BuildHostProcessKind.Mono;
-            buildHost = await buildHostProcessManager.GetBuildHostAsync(kind, CancellationToken.None);
-        }
-
-        var projects = await buildHost.GetProjectsInSolutionAsync(solutionFilePath, CancellationToken.None);
+        var (_, projects) = await SolutionFileReader.ReadSolutionFileAsync(solutionFilePath, CancellationToken.None);
path: src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs
user: login: jasonmalinowski
body: I'd say that's probably reasonable anyways; the only real thing the solution path is used for is caches, and I could imagine what we have to cache is different for different filters over the same solution.
created_at: 2025-05-28T23:25:09Z
author_association: MEMBER
diff_hunk: @@ -57,19 +57,7 @@ public async Task OpenSolutionAsync(string solutionFilePath)
         _logger.LogInformation(string.Format(LanguageServerResources.Loading_0, solutionFilePath));
         ProjectFactory.SolutionPath = solutionFilePath;
 
-        // We'll load solutions out-of-proc, since it's possible we might be running on a runtime that doesn't have a matching SDK installed,
-        // and we don't want any MSBuild registration to set environment variables in our process that might impact child processes.
-        await using var buildHostProcessManager = new BuildHostProcessManager(globalMSBuildProperties: AdditionalProperties, loggerFactory: LoggerFactory);
-        var buildHost = await buildHostProcessManager.GetBuildHostAsync(BuildHostProcessKind.NetCore, CancellationToken.None);
-
-        // If we don't have a .NET Core SDK on this machine at all, try .NET Framework
-        if (!await buildHost.HasUsableMSBuildAsync(solutionFilePath, CancellationToken.None))
-        {
-            var kind = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? BuildHostProcessKind.NetFramework : BuildHostProcessKind.Mono;
-            buildHost = await buildHostProcessManager.GetBuildHostAsync(kind, CancellationToken.None);
-        }
-
-        var projects = await buildHost.GetProjectsInSolutionAsync(solutionFilePath, CancellationToken.None);
+        var (_, projects) = await SolutionFileReader.ReadSolutionFileAsync(solutionFilePath, CancellationToken.None);
path: src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/HostWorkspace/LanguageServerProjectSystem.cs
user: login: jasonmalinowski
body: (if that's bad, maybe the fix is we should be passing around the workspace path or something _else_ for that...)
created_at: 2025-05-28T23:25:28Z
author_association: MEMBER
diff_hunk: @@ -11,9 +11,9 @@ namespace Microsoft.CodeAnalysis.MSBuild;
 
 internal sealed class PathResolver
 {
-    private readonly DiagnosticReporter _diagnosticReporter;
+    private readonly DiagnosticReporter? _diagnosticReporter;
 
-    public PathResolver(DiagnosticReporter diagnosticReporter)
+    public PathResolver(DiagnosticReporter? diagnosticReporter)
path: src/Workspaces/MSBuild/Core/MSBuild/PathResolver.cs
user: login: jasonmalinowski
body: Should we be though?
created_at: 2025-05-28T23:37:24Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal partial class SolutionFileReader
+{
+    public static Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, CancellationToken cancellationToken)
+    {
+        return ReadSolutionFileAsync(solutionFilePath, new PathResolver(diagnosticReporter: null), cancellationToken);
+    }
+
+    public static async Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, CancellationToken cancellationToken)
+    {
+        Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath));
path: src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.cs
user: login: jasonmalinowski
body: I'd expect this Directory.GetCurrentDirectory() code is only used in the classic MSBuildWorkspace path; we shouldn't have relative paths bouncing around in the language server at all, and I'd actually be a bit worried what it might resolve to at all...
created_at: 2025-05-28T23:38:47Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal partial class SolutionFileReader
+{
+    public static Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, CancellationToken cancellationToken)
+    {
+        return ReadSolutionFileAsync(solutionFilePath, new PathResolver(diagnosticReporter: null), cancellationToken);
+    }
+
+    public static async Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, CancellationToken cancellationToken)
+    {
+        Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath));
+
+        // When passed a solution filter, we need to read the filter file to get the solution path and included project paths.
+        var projectFilter = ImmutableHashSet<string>.Empty;
+        if (SolutionFilterReader.IsSolutionFilterFilename(absoluteSolutionPath) &&
+            !SolutionFilterReader.TryRead(absoluteSolutionPath, pathResolver, out absoluteSolutionPath, out projectFilter))
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_filter_0, solutionFilePath));
+        }
+
+        var projects = await TryReadSolutionFileAsync(absoluteSolutionPath, pathResolver, projectFilter, cancellationToken).ConfigureAwait(false);
+        if (!projects.HasValue)
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_0, absoluteSolutionPath));
path: src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.cs
user: login: jasonmalinowski
body: Is the only case this can happen is the format of the solution file isn't recognized? Should we give a better message in this case?
created_at: 2025-05-28T23:41:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal partial class SolutionFileReader
+{
+    public static Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, CancellationToken cancellationToken)
+    {
+        return ReadSolutionFileAsync(solutionFilePath, new PathResolver(diagnosticReporter: null), cancellationToken);
+    }
+
+    public static async Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, CancellationToken cancellationToken)
+    {
+        Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath));
+
+        // When passed a solution filter, we need to read the filter file to get the solution path and included project paths.
+        var projectFilter = ImmutableHashSet<string>.Empty;
+        if (SolutionFilterReader.IsSolutionFilterFilename(absoluteSolutionPath) &&
+            !SolutionFilterReader.TryRead(absoluteSolutionPath, pathResolver, out absoluteSolutionPath, out projectFilter))
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_filter_0, solutionFilePath));
+        }
+
+        var projects = await TryReadSolutionFileAsync(absoluteSolutionPath, pathResolver, projectFilter, cancellationToken).ConfigureAwait(false);
+        if (!projects.HasValue)
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_0, absoluteSolutionPath));
+        }
+
+        return (absoluteSolutionPath, projects.Value);
+    }
+
+    private static async Task<ImmutableArray<(string ProjectPath, string ProjectGuid)>?> TryReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, ImmutableHashSet<string> projectFilter, CancellationToken cancellationToken)
+    {
+        var serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
+        if (serializer == null)
+        {
+            return null;
path: src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.cs
user: login: jasonmalinowski
body: Should we have this just throw? Otherwise we're having to deal with the null return at the caller which isn't really helping much since it makes me think the rest of the method isn't supposed to throw (but absolutely would if something went wrong?)
created_at: 2025-05-28T23:41:37Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal partial class SolutionFileReader
+{
+    public static Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, CancellationToken cancellationToken)
+    {
+        return ReadSolutionFileAsync(solutionFilePath, new PathResolver(diagnosticReporter: null), cancellationToken);
+    }
+
+    public static async Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, CancellationToken cancellationToken)
+    {
+        Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath));
+
+        // When passed a solution filter, we need to read the filter file to get the solution path and included project paths.
+        var projectFilter = ImmutableHashSet<string>.Empty;
+        if (SolutionFilterReader.IsSolutionFilterFilename(absoluteSolutionPath) &&
+            !SolutionFilterReader.TryRead(absoluteSolutionPath, pathResolver, out absoluteSolutionPath, out projectFilter))
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_filter_0, solutionFilePath));
+        }
+
+        var projects = await TryReadSolutionFileAsync(absoluteSolutionPath, pathResolver, projectFilter, cancellationToken).ConfigureAwait(false);
+        if (!projects.HasValue)
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_0, absoluteSolutionPath));
+        }
+
+        return (absoluteSolutionPath, projects.Value);
+    }
+
+    private static async Task<ImmutableArray<(string ProjectPath, string ProjectGuid)>?> TryReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, ImmutableHashSet<string> projectFilter, CancellationToken cancellationToken)
+    {
+        var serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
+        if (serializer == null)
+        {
+            return null;
+        }
+
+        // The solution folder is the base directory for project paths.
+        var baseDirectory = Path.GetDirectoryName(solutionFilePath);
+        RoslynDebug.AssertNotNull(baseDirectory);
+
+        var solutionModel = await serializer.OpenAsync(solutionFilePath, cancellationToken).ConfigureAwait(false);
+
+        var builder = ImmutableArray.CreateBuilder<(string ProjectPath, string ProjectGuid)>();
+        foreach (var projectModel in solutionModel.SolutionProjects)
+        {
+            // If we are filtering based on a solution filter, then we need to verify the project is included.
+            if (!projectFilter.IsEmpty)
path: src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.cs
user: login: jasonmalinowski
body: If you have an empty filter, what happens? Should this use a nullable set to make it more clear?
created_at: 2025-05-28T23:42:04Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal partial class SolutionFileReader
+{
+    public static Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, CancellationToken cancellationToken)
+    {
+        return ReadSolutionFileAsync(solutionFilePath, new PathResolver(diagnosticReporter: null), cancellationToken);
+    }
+
+    public static async Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, CancellationToken cancellationToken)
+    {
+        Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath));
+
+        // When passed a solution filter, we need to read the filter file to get the solution path and included project paths.
+        var projectFilter = ImmutableHashSet<string>.Empty;
+        if (SolutionFilterReader.IsSolutionFilterFilename(absoluteSolutionPath) &&
+            !SolutionFilterReader.TryRead(absoluteSolutionPath, pathResolver, out absoluteSolutionPath, out projectFilter))
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_filter_0, solutionFilePath));
+        }
+
+        var projects = await TryReadSolutionFileAsync(absoluteSolutionPath, pathResolver, projectFilter, cancellationToken).ConfigureAwait(false);
+        if (!projects.HasValue)
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_0, absoluteSolutionPath));
+        }
+
+        return (absoluteSolutionPath, projects.Value);
+    }
+
+    private static async Task<ImmutableArray<(string ProjectPath, string ProjectGuid)>?> TryReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, ImmutableHashSet<string> projectFilter, CancellationToken cancellationToken)
+    {
+        var serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
+        if (serializer == null)
+        {
+            return null;
+        }
+
+        // The solution folder is the base directory for project paths.
+        var baseDirectory = Path.GetDirectoryName(solutionFilePath);
+        RoslynDebug.AssertNotNull(baseDirectory);
+
+        var solutionModel = await serializer.OpenAsync(solutionFilePath, cancellationToken).ConfigureAwait(false);
+
+        var builder = ImmutableArray.CreateBuilder<(string ProjectPath, string ProjectGuid)>();
+        foreach (var projectModel in solutionModel.SolutionProjects)
+        {
+            // If we are filtering based on a solution filter, then we need to verify the project is included.
+            if (!projectFilter.IsEmpty)
+            {
+                Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteProjectPath(projectModel.FilePath, baseDirectory, DiagnosticReportingMode.Throw, out var absoluteProjectPath));
+                if (!projectFilter.Contains(absoluteProjectPath))
+                {
+                    continue;
+                }
+            }
+
+            builder.Add((projectModel.FilePath, projectModel.Id.ToString()));
path: src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.cs
user: login: jasonmalinowski
body: Is the file path here still relative? Should we just make it absolute here too since we're already doing path resolution?
created_at: 2025-05-28T23:43:04Z
author_association: MEMBER
diff_hunk: @@ -22,10 +22,17 @@
     </PackageDescription>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="Microsoft.Build" />
-    <PackageReference Include="Microsoft.Build.Tasks.Core" />
+    <PackageReference Include="Microsoft.Build.Framework" />
     <PackageReference Include="Microsoft.Extensions.Logging" />
+    <PackageReference Include="Microsoft.VisualStudio.SolutionPersistence" />
     <PackageReference Include="Newtonsoft.Json" />
+    <!--
+      Since System.Text.Json is part of the .NET BCL we do not want to add
+      it as a package reference. Doing so will pin the version to what we use
+      in our build instead of allowing it to use the version from the .NET Runtime
+      we are running against.
path: src/Workspaces/MSBuild/Core/Microsoft.CodeAnalysis.Workspaces.MSBuild.csproj
user: login: jasonmalinowski
body: Is System.Text.Json extra special here, or should we be doing this for other references too?
created_at: 2025-05-28T23:43:44Z
author_association: MEMBER
diff_hunk: @@ -35,13 +35,7 @@ public static bool TryRead(string filterFilename, PathResolver pathResolver, [No
                     return false;
                 }
 
-                if (!pathResolver.TryGetAbsoluteSolutionPath(solutionPath, baseDirectory, DiagnosticReportingMode.Throw, out solutionFilename))
-                {
-                    // TryGetAbsoluteSolutionPath should throw before we get here.
path: src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.SolutionFilterReader.cs
user: login: jasonmalinowski
body: Might be good to bring this comment back -- since if you're not aware of the DiagnosticReportingMode bit this might seem a bit odd...
created_at: 2025-05-28T23:44:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,75 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Immutable;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.SolutionPersistence.Serializer;
+using Roslyn.Utilities;
+
+namespace Microsoft.CodeAnalysis.MSBuild;
+
+internal partial class SolutionFileReader
+{
+    public static Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, CancellationToken cancellationToken)
+    {
+        return ReadSolutionFileAsync(solutionFilePath, new PathResolver(diagnosticReporter: null), cancellationToken);
+    }
+
+    public static async Task<(string AbsoluteSolutionPath, ImmutableArray<(string ProjectPath, string ProjectGuid)> Projects)> ReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, CancellationToken cancellationToken)
+    {
+        Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteSolutionPath(solutionFilePath, baseDirectory: Directory.GetCurrentDirectory(), DiagnosticReportingMode.Throw, out var absoluteSolutionPath));
+
+        // When passed a solution filter, we need to read the filter file to get the solution path and included project paths.
+        var projectFilter = ImmutableHashSet<string>.Empty;
+        if (SolutionFilterReader.IsSolutionFilterFilename(absoluteSolutionPath) &&
+            !SolutionFilterReader.TryRead(absoluteSolutionPath, pathResolver, out absoluteSolutionPath, out projectFilter))
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_filter_0, solutionFilePath));
+        }
+
+        var projects = await TryReadSolutionFileAsync(absoluteSolutionPath, pathResolver, projectFilter, cancellationToken).ConfigureAwait(false);
+        if (!projects.HasValue)
+        {
+            throw new Exception(string.Format(WorkspaceMSBuildResources.Failed_to_load_solution_0, absoluteSolutionPath));
+        }
+
+        return (absoluteSolutionPath, projects.Value);
+    }
+
+    private static async Task<ImmutableArray<(string ProjectPath, string ProjectGuid)>?> TryReadSolutionFileAsync(string solutionFilePath, PathResolver pathResolver, ImmutableHashSet<string> projectFilter, CancellationToken cancellationToken)
+    {
+        var serializer = SolutionSerializers.GetSerializerByMoniker(solutionFilePath);
+        if (serializer == null)
+        {
+            return null;
+        }
+
+        // The solution folder is the base directory for project paths.
+        var baseDirectory = Path.GetDirectoryName(solutionFilePath);
+        RoslynDebug.AssertNotNull(baseDirectory);
+
+        var solutionModel = await serializer.OpenAsync(solutionFilePath, cancellationToken).ConfigureAwait(false);
+
+        var builder = ImmutableArray.CreateBuilder<(string ProjectPath, string ProjectGuid)>();
+        foreach (var projectModel in solutionModel.SolutionProjects)
+        {
+            // If we are filtering based on a solution filter, then we need to verify the project is included.
+            if (!projectFilter.IsEmpty)
+            {
+                Contract.ThrowIfFalse(pathResolver.TryGetAbsoluteProjectPath(projectModel.FilePath, baseDirectory, DiagnosticReportingMode.Throw, out var absoluteProjectPath));
+                if (!projectFilter.Contains(absoluteProjectPath))
+                {
+                    continue;
+                }
+            }
+
+            builder.Add((projectModel.FilePath, projectModel.Id.ToString()));
path: src/Workspaces/MSBuild/Core/MSBuild/SolutionFileReader.cs
user: login: jasonmalinowski
body: Fixed in https://github.com/dotnet/roslyn/pull/78772.
created_at: 2025-05-30T20:29:19Z
author_association: MEMBER

PR comments:
user: login: RikkiGibson
created_at: 2025-05-19T20:49:21Z
body: What is the effect of the change? Is it reducing the net msbuild surface we are depending on / making it harder to inadvertently use msbuild APIs that we should not be using?
author_association: MEMBER
user: login: JoeRobich
created_at: 2025-05-19T20:55:12Z
body: > What is the effect of the change?

Currently our package brings in a broad set of MSBuild dependencies (see below). This change limits it to only the dependency that is necessary.

<img width="406" alt="image" src="https://github.com/user-attachments/assets/b2ec066f-797e-449f-90b8-289251907d2d" />

author_association: MEMBER
user: login: RikkiGibson
created_at: 2025-05-19T21:12:01Z
body: It looks like possibly a subset of the transitive dependencies need to be brought back to fix the build
author_association: MEMBER
user: login: johnkors
created_at: 2025-06-04T12:46:00Z
body: Can someone tell me how to read a slnx-file programatically after this change?

Using `Microsoft.CodeAnalysis.CSharp.Workspaces/4.14.0` &

```cs
await workspace.OpenSolutionAsync("some.slnx");
```

still throws exceptions. 

Is there another way?
author_association: NONE