Skip to content

Commit 99a5ce1

Browse files
authored
Allow SDKs to overwrite default file-based app properties (#51004)
1 parent 35c5339 commit 99a5ce1

File tree

3 files changed

+199
-48
lines changed

3 files changed

+199
-48
lines changed

src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ public override int Execute()
7373
using var stream = File.Open(projectFile, FileMode.Create, FileAccess.Write);
7474
using var writer = new StreamWriter(stream, Encoding.UTF8);
7575
VirtualProjectBuildingCommand.WriteProjectFile(writer, UpdateDirectives(directives), isVirtualProject: false,
76-
userSecretsId: DetermineUserSecretsId());
76+
userSecretsId: DetermineUserSecretsId(),
77+
excludeDefaultProperties: FindDefaultPropertiesToExclude());
7778
}
7879

7980
// Copy or move over included items.
@@ -184,6 +185,18 @@ ImmutableArray<CSharpDirective> UpdateDirectives(ImmutableArray<CSharpDirective>
184185

185186
return result.DrainToImmutable();
186187
}
188+
189+
IEnumerable<string> FindDefaultPropertiesToExclude()
190+
{
191+
foreach (var (name, defaultValue) in VirtualProjectBuildingCommand.DefaultProperties)
192+
{
193+
string projectValue = projectInstance.GetPropertyValue(name);
194+
if (!string.Equals(projectValue, defaultValue, StringComparison.OrdinalIgnoreCase))
195+
{
196+
yield return name;
197+
}
198+
}
199+
}
187200
}
188201

189202
private string DetermineOutputDirectory(string file)

src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ internal sealed class VirtualProjectBuildingCommand : CommandBase
7575
/// <remarks>
7676
/// Kept in sync with the default <c>dotnet new console</c> project file (enforced by <c>DotnetProjectAddTests.SameAsTemplate</c>).
7777
/// </remarks>
78-
private static readonly FrozenDictionary<string, string> s_defaultProperties = FrozenDictionary.Create<string, string>(StringComparer.OrdinalIgnoreCase,
78+
public static readonly FrozenDictionary<string, string> DefaultProperties = FrozenDictionary.Create<string, string>(StringComparer.OrdinalIgnoreCase,
7979
[
8080
new("OutputType", "Exe"),
8181
new("TargetFramework", $"net{TargetFrameworkVersion}"),
@@ -1141,8 +1141,12 @@ public static void WriteProjectFile(
11411141
string? targetFilePath = null,
11421142
string? artifactsPath = null,
11431143
bool includeRuntimeConfigInformation = true,
1144-
string? userSecretsId = null)
1144+
string? userSecretsId = null,
1145+
IEnumerable<string>? excludeDefaultProperties = null)
11451146
{
1147+
Debug.Assert(userSecretsId == null || !isVirtualProject);
1148+
Debug.Assert(excludeDefaultProperties == null || !isVirtualProject);
1149+
11461150
int processedDirectives = 0;
11471151

11481152
var sdkDirectives = directives.OfType<CSharpDirective.Sdk>();
@@ -1181,6 +1185,20 @@ public static void WriteProjectFile(
11811185
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
11821186
<PackageOutputPath>artifacts/$(MSBuildProjectName)</PackageOutputPath>
11831187
<FileBasedProgram>true</FileBasedProgram>
1188+
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
1189+
<DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder>
1190+
""");
1191+
1192+
// Write default properties before importing SDKs so they can be overridden by SDKs
1193+
// (and implicit build files which are imported by the default .NET SDK).
1194+
foreach (var (name, value) in DefaultProperties)
1195+
{
1196+
writer.WriteLine($"""
1197+
<{name}>{EscapeValue(value)}</{name}>
1198+
""");
1199+
}
1200+
1201+
writer.WriteLine($"""
11841202
</PropertyGroup>
11851203
11861204
<ItemGroup>
@@ -1247,34 +1265,30 @@ public static void WriteProjectFile(
12471265
""");
12481266

12491267
// First write the default properties except those specified by the user.
1250-
var customPropertyNames = propertyDirectives.Select(d => d.Name).ToHashSet(StringComparer.OrdinalIgnoreCase);
1251-
foreach (var (name, value) in s_defaultProperties)
1268+
if (!isVirtualProject)
12521269
{
1253-
if (!customPropertyNames.Contains(name))
1270+
var customPropertyNames = propertyDirectives
1271+
.Select(static d => d.Name)
1272+
.Concat(excludeDefaultProperties ?? [])
1273+
.ToHashSet(StringComparer.OrdinalIgnoreCase);
1274+
foreach (var (name, value) in DefaultProperties)
1275+
{
1276+
if (!customPropertyNames.Contains(name))
1277+
{
1278+
writer.WriteLine($"""
1279+
<{name}>{EscapeValue(value)}</{name}>
1280+
""");
1281+
}
1282+
}
1283+
1284+
if (userSecretsId != null && !customPropertyNames.Contains("UserSecretsId"))
12541285
{
12551286
writer.WriteLine($"""
1256-
<{name}>{EscapeValue(value)}</{name}>
1287+
<UserSecretsId>{EscapeValue(userSecretsId)}</UserSecretsId>
12571288
""");
12581289
}
12591290
}
12601291

1261-
if (userSecretsId != null && !customPropertyNames.Contains("UserSecretsId"))
1262-
{
1263-
writer.WriteLine($"""
1264-
<UserSecretsId>{EscapeValue(userSecretsId)}</UserSecretsId>
1265-
""");
1266-
}
1267-
1268-
// Write virtual-only properties.
1269-
if (isVirtualProject)
1270-
{
1271-
writer.WriteLine("""
1272-
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
1273-
<DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder>
1274-
<RestoreUseStaticGraphEvaluation>false</RestoreUseStaticGraphEvaluation>
1275-
""");
1276-
}
1277-
12781292
// Write custom properties.
12791293
foreach (var property in propertyDirectives)
12801294
{
@@ -1289,6 +1303,7 @@ public static void WriteProjectFile(
12891303
if (isVirtualProject)
12901304
{
12911305
writer.WriteLine("""
1306+
<RestoreUseStaticGraphEvaluation>false</RestoreUseStaticGraphEvaluation>
12921307
<Features>$(Features);FileBasedProgram</Features>
12931308
""");
12941309
}

test/dotnet.Tests/CommandTests/Run/RunFileTests.cs

Lines changed: 147 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,128 @@ public void DirectoryBuildProps()
878878
.And.HaveStdOut("Hello from TestName");
879879
}
880880

881+
/// <summary>
882+
/// Overriding default (implicit) properties of file-based apps via implicit build files.
883+
/// </summary>
884+
[Fact]
885+
public void DefaultProps_DirectoryBuildProps()
886+
{
887+
var testInstance = _testAssetsManager.CreateTestDirectory();
888+
File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
889+
Console.WriteLine("Hi");
890+
""");
891+
File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """
892+
<Project>
893+
<PropertyGroup>
894+
<ImplicitUsings>disable</ImplicitUsings>
895+
</PropertyGroup>
896+
</Project>
897+
""");
898+
899+
new DotnetCommand(Log, "run", "Program.cs")
900+
.WithWorkingDirectory(testInstance.Path)
901+
.Execute()
902+
.Should().Fail()
903+
// error CS0103: The name 'Console' does not exist in the current context
904+
.And.HaveStdOutContaining("error CS0103");
905+
906+
// Converting to a project should not change the behavior.
907+
908+
new DotnetCommand(Log, "project", "convert", "Program.cs")
909+
.WithWorkingDirectory(testInstance.Path)
910+
.Execute()
911+
.Should().Pass();
912+
913+
new DotnetCommand(Log, "run")
914+
.WithWorkingDirectory(Path.Join(testInstance.Path, "Program"))
915+
.Execute()
916+
.Should().Fail()
917+
// error CS0103: The name 'Console' does not exist in the current context
918+
.And.HaveStdOutContaining("error CS0103");
919+
}
920+
921+
/// <summary>
922+
/// Overriding default (implicit) properties of file-based apps from custom SDKs.
923+
/// </summary>
924+
[Fact]
925+
public void DefaultProps_CustomSdk()
926+
{
927+
var testInstance = _testAssetsManager.CreateTestDirectory();
928+
929+
var sdkDir = Path.Join(testInstance.Path, "MySdk");
930+
Directory.CreateDirectory(sdkDir);
931+
File.WriteAllText(Path.Join(sdkDir, "Sdk.props"), """
932+
<Project>
933+
<PropertyGroup>
934+
<ImplicitUsings>disable</ImplicitUsings>
935+
</PropertyGroup>
936+
</Project>
937+
""");
938+
File.WriteAllText(Path.Join(sdkDir, "Sdk.targets"), """
939+
<Project />
940+
""");
941+
File.WriteAllText(Path.Join(sdkDir, "MySdk.csproj"), $"""
942+
<Project Sdk="Microsoft.NET.Sdk">
943+
<PropertyGroup>
944+
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
945+
<PackageType>MSBuildSdk</PackageType>
946+
<IncludeBuildOutput>false</IncludeBuildOutput>
947+
</PropertyGroup>
948+
<ItemGroup>
949+
<None Include="Sdk.*" Pack="true" PackagePath="Sdk" />
950+
</ItemGroup>
951+
</Project>
952+
""");
953+
954+
new DotnetCommand(Log, "pack")
955+
.WithWorkingDirectory(sdkDir)
956+
.Execute()
957+
.Should().Pass();
958+
959+
var appDir = Path.Join(testInstance.Path, "app");
960+
Directory.CreateDirectory(appDir);
961+
File.WriteAllText(Path.Join(appDir, "NuGet.config"), $"""
962+
<configuration>
963+
<packageSources>
964+
<add key="local" value="{Path.Join(sdkDir, "bin", "Release")}" />
965+
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
966+
</packageSources>
967+
</configuration>
968+
""");
969+
File.WriteAllText(Path.Join(appDir, "Program.cs"), """
970+
#:sdk Microsoft.NET.Sdk
971+
#:sdk MySdk@1.0.0
972+
Console.WriteLine("Hi");
973+
""");
974+
975+
// Use custom package cache to avoid reuse of the custom SDK packed by previous test runs.
976+
var packagesDir = Path.Join(testInstance.Path, ".packages");
977+
978+
new DotnetCommand(Log, "run", "Program.cs")
979+
.WithEnvironmentVariable("NUGET_PACKAGES", packagesDir)
980+
.WithWorkingDirectory(appDir)
981+
.Execute()
982+
.Should().Fail()
983+
// error CS0103: The name 'Console' does not exist in the current context
984+
.And.HaveStdOutContaining("error CS0103");
985+
986+
// Converting to a project should not change the behavior.
987+
988+
new DotnetCommand(Log, "project", "convert", "Program.cs")
989+
.WithEnvironmentVariable("NUGET_PACKAGES", packagesDir)
990+
.WithWorkingDirectory(appDir)
991+
.Execute()
992+
.Should().Pass();
993+
994+
new DotnetCommand(Log, "run")
995+
.WithEnvironmentVariable("NUGET_PACKAGES", packagesDir)
996+
.WithWorkingDirectory(Path.Join(appDir, "Program"))
997+
.Execute()
998+
.Should().Fail()
999+
// error CS0103: The name 'Console' does not exist in the current context
1000+
.And.HaveStdOutContaining("error CS0103");
1001+
}
1002+
8811003
[Fact]
8821004
public void ComputeRunArguments_Success()
8831005
{
@@ -3441,6 +3563,14 @@ public void Api()
34413563
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
34423564
<PackageOutputPath>artifacts/$(MSBuildProjectName)</PackageOutputPath>
34433565
<FileBasedProgram>true</FileBasedProgram>
3566+
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
3567+
<DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder>
3568+
<OutputType>Exe</OutputType>
3569+
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
3570+
<ImplicitUsings>enable</ImplicitUsings>
3571+
<Nullable>enable</Nullable>
3572+
<PublishAot>true</PublishAot>
3573+
<PackAsTool>true</PackAsTool>
34443574
</PropertyGroup>
34453575
34463576
<ItemGroup>
@@ -3451,16 +3581,9 @@ public void Api()
34513581
<Import Project="Sdk.props" Sdk="Aspire.Hosting.Sdk" Version="9.1.0" />
34523582
34533583
<PropertyGroup>
3454-
<OutputType>Exe</OutputType>
3455-
<ImplicitUsings>enable</ImplicitUsings>
3456-
<Nullable>enable</Nullable>
3457-
<PublishAot>true</PublishAot>
3458-
<PackAsTool>true</PackAsTool>
3459-
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
3460-
<DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder>
3461-
<RestoreUseStaticGraphEvaluation>false</RestoreUseStaticGraphEvaluation>
34623584
<TargetFramework>net11.0</TargetFramework>
34633585
<LangVersion>preview</LangVersion>
3586+
<RestoreUseStaticGraphEvaluation>false</RestoreUseStaticGraphEvaluation>
34643587
<Features>$(Features);FileBasedProgram</Features>
34653588
</PropertyGroup>
34663589
@@ -3512,6 +3635,14 @@ public void Api_Diagnostic_01()
35123635
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
35133636
<PackageOutputPath>artifacts/$(MSBuildProjectName)</PackageOutputPath>
35143637
<FileBasedProgram>true</FileBasedProgram>
3638+
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
3639+
<DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder>
3640+
<OutputType>Exe</OutputType>
3641+
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
3642+
<ImplicitUsings>enable</ImplicitUsings>
3643+
<Nullable>enable</Nullable>
3644+
<PublishAot>true</PublishAot>
3645+
<PackAsTool>true</PackAsTool>
35153646
</PropertyGroup>
35163647
35173648
<ItemGroup>
@@ -3521,14 +3652,6 @@ public void Api_Diagnostic_01()
35213652
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
35223653
35233654
<PropertyGroup>
3524-
<OutputType>Exe</OutputType>
3525-
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
3526-
<ImplicitUsings>enable</ImplicitUsings>
3527-
<Nullable>enable</Nullable>
3528-
<PublishAot>true</PublishAot>
3529-
<PackAsTool>true</PackAsTool>
3530-
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
3531-
<DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder>
35323655
<RestoreUseStaticGraphEvaluation>false</RestoreUseStaticGraphEvaluation>
35333656
<Features>$(Features);FileBasedProgram</Features>
35343657
</PropertyGroup>
@@ -3580,6 +3703,14 @@ public void Api_Diagnostic_02()
35803703
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
35813704
<PackageOutputPath>artifacts/$(MSBuildProjectName)</PackageOutputPath>
35823705
<FileBasedProgram>true</FileBasedProgram>
3706+
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
3707+
<DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder>
3708+
<OutputType>Exe</OutputType>
3709+
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
3710+
<ImplicitUsings>enable</ImplicitUsings>
3711+
<Nullable>enable</Nullable>
3712+
<PublishAot>true</PublishAot>
3713+
<PackAsTool>true</PackAsTool>
35833714
</PropertyGroup>
35843715
35853716
<ItemGroup>
@@ -3589,14 +3720,6 @@ public void Api_Diagnostic_02()
35893720
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
35903721
35913722
<PropertyGroup>
3592-
<OutputType>Exe</OutputType>
3593-
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
3594-
<ImplicitUsings>enable</ImplicitUsings>
3595-
<Nullable>enable</Nullable>
3596-
<PublishAot>true</PublishAot>
3597-
<PackAsTool>true</PackAsTool>
3598-
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
3599-
<DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder>
36003723
<RestoreUseStaticGraphEvaluation>false</RestoreUseStaticGraphEvaluation>
36013724
<Features>$(Features);FileBasedProgram</Features>
36023725
</PropertyGroup>

0 commit comments

Comments
 (0)