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
15 changes: 14 additions & 1 deletion src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -184,6 +185,18 @@ ImmutableArray<CSharpDirective> UpdateDirectives(ImmutableArray<CSharpDirective>

return result.DrainToImmutable();
}

IEnumerable<string> 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)
Expand Down
61 changes: 38 additions & 23 deletions src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ internal sealed class VirtualProjectBuildingCommand : CommandBase
/// <remarks>
/// Kept in sync with the default <c>dotnet new console</c> project file (enforced by <c>DotnetProjectAddTests.SameAsTemplate</c>).
/// </remarks>
private static readonly FrozenDictionary<string, string> s_defaultProperties = FrozenDictionary.Create<string, string>(StringComparer.OrdinalIgnoreCase,
public static readonly FrozenDictionary<string, string> DefaultProperties = FrozenDictionary.Create<string, string>(StringComparer.OrdinalIgnoreCase,
[
new("OutputType", "Exe"),
new("TargetFramework", $"net{TargetFrameworkVersion}"),
Expand Down Expand Up @@ -1140,8 +1140,12 @@ public static void WriteProjectFile(
string? targetFilePath = null,
string? artifactsPath = null,
bool includeRuntimeConfigInformation = true,
string? userSecretsId = null)
string? userSecretsId = null,
IEnumerable<string>? excludeDefaultProperties = null)
{
Debug.Assert(userSecretsId == null || !isVirtualProject);
Debug.Assert(excludeDefaultProperties == null || !isVirtualProject);

int processedDirectives = 0;

var sdkDirectives = directives.OfType<CSharpDirective.Sdk>();
Expand Down Expand Up @@ -1180,6 +1184,20 @@ public static void WriteProjectFile(
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
<PackageOutputPath>artifacts/$(MSBuildProjectName)</PackageOutputPath>
<FileBasedProgram>true</FileBasedProgram>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder>
""");

// 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($"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: IMO we shouldn't be writing raw XML project files like this. If the MSbuild in-memory object APIs don't make it convenient to create the structures we need I'd love to see issues raised calling out the pain points. Raw XML ties us to the format rather than the logical values we're trying to control.

Copy link
Member Author

@jjonescz jjonescz Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was discussed previously: #47702 (comment)
Opened dotnet/msbuild#12553.

<{name}>{EscapeValue(value)}</{name}>
""");
}

writer.WriteLine($"""
</PropertyGroup>

<ItemGroup>
Expand Down Expand Up @@ -1246,34 +1264,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)}</{name}>
""");
}
}

if (userSecretsId != null && !customPropertyNames.Contains("UserSecretsId"))
{
writer.WriteLine($"""
<{name}>{EscapeValue(value)}</{name}>
<UserSecretsId>{EscapeValue(userSecretsId)}</UserSecretsId>
""");
}
}

if (userSecretsId != null && !customPropertyNames.Contains("UserSecretsId"))
{
writer.WriteLine($"""
<UserSecretsId>{EscapeValue(userSecretsId)}</UserSecretsId>
""");
}

// Write virtual-only properties.
if (isVirtualProject)
{
writer.WriteLine("""
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder>
<RestoreUseStaticGraphEvaluation>false</RestoreUseStaticGraphEvaluation>
""");
}

// Write custom properties.
foreach (var property in propertyDirectives)
{
Expand All @@ -1288,6 +1302,7 @@ public static void WriteProjectFile(
if (isVirtualProject)
{
writer.WriteLine("""
<RestoreUseStaticGraphEvaluation>false</RestoreUseStaticGraphEvaluation>
<Features>$(Features);FileBasedProgram</Features>
""");
}
Expand Down
171 changes: 147 additions & 24 deletions test/dotnet.Tests/CommandTests/Run/RunFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,128 @@ public void DirectoryBuildProps()
.And.HaveStdOut("Hello from TestName");
}

/// <summary>
/// Overriding default (implicit) properties of file-based apps via implicit build files.
/// </summary>
[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"), """
<Project>
<PropertyGroup>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
</Project>
""");

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

/// <summary>
/// Overriding default (implicit) properties of file-based apps from custom SDKs.
/// </summary>
[Fact]
public void DefaultProps_CustomSdk()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DamianEdwards is the behavior captured by this test conceptually what you want to have working for aspire sdk?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so, yes. IIUC it's demonstrating a file-based app referencing a custom SDK, and that SDK disables implicit usings, which is something explicitly defaulted to on for file-based apps. We should then be able to achieve the same thing for AOT publishing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, if your SDK wants to customize some properties in its Sdk.props like this, that should work.

{
var testInstance = _testAssetsManager.CreateTestDirectory();

var sdkDir = Path.Join(testInstance.Path, "MySdk");
Directory.CreateDirectory(sdkDir);
File.WriteAllText(Path.Join(sdkDir, "Sdk.props"), """
<Project>
<PropertyGroup>
<ImplicitUsings>disable</ImplicitUsings>
</PropertyGroup>
</Project>
""");
File.WriteAllText(Path.Join(sdkDir, "Sdk.targets"), """
<Project />
""");
File.WriteAllText(Path.Join(sdkDir, "MySdk.csproj"), $"""
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
<PackageType>MSBuildSdk</PackageType>
<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>
<ItemGroup>
<None Include="Sdk.*" Pack="true" PackagePath="Sdk" />
</ItemGroup>
</Project>
""");

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"), $"""
<configuration>
<packageSources>
<add key="local" value="{Path.Join(sdkDir, "bin", "Release")}" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>
""");
File.WriteAllText(Path.Join(appDir, "Program.cs"), """
#:sdk Microsoft.NET.Sdk
#:sdk [email protected]
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()
{
Expand Down Expand Up @@ -3360,6 +3482,14 @@ public void Api()
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
<PackageOutputPath>artifacts/$(MSBuildProjectName)</PackageOutputPath>
<FileBasedProgram>true</FileBasedProgram>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder>
<OutputType>Exe</OutputType>
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<PackAsTool>true</PackAsTool>
</PropertyGroup>

<ItemGroup>
Expand All @@ -3370,16 +3500,9 @@ public void Api()
<Import Project="Sdk.props" Sdk="Aspire.Hosting.Sdk" Version="9.1.0" />

<PropertyGroup>
<OutputType>Exe</OutputType>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<PackAsTool>true</PackAsTool>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder>
<RestoreUseStaticGraphEvaluation>false</RestoreUseStaticGraphEvaluation>
<TargetFramework>net11.0</TargetFramework>
<LangVersion>preview</LangVersion>
<RestoreUseStaticGraphEvaluation>false</RestoreUseStaticGraphEvaluation>
<Features>$(Features);FileBasedProgram</Features>
</PropertyGroup>

Expand Down Expand Up @@ -3431,6 +3554,14 @@ public void Api_Diagnostic_01()
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
<PackageOutputPath>artifacts/$(MSBuildProjectName)</PackageOutputPath>
<FileBasedProgram>true</FileBasedProgram>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder>
<OutputType>Exe</OutputType>
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<PackAsTool>true</PackAsTool>
</PropertyGroup>

<ItemGroup>
Expand All @@ -3440,14 +3571,6 @@ public void Api_Diagnostic_01()
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<PackAsTool>true</PackAsTool>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder>
<RestoreUseStaticGraphEvaluation>false</RestoreUseStaticGraphEvaluation>
<Features>$(Features);FileBasedProgram</Features>
</PropertyGroup>
Expand Down Expand Up @@ -3499,6 +3622,14 @@ public void Api_Diagnostic_02()
<PublishDir>artifacts/$(MSBuildProjectName)</PublishDir>
<PackageOutputPath>artifacts/$(MSBuildProjectName)</PackageOutputPath>
<FileBasedProgram>true</FileBasedProgram>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder>
<OutputType>Exe</OutputType>
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<PackAsTool>true</PackAsTool>
</PropertyGroup>

<ItemGroup>
Expand All @@ -3508,14 +3639,6 @@ public void Api_Diagnostic_02()
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<PublishAot>true</PublishAot>
<PackAsTool>true</PackAsTool>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<DisableDefaultItemsInProjectFolder>true</DisableDefaultItemsInProjectFolder>
<RestoreUseStaticGraphEvaluation>false</RestoreUseStaticGraphEvaluation>
<Features>$(Features);FileBasedProgram</Features>
</PropertyGroup>
Expand Down
Loading