Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ public sealed class VirtualProjectBuilder

private (ImmutableArray<CSharpDirective> Original, ImmutableArray<CSharpDirective> Evaluated)? _evaluatedDirectives;

/// <summary>
/// Prevents the virtual project's <see cref="ProjectRootElement"/> from being garbage collected
/// when MSBuild's <see cref="ProjectRootElementCache"/> demotes it to a weak reference
/// (which can happen when many SDK import files fill the cache during NuGet restore).
/// Without this, nested <c>&lt;MSBuild&gt;</c> tasks that re-evaluate the project with different properties
/// would fail to find the <see cref="ProjectRootElement"/> in the cache and try to load it from disk,
/// resulting in MSB4025 because the virtual project file does not exist on disk.
/// </summary>
private ProjectRootElement? _projectRootElement;

internal string EntryPointFileFullPath { get; }

internal SourceFile EntryPointSourceFile
Expand Down Expand Up @@ -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;
}
}
Expand Down
48 changes: 48 additions & 0 deletions test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6432,4 +6432,52 @@ Dictionary<string, string> ReadFiles()
return result;
}
}

/// <summary>
/// Regression test for https://github.com/dotnet/sdk/issues/52714.
/// The virtual project's <see cref="ProjectRootElement"/> must survive GC
/// even after being evicted from MSBuild's strong cache (LRU of size N).
/// We force eviction via <c>MSBUILDPROJECTROOTELEMENTCACHESIZE=1</c>
/// and trigger GC via an inline task during NuGet restore.
/// Without the fix (strong reference in <c>VirtualProjectBuilder._projectRootElement</c>),
/// this fails with MSB4025 "The project file could not be loaded".
/// </summary>
[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"), """
<Project>
<UsingTask TaskName="_ForceGCTask"
TaskFactory="RoslynCodeTaskFactory"
AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll">
<Task>
<Code Type="Fragment" Language="cs"><![CDATA[
System.GC.Collect(2, System.GCCollectionMode.Forced, blocking: true);
System.GC.WaitForPendingFinalizers();
System.GC.Collect(2, System.GCCollectionMode.Forced, blocking: true);
]]></Code>
</Task>
</UsingTask>
<Target Name="_ForceGC" BeforeTargets="_FilterRestoreGraphProjectInputItems">
<_ForceGCTask />
</Target>
</Project>
""");

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");
}
}
Loading