diff --git a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs index 76bba800ff4a..f1bc3ab7d655 100644 --- a/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs +++ b/src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs @@ -73,7 +73,8 @@ public override int Execute() using var stream = File.Open(projectFile, FileMode.Create, FileAccess.Write); using var writer = new StreamWriter(stream, Encoding.UTF8); VirtualProjectBuildingCommand.WriteProjectFile(writer, UpdateDirectives(directives), isVirtualProject: false, - userSecretsId: DetermineUserSecretsId()); + userSecretsId: DetermineUserSecretsId(), + excludeDefaultProperties: FindDefaultPropertiesToExclude()); } // Copy or move over included items. @@ -184,6 +185,18 @@ ImmutableArray UpdateDirectives(ImmutableArray return result.DrainToImmutable(); } + + IEnumerable FindDefaultPropertiesToExclude() + { + foreach (var (name, defaultValue) in VirtualProjectBuildingCommand.DefaultProperties) + { + string projectValue = projectInstance.GetPropertyValue(name); + if (!string.Equals(projectValue, defaultValue, StringComparison.OrdinalIgnoreCase)) + { + yield return name; + } + } + } } private string DetermineOutputDirectory(string file) diff --git a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs index 8da06cc0a47f..17f5ce1b226e 100644 --- a/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs +++ b/src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs @@ -75,7 +75,7 @@ internal sealed class VirtualProjectBuildingCommand : CommandBase /// /// Kept in sync with the default dotnet new console project file (enforced by DotnetProjectAddTests.SameAsTemplate). /// - private static readonly FrozenDictionary s_defaultProperties = FrozenDictionary.Create(StringComparer.OrdinalIgnoreCase, + public static readonly FrozenDictionary DefaultProperties = FrozenDictionary.Create(StringComparer.OrdinalIgnoreCase, [ new("OutputType", "Exe"), new("TargetFramework", $"net{TargetFrameworkVersion}"), @@ -1141,8 +1141,12 @@ public static void WriteProjectFile( string? targetFilePath = null, string? artifactsPath = null, bool includeRuntimeConfigInformation = true, - string? userSecretsId = null) + string? userSecretsId = null, + IEnumerable? excludeDefaultProperties = null) { + Debug.Assert(userSecretsId == null || !isVirtualProject); + Debug.Assert(excludeDefaultProperties == null || !isVirtualProject); + int processedDirectives = 0; var sdkDirectives = directives.OfType(); @@ -1181,6 +1185,20 @@ public static void WriteProjectFile( artifacts/$(MSBuildProjectName) artifacts/$(MSBuildProjectName) true + false + true + """); + + // Write default properties before importing SDKs so they can be overridden by SDKs + // (and implicit build files which are imported by the default .NET SDK). + foreach (var (name, value) in DefaultProperties) + { + writer.WriteLine($""" + <{name}>{EscapeValue(value)} + """); + } + + writer.WriteLine($""" @@ -1247,34 +1265,30 @@ public static void WriteProjectFile( """); // First write the default properties except those specified by the user. - var customPropertyNames = propertyDirectives.Select(d => d.Name).ToHashSet(StringComparer.OrdinalIgnoreCase); - foreach (var (name, value) in s_defaultProperties) + if (!isVirtualProject) { - if (!customPropertyNames.Contains(name)) + var customPropertyNames = propertyDirectives + .Select(static d => d.Name) + .Concat(excludeDefaultProperties ?? []) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + foreach (var (name, value) in DefaultProperties) + { + if (!customPropertyNames.Contains(name)) + { + writer.WriteLine($""" + <{name}>{EscapeValue(value)} + """); + } + } + + if (userSecretsId != null && !customPropertyNames.Contains("UserSecretsId")) { writer.WriteLine($""" - <{name}>{EscapeValue(value)} + {EscapeValue(userSecretsId)} """); } } - if (userSecretsId != null && !customPropertyNames.Contains("UserSecretsId")) - { - writer.WriteLine($""" - {EscapeValue(userSecretsId)} - """); - } - - // Write virtual-only properties. - if (isVirtualProject) - { - writer.WriteLine(""" - false - true - false - """); - } - // Write custom properties. foreach (var property in propertyDirectives) { @@ -1289,6 +1303,7 @@ public static void WriteProjectFile( if (isVirtualProject) { writer.WriteLine(""" + false $(Features);FileBasedProgram """); } diff --git a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs index 5cdf670159a3..541788369fb5 100644 --- a/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs +++ b/test/dotnet.Tests/CommandTests/Run/RunFileTests.cs @@ -878,6 +878,128 @@ public void DirectoryBuildProps() .And.HaveStdOut("Hello from TestName"); } + /// + /// Overriding default (implicit) properties of file-based apps via implicit build files. + /// + [Fact] + public void DefaultProps_DirectoryBuildProps() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """ + Console.WriteLine("Hi"); + """); + File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """ + + + disable + + + """); + + new DotnetCommand(Log, "run", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Fail() + // error CS0103: The name 'Console' does not exist in the current context + .And.HaveStdOutContaining("error CS0103"); + + // Converting to a project should not change the behavior. + + new DotnetCommand(Log, "project", "convert", "Program.cs") + .WithWorkingDirectory(testInstance.Path) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run") + .WithWorkingDirectory(Path.Join(testInstance.Path, "Program")) + .Execute() + .Should().Fail() + // error CS0103: The name 'Console' does not exist in the current context + .And.HaveStdOutContaining("error CS0103"); + } + + /// + /// Overriding default (implicit) properties of file-based apps from custom SDKs. + /// + [Fact] + public void DefaultProps_CustomSdk() + { + var testInstance = _testAssetsManager.CreateTestDirectory(); + + var sdkDir = Path.Join(testInstance.Path, "MySdk"); + Directory.CreateDirectory(sdkDir); + File.WriteAllText(Path.Join(sdkDir, "Sdk.props"), """ + + + disable + + + """); + File.WriteAllText(Path.Join(sdkDir, "Sdk.targets"), """ + + """); + File.WriteAllText(Path.Join(sdkDir, "MySdk.csproj"), $""" + + + {ToolsetInfo.CurrentTargetFramework} + MSBuildSdk + false + + + + + + """); + + new DotnetCommand(Log, "pack") + .WithWorkingDirectory(sdkDir) + .Execute() + .Should().Pass(); + + var appDir = Path.Join(testInstance.Path, "app"); + Directory.CreateDirectory(appDir); + File.WriteAllText(Path.Join(appDir, "NuGet.config"), $""" + + + + + + + """); + File.WriteAllText(Path.Join(appDir, "Program.cs"), """ + #:sdk Microsoft.NET.Sdk + #:sdk MySdk@1.0.0 + Console.WriteLine("Hi"); + """); + + // Use custom package cache to avoid reuse of the custom SDK packed by previous test runs. + var packagesDir = Path.Join(testInstance.Path, ".packages"); + + new DotnetCommand(Log, "run", "Program.cs") + .WithEnvironmentVariable("NUGET_PACKAGES", packagesDir) + .WithWorkingDirectory(appDir) + .Execute() + .Should().Fail() + // error CS0103: The name 'Console' does not exist in the current context + .And.HaveStdOutContaining("error CS0103"); + + // Converting to a project should not change the behavior. + + new DotnetCommand(Log, "project", "convert", "Program.cs") + .WithEnvironmentVariable("NUGET_PACKAGES", packagesDir) + .WithWorkingDirectory(appDir) + .Execute() + .Should().Pass(); + + new DotnetCommand(Log, "run") + .WithEnvironmentVariable("NUGET_PACKAGES", packagesDir) + .WithWorkingDirectory(Path.Join(appDir, "Program")) + .Execute() + .Should().Fail() + // error CS0103: The name 'Console' does not exist in the current context + .And.HaveStdOutContaining("error CS0103"); + } + [Fact] public void ComputeRunArguments_Success() { @@ -3441,6 +3563,14 @@ public void Api() artifacts/$(MSBuildProjectName) artifacts/$(MSBuildProjectName) true + false + true + Exe + {ToolsetInfo.CurrentTargetFramework} + enable + enable + true + true @@ -3451,16 +3581,9 @@ public void Api() - Exe - enable - enable - true - true - false - true - false net11.0 preview + false $(Features);FileBasedProgram @@ -3512,6 +3635,14 @@ public void Api_Diagnostic_01() artifacts/$(MSBuildProjectName) artifacts/$(MSBuildProjectName) true + false + true + Exe + {ToolsetInfo.CurrentTargetFramework} + enable + enable + true + true @@ -3521,14 +3652,6 @@ public void Api_Diagnostic_01() - Exe - {ToolsetInfo.CurrentTargetFramework} - enable - enable - true - true - false - true false $(Features);FileBasedProgram @@ -3580,6 +3703,14 @@ public void Api_Diagnostic_02() artifacts/$(MSBuildProjectName) artifacts/$(MSBuildProjectName) true + false + true + Exe + {ToolsetInfo.CurrentTargetFramework} + enable + enable + true + true @@ -3589,14 +3720,6 @@ public void Api_Diagnostic_02() - Exe - {ToolsetInfo.CurrentTargetFramework} - enable - enable - true - true - false - true false $(Features);FileBasedProgram