From 3135dd6fab71edacf966b0d2d9c92980eaeb4d07 Mon Sep 17 00:00:00 2001 From: Jan Jones Date: Fri, 27 Feb 2026 11:06:01 +0100 Subject: [PATCH 1/2] Keep `.cs` in file-based app virtual `.csproj` file name --- .../dotnet/Commands/Run/Api/RunApiCommand.cs | 4 ++- .../VirtualProjectBuilder.cs | 4 +-- .../CommandTests/Run/RunFileTests.cs | 31 ++++++++++++++++--- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs b/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs index 8960eabd51cc..9504d64da1dd 100644 --- a/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs +++ b/src/Cli/dotnet/Commands/Run/Api/RunApiCommand.cs @@ -75,7 +75,7 @@ public override RunApiOutput Execute() builder.CreateProjectInstance( new ProjectCollection(), errorReporter, - out _, + out var project, out var evaluatedDirectives, validateAllDirectives: true); @@ -91,6 +91,7 @@ public override RunApiOutput Execute() return new RunApiOutput.Project { Content = csprojWriter.ToString(), + ProjectPath = project.FullPath, Diagnostics = diagnostics.ToImmutableArray(), }; } @@ -163,6 +164,7 @@ public sealed class Error : RunApiOutput public sealed class Project : RunApiOutput { public required string Content { get; init; } + public required string ProjectPath { get; init; } public required ImmutableArray Diagnostics { get; init; } } diff --git a/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs b/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs index 2356b742ea2c..508eb7870630 100644 --- a/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs +++ b/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs @@ -85,7 +85,7 @@ internal static string GetArtifactsPath(string entryPointFileFullPath) } public static string GetVirtualProjectPath(string entryPointFilePath) - => Path.ChangeExtension(entryPointFilePath, ".csproj"); + => entryPointFilePath + ".csproj"; /// /// Obtains a temporary subdirectory for file-based app artifacts, e.g., /tmp/dotnet/runfile/. @@ -375,7 +375,7 @@ ProjectRootElement CreateProjectRootElement(string projectFileText, ProjectColle using var reader = new StringReader(projectFileText); using var xmlReader = XmlReader.Create(reader); var projectRoot = ProjectRootElement.Create(xmlReader, projectCollection); - projectRoot.FullPath = Path.ChangeExtension(EntryPointFileFullPath, ".csproj"); + projectRoot.FullPath = GetVirtualProjectPath(EntryPointFileFullPath); return projectRoot; } } diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs index c908a7142cf2..a7a18165dd37 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs @@ -1903,6 +1903,23 @@ public void EmbeddedResource_AlongsideProj([CombinatorialValues("sln", "slnx", " .And.HaveStdOut(considered ? "Resource not found" : "[MyString, TestValue]"); } + [Fact] + public void Restore_NonExistentPackage() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + var programFile = Path.Join(testInstance.Path, "Program.cs"); + File.WriteAllText(programFile, """ + #:package Microsoft.ThisPackageDoesNotExist@1.0.0 + Console.WriteLine(); + """); + + new DotnetCommand(Log, "restore", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + .And.HaveStdOutContaining("Program.cs.csproj : error NU1101"); + } + [Fact] public void NoRestore_01() { @@ -5520,6 +5537,7 @@ public void Api() Console.WriteLine(); """); + var projectPath = VirtualProjectBuilder.GetVirtualProjectPath(programPath); new DotnetCommand(Log, "run-api") .WithStandardInput($$""" {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"} @@ -5579,7 +5597,7 @@ public void Api() - """)}},"Diagnostics":[]} + """)}},"ProjectPath":{{ToJson(projectPath)}},"Diagnostics":[]} """); } @@ -5609,6 +5627,7 @@ public void Api_Evaluation() var bPath = Path.Join(testInstance.Path, "B.cs"); File.WriteAllText(bPath, ""); + var projectPath = VirtualProjectBuilder.GetVirtualProjectPath(programPath); new DotnetCommand(Log, "run-api") .WithStandardInput($$""" {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"} @@ -5667,7 +5686,7 @@ public void Api_Evaluation() - """)}},"Diagnostics":[]} + """)}},"ProjectPath":{{ToJson(projectPath)}},"Diagnostics":[]} """); } @@ -5681,6 +5700,7 @@ public void Api_Diagnostic_01() #:property LangVersion=preview """); + var projectPath = VirtualProjectBuilder.GetVirtualProjectPath(programPath); new DotnetCommand(Log, "run-api") .WithStandardInput($$""" {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"} @@ -5734,7 +5754,7 @@ public void Api_Diagnostic_01() - """)}},"Diagnostics": + """)}},"ProjectPath":{{ToJson(projectPath)}},"Diagnostics": [{"Location":{ "Path":{{ToJson(programPath)}}, "Span":{"Start":{"Line":1,"Character":0},"End":{"Line":1,"Character":30}{{nop}}}{{nop}}}, @@ -5752,6 +5772,7 @@ public void Api_Diagnostic_02() Console.WriteLine(); """); + var projectPath = VirtualProjectBuilder.GetVirtualProjectPath(programPath); new DotnetCommand(Log, "run-api") .WithStandardInput($$""" {"$type":"GetProject","EntryPointFileFullPath":{{ToJson(programPath)}},"ArtifactsPath":"/artifacts"} @@ -5805,7 +5826,7 @@ public void Api_Diagnostic_02() - """)}},"Diagnostics": + """)}},"ProjectPath":{{ToJson(projectPath)}},"Diagnostics": [{"Location":{ "Path":{{ToJson(programPath)}}, "Span":{"Start":{"Line":0,"Character":0},"End":{"Line":1,"Character":0}{{nop}}}{{nop}}}, @@ -5840,7 +5861,7 @@ public void Api_RunCommand() """); string artifactsPath = OperatingSystem.IsWindows() ? @"C:\artifacts" : "/artifacts"; - string executablePath = OperatingSystem.IsWindows() ? @"C:\artifacts\bin\debug\Program.exe" : "/artifacts/bin/debug/Program"; + string executablePath = OperatingSystem.IsWindows() ? @"C:\artifacts\bin\debug\Program.cs.exe" : "/artifacts/bin/debug/Program.cs"; new DotnetCommand(Log, "run-api") // The command outputs only _custom_ environment variables (not inherited ones), // so make sure we don't pass DOTNET_ROOT_* so we can assert that it is set by the run command. From 661d6b68f3c855ac76c08c8f98d67efac2780284 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 3 Mar 2026 01:35:37 +0100 Subject: [PATCH 2/2] Preserve binary name when virtual project uses `.cs.csproj` extension Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jjonescz <3669664+jjonescz@users.noreply.github.com> --- documentation/general/dotnet-run-file.md | 3 +++ src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs | 2 ++ test/dotnet.Tests/CommandTests/Run/RunFileTests.cs | 6 +++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/documentation/general/dotnet-run-file.md b/documentation/general/dotnet-run-file.md index 3fe98605ab2b..c8d9b213282c 100644 --- a/documentation/general/dotnet-run-file.md +++ b/documentation/general/dotnet-run-file.md @@ -65,6 +65,9 @@ Additionally, the implicit project file has the following customizations: This avoids including files like `./**/*.resx` in simple file-based apps where users usually don't expect that. See [multiple files](#multiple-files) for more details. + - `AssemblyName` is set to the entry-point file name without extension (e.g., `Program` for `Program.cs`) + to ensure the binary name is not affected by the virtual project file name (which includes the `.cs` extension). + ## Grow up When file-based programs reach an inflection point where build customizations in a project file are needed, diff --git a/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs b/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs index 508eb7870630..1d651ca113f7 100644 --- a/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs +++ b/src/Microsoft.DotNet.ProjectTools/VirtualProjectBuilder.cs @@ -465,6 +465,7 @@ internal static void WriteProjectFile( if (isVirtualProject) { Debug.Assert(!string.IsNullOrWhiteSpace(artifactsPath)); + Debug.Assert(entryPointFilePath is not null); // Note that ArtifactsPath needs to be specified before Sdk.props // (usually it's recommended to specify it in Directory.Build.props @@ -481,6 +482,7 @@ internal static void WriteProjectFile( {CSharpDirective.IncludeOrExclude.DefaultMappingString} false true + {EscapeValue(Path.GetFileNameWithoutExtension(entryPointFilePath))} """); // Only set these to false when using the default SDK with no additional SDKs diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs index a7a18165dd37..28b45358846f 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs @@ -5557,6 +5557,7 @@ public void Api() .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content false true + Program Exe {ToolsetInfo.CurrentTargetFramework} enable @@ -5647,6 +5648,7 @@ public void Api_Evaluation() .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content false true + Program false false Exe @@ -5720,6 +5722,7 @@ public void Api_Diagnostic_01() .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content false true + Program false false Exe @@ -5792,6 +5795,7 @@ public void Api_Diagnostic_02() .cs=Compile;.resx=EmbeddedResource;.json=None;.razor=Content false true + Program false false Exe @@ -5861,7 +5865,7 @@ public void Api_RunCommand() """); string artifactsPath = OperatingSystem.IsWindows() ? @"C:\artifacts" : "/artifacts"; - string executablePath = OperatingSystem.IsWindows() ? @"C:\artifacts\bin\debug\Program.cs.exe" : "/artifacts/bin/debug/Program.cs"; + string executablePath = OperatingSystem.IsWindows() ? @"C:\artifacts\bin\debug\Program.exe" : "/artifacts/bin/debug/Program"; new DotnetCommand(Log, "run-api") // The command outputs only _custom_ environment variables (not inherited ones), // so make sure we don't pass DOTNET_ROOT_* so we can assert that it is set by the run command.