diff --git a/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs b/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs index b4adf03ea70c..11a88db5fe9b 100644 --- a/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs +++ b/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs @@ -22,6 +22,16 @@ public sealed class VirtualProjectBuilder private (ImmutableArray Original, ImmutableArray Evaluated)? _evaluatedDirectives; + /// + /// Prevents the virtual project's from being garbage collected + /// when MSBuild's demotes it to a weak reference + /// (which can happen when many SDK import files fill the cache during NuGet restore). + /// Without this, nested <MSBuild> tasks that re-evaluate the project with different properties + /// would fail to find the in the cache and try to load it from disk, + /// resulting in MSB4025 because the virtual project file does not exist on disk. + /// + private ProjectRootElement? _projectRootElement; + internal string EntryPointFileFullPath { get; } internal SourceFile EntryPointSourceFile @@ -394,6 +404,7 @@ ProjectRootElement CreateProjectRootElement(string projectFileText, ProjectColle using var xmlReader = XmlReader.Create(reader); var projectRoot = ProjectRootElement.Create(xmlReader, projectCollection); projectRoot.FullPath = GetVirtualProjectPath(EntryPointFileFullPath); + _projectRootElement = projectRoot; return projectRoot; } } diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs index cab95ebf77b3..9810feeb8d6e 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs @@ -6432,4 +6432,52 @@ Dictionary ReadFiles() return result; } } + + /// + /// Regression test for https://github.com/dotnet/sdk/issues/52714. + /// The virtual project's must survive GC + /// even after being evicted from MSBuild's strong cache (LRU of size N). + /// We force eviction via MSBUILDPROJECTROOTELEMENTCACHESIZE=1 + /// and trigger GC via an inline task during NuGet restore. + /// Without the fix (strong reference in VirtualProjectBuilder._projectRootElement), + /// this fails with MSB4025 "The project file could not be loaded". + /// + [Fact] + public void VirtualProject_SurvivesGCDuringRestore() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + Console.WriteLine("Hello from virtual project"); + """); + + // Directory.Build.targets that forces GC during restore, + // after SDK imports have already evicted the virtual PRE from the strong cache. + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.targets"), """ + + + + + + + + <_ForceGCTask /> + + + """); + + new DotnetCommand(Log, "run", "--no-cache", "Program.cs") + // A cache size of 1 ensures the virtual PRE is evicted from the strong cache + // as soon as any SDK .targets/.props file is loaded during evaluation. + .WithEnvironmentVariable("MSBUILDPROJECTROOTELEMENTCACHESIZE", "1") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass() + .And.HaveStdOut("Hello from virtual project"); + } }