Skip to content

Commit 52647af

Browse files
CopilotmitchdennyCopilot
authored
Add TFM tracking to InitContext for determining required AppHost framework (#11853)
* Initial plan * Add ExecutableProjectInfo class with TFM tracking and RequiredAppHostFramework property Co-authored-by: mitchdenny <[email protected]> * Fix test compilation errors by adding missing interface methods Co-authored-by: mitchdenny <[email protected]> * Support apphost creation with max TFM verison. * Fix build error. * Some files somehow missed when rebased? * Use SemVersion for TFM parsing and add IsSupportedTfm filter Co-authored-by: mitchdenny <[email protected]> * Update src/Aspire.Cli/Commands/InitCommand.cs Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: mitchdenny <[email protected]> Co-authored-by: Mitch Denny <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent a949010 commit 52647af

15 files changed

+159
-64
lines changed

src/Aspire.Cli/Commands/InitCommand.cs

Lines changed: 110 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ later as needed.
176176
var selectedProjects = await InteractionService.PromptForSelectionsAsync(
177177
"Select projects to add to the AppHost:",
178178
initContext.ExecutableProjects,
179-
project => Path.GetFileNameWithoutExtension(project.Name),
179+
project => Path.GetFileNameWithoutExtension(project.ProjectFile.Name),
180180
cancellationToken);
181181

182182
initContext.ExecutableProjectsToAddToAppHost = selectedProjects;
@@ -190,7 +190,7 @@ later as needed.
190190

191191
foreach (var project in initContext.ExecutableProjectsToAddToAppHost)
192192
{
193-
InteractionService.DisplayMessage("check_box_with_check", project.Name);
193+
InteractionService.DisplayMessage("check_box_with_check", project.ProjectFile.Name);
194194
}
195195

196196
var addServiceDefaultsMessage = """
@@ -229,11 +229,11 @@ ServiceDefaults project contains helper code to make it easier
229229
initContext.ProjectsToAddServiceDefaultsTo = await InteractionService.PromptForSelectionsAsync(
230230
"Select projects to add ServiceDefaults reference to:",
231231
initContext.ExecutableProjectsToAddToAppHost,
232-
project => Path.GetFileNameWithoutExtension(project.Name),
232+
project => Path.GetFileNameWithoutExtension(project.ProjectFile.Name),
233233
cancellationToken);
234234
break;
235235
case "none":
236-
initContext.ProjectsToAddServiceDefaultsTo = Array.Empty<FileInfo>();
236+
initContext.ProjectsToAddServiceDefaultsTo = Array.Empty<ExecutableProjectInfo>();
237237
break;
238238
}
239239
}
@@ -281,7 +281,7 @@ ServiceDefaults project contains helper code to make it easier
281281
"aspire",
282282
initContext.SolutionName,
283283
tempProjectDir,
284-
[], // No extra args needed for aspire template
284+
["--framework", initContext.RequiredAppHostFramework],
285285
new DotNetCliRunnerInvocationOptions(),
286286
cancellationToken);
287287
});
@@ -354,18 +354,18 @@ ServiceDefaults project contains helper code to make it easier
354354
foreach(var project in initContext.ExecutableProjectsToAddToAppHost)
355355
{
356356
var addRefResult = await InteractionService.ShowStatusAsync(
357-
$"Adding {project.Name} to AppHost...", async () =>
357+
$"Adding {project.ProjectFile.Name} to AppHost...", async () =>
358358
{
359359
return await _runner.AddProjectReferenceAsync(
360360
appHostProjectFile,
361-
project,
361+
project.ProjectFile,
362362
new DotNetCliRunnerInvocationOptions(),
363363
cancellationToken);
364364
});
365365

366366
if (addRefResult != 0)
367367
{
368-
InteractionService.DisplayError($"Failed to add reference to {Path.GetFileNameWithoutExtension(project.Name)}.");
368+
InteractionService.DisplayError($"Failed to add reference to {Path.GetFileNameWithoutExtension(project.ProjectFile.Name)}.");
369369
return addRefResult;
370370
}
371371
}
@@ -377,18 +377,18 @@ ServiceDefaults project contains helper code to make it easier
377377
foreach (var project in initContext.ProjectsToAddServiceDefaultsTo)
378378
{
379379
var addRefResult = await InteractionService.ShowStatusAsync(
380-
$"Adding ServiceDefaults reference to {project.Name}...", async () =>
380+
$"Adding ServiceDefaults reference to {project.ProjectFile.Name}...", async () =>
381381
{
382382
return await _runner.AddProjectReferenceAsync(
383-
project,
383+
project.ProjectFile,
384384
serviceDefaultsProjectFile,
385385
new DotNetCliRunnerInvocationOptions(),
386386
cancellationToken);
387387
});
388388

389389
if (addRefResult != 0)
390390
{
391-
InteractionService.DisplayError($"Failed to add ServiceDefaults reference to {Path.GetFileNameWithoutExtension(project.Name)}.");
391+
InteractionService.DisplayError($"Failed to add ServiceDefaults reference to {Path.GetFileNameWithoutExtension(project.ProjectFile.Name)}.");
392392
return addRefResult;
393393
}
394394
}
@@ -449,15 +449,15 @@ private async Task<int> CreateEmptyAppHostAsync(ParseResult parseResult, Cancell
449449

450450
private async Task EvaluateSolutionProjectsAsync(InitContext initContext, CancellationToken cancellationToken)
451451
{
452-
var executableProjects = new List<FileInfo>();
452+
var executableProjects = new List<ExecutableProjectInfo>();
453453

454454
foreach (var project in initContext.SolutionProjects)
455455
{
456-
// Get both IsAspireHost and OutputType properties in a single call
456+
// Get IsAspireHost, OutputType, and TargetFramework properties in a single call
457457
var (exitCode, jsonDoc) = await _runner.GetProjectItemsAndPropertiesAsync(
458458
project,
459459
[],
460-
["IsAspireHost", "OutputType"],
460+
["IsAspireHost", "OutputType", "TargetFramework"],
461461
new DotNetCliRunnerInvocationOptions(),
462462
cancellationToken);
463463

@@ -483,7 +483,22 @@ private async Task EvaluateSolutionProjectsAsync(InitContext initContext, Cancel
483483
var outputType = outputTypeElement.GetString();
484484
if (outputType == "Exe" || outputType == "WinExe")
485485
{
486-
executableProjects.Add(project);
486+
// Get the target framework
487+
var targetFramework = "net9.0"; // Default if not found
488+
if (properties.TryGetProperty("TargetFramework", out var targetFrameworkElement))
489+
{
490+
targetFramework = targetFrameworkElement.GetString() ?? "net9.0";
491+
}
492+
493+
// Only add projects with supported TFMs
494+
if (IsSupportedTfm(targetFramework))
495+
{
496+
executableProjects.Add(new ExecutableProjectInfo
497+
{
498+
ProjectFile = project,
499+
TargetFramework = targetFramework
500+
});
501+
}
487502
}
488503
}
489504
}
@@ -493,6 +508,22 @@ private async Task EvaluateSolutionProjectsAsync(InitContext initContext, Cancel
493508
initContext.ExecutableProjects = executableProjects;
494509
}
495510

511+
/// <summary>
512+
/// Determines if the specified target framework moniker is supported.
513+
/// </summary>
514+
/// <param name="tfm">The target framework moniker to check.</param>
515+
/// <returns>True if the TFM is supported; otherwise, false.</returns>
516+
private static bool IsSupportedTfm(string tfm)
517+
{
518+
return tfm switch
519+
{
520+
"net8.0" => true,
521+
"net9.0" => true,
522+
"net10.0" => true,
523+
_ => false
524+
};
525+
}
526+
496527
private async Task<(NuGetPackage Package, PackageChannel Channel)> GetProjectTemplatesVersionAsync(ParseResult parseResult, CancellationToken cancellationToken)
497528
{
498529
var channels = await _packagingService.GetChannelsAsync(cancellationToken);
@@ -550,6 +581,22 @@ the latest stable version choose ***{{latestStable.Package.Version}}***.
550581
}
551582
}
552583

584+
/// <summary>
585+
/// Represents information about an executable project including its file and target framework.
586+
/// </summary>
587+
internal sealed class ExecutableProjectInfo
588+
{
589+
/// <summary>
590+
/// Gets the project file.
591+
/// </summary>
592+
public required FileInfo ProjectFile { get; init; }
593+
594+
/// <summary>
595+
/// Gets the target framework moniker (e.g., "net9.0", "net10.0").
596+
/// </summary>
597+
public required string TargetFramework { get; init; }
598+
}
599+
553600
/// <summary>
554601
/// Context class for building up a model of the init operation before executing changes.
555602
/// </summary>
@@ -593,15 +640,60 @@ internal sealed class InitContext
593640
/// <summary>
594641
/// List of executable projects found in the solution (excluding the AppHost).
595642
/// </summary>
596-
public IReadOnlyList<FileInfo> ExecutableProjects { get; set; } = Array.Empty<FileInfo>();
643+
public IReadOnlyList<ExecutableProjectInfo> ExecutableProjects { get; set; } = Array.Empty<ExecutableProjectInfo>();
597644

598645
/// <summary>
599646
/// Executable projects selected by the user to add to the AppHost.
600647
/// </summary>
601-
public IReadOnlyList<FileInfo> ExecutableProjectsToAddToAppHost { get; set; } = Array.Empty<FileInfo>();
648+
public IReadOnlyList<ExecutableProjectInfo> ExecutableProjectsToAddToAppHost { get; set; } = Array.Empty<ExecutableProjectInfo>();
602649

603650
/// <summary>
604651
/// Projects selected by the user to add ServiceDefaults reference to.
605652
/// </summary>
606-
public IReadOnlyList<FileInfo> ProjectsToAddServiceDefaultsTo { get; set; } = Array.Empty<FileInfo>();
653+
public IReadOnlyList<ExecutableProjectInfo> ProjectsToAddServiceDefaultsTo { get; set; } = Array.Empty<ExecutableProjectInfo>();
654+
655+
/// <summary>
656+
/// Gets the required AppHost framework based on the highest TFM of all selected executable projects.
657+
/// </summary>
658+
public string RequiredAppHostFramework
659+
{
660+
get
661+
{
662+
if (ExecutableProjectsToAddToAppHost.Count == 0)
663+
{
664+
return "net9.0"; // Default framework if no projects selected
665+
}
666+
667+
// Parse and compare TFMs to find the highest one using SemVersion
668+
SemVersion? highestVersion = null;
669+
var highestTfm = "net9.0";
670+
671+
foreach (var project in ExecutableProjectsToAddToAppHost)
672+
{
673+
var tfm = project.TargetFramework;
674+
if (tfm.StartsWith("net", StringComparison.OrdinalIgnoreCase))
675+
{
676+
var versionString = tfm[3..];
677+
// Add patch version if not present for SemVersion parsing
678+
// TFMs are in format "8.0", "9.0", "10.0", need to make them "8.0.0", "9.0.0", "10.0.0"
679+
var dotCount = versionString.Count(c => c == '.');
680+
if (dotCount == 1)
681+
{
682+
versionString += ".0";
683+
}
684+
685+
if (SemVersion.TryParse(versionString, SemVersionStyles.Strict, out var version))
686+
{
687+
if (highestVersion is null || SemVersion.ComparePrecedence(version, highestVersion) > 0)
688+
{
689+
highestVersion = version;
690+
highestTfm = tfm;
691+
}
692+
}
693+
}
694+
}
695+
696+
return highestTfm;
697+
}
698+
}
607699
}

tests/Aspire.Cli.Tests/Commands/InitCommandTests.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,49 @@ namespace Aspire.Cli.Tests.Commands;
1313

1414
public class InitCommandTests(ITestOutputHelper outputHelper)
1515
{
16+
[Fact]
17+
public void InitContext_RequiredAppHostFramework_ReturnsHighestTfm()
18+
{
19+
// Arrange
20+
var initContext = new InitContext();
21+
22+
// Act & Assert - No projects selected returns default
23+
Assert.Equal("net9.0", initContext.RequiredAppHostFramework);
24+
25+
// Set up projects with different TFMs
26+
initContext.ExecutableProjectsToAddToAppHost = new List<ExecutableProjectInfo>
27+
{
28+
new() { ProjectFile = new FileInfo("/test/project1.csproj"), TargetFramework = "net8.0" },
29+
new() { ProjectFile = new FileInfo("/test/project2.csproj"), TargetFramework = "net9.0" },
30+
new() { ProjectFile = new FileInfo("/test/project3.csproj"), TargetFramework = "net10.0" }
31+
};
32+
33+
// Act
34+
var result = initContext.RequiredAppHostFramework;
35+
36+
// Assert
37+
Assert.Equal("net10.0", result);
38+
39+
// Test with only lower versions
40+
initContext.ExecutableProjectsToAddToAppHost = new List<ExecutableProjectInfo>
41+
{
42+
new() { ProjectFile = new FileInfo("/test/project1.csproj"), TargetFramework = "net8.0" },
43+
new() { ProjectFile = new FileInfo("/test/project2.csproj"), TargetFramework = "net9.0" }
44+
};
45+
46+
result = initContext.RequiredAppHostFramework;
47+
Assert.Equal("net9.0", result);
48+
49+
// Test with only net8.0
50+
initContext.ExecutableProjectsToAddToAppHost = new List<ExecutableProjectInfo>
51+
{
52+
new() { ProjectFile = new FileInfo("/test/project1.csproj"), TargetFramework = "net8.0" }
53+
};
54+
55+
result = initContext.RequiredAppHostFramework;
56+
Assert.Equal("net8.0", result);
57+
}
58+
1659
[Fact]
1760
public async Task InitCommand_WithSingleFileAppHost_DoesNotPromptForProjectNameOrOutputPath()
1861
{

tests/Aspire.Cli.Tests/Snapshots/Merge_ExtraPatternOnDailyFeedWhenOnPrFeedGetsConsolidatedWithOtherPatterns_ProducesExpectedXml.daily.verified.xml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
<add key="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" />
55
<add key="https://api.nuget.org/v3/index.json" value="https://api.nuget.org/v3/index.json" />
66
<add key="mycompany" value="http://mycompany.com/feed" />
7-
<add key="C:\Users\midenn\.aspire\hives\pr-11275" value="C:\Users\midenn\.aspire\hives\pr-11275" />
87
</packageSources>
98
<packageSourceMapping>
109
<packageSource key="https://api.nuget.org/v3/index.json">
@@ -14,9 +13,6 @@
1413
<package pattern="Microsoft.Extensions.HelperStuff*" />
1514
<package pattern="Aspire*" />
1615
</packageSource>
17-
<packageSource key="C:\Users\midenn\.aspire\hives\pr-11275">
18-
<package pattern="Microsoft.Extensions.ServiceDiscovery*" />
19-
</packageSource>
2016
<packageSource key="mycompany">
2117
<package pattern="*" />
2218
</packageSource>

tests/Aspire.Cli.Tests/Snapshots/Merge_ExtraPatternOnDailyFeedWhenOnPrFeedGetsConsolidatedWithOtherPatterns_ProducesExpectedXml.pr-1234.verified.xml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
<add key="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" />
55
<add key="https://api.nuget.org/v3/index.json" value="https://api.nuget.org/v3/index.json" />
66
<add key="mycompany" value="http://mycompany.com/feed" />
7-
<add key="C:\Users\midenn\.aspire\hives\pr-11275" value="C:\Users\midenn\.aspire\hives\pr-11275" />
87
<add key="{PR_HIVE}" value="{PR_HIVE}" />
98
</packageSources>
109
<packageSourceMapping>
@@ -14,9 +13,6 @@
1413
<packageSource key="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json">
1514
<package pattern="Microsoft.Extensions.HelperStuff*" />
1615
</packageSource>
17-
<packageSource key="C:\Users\midenn\.aspire\hives\pr-11275">
18-
<package pattern="Microsoft.Extensions.ServiceDiscovery*" />
19-
</packageSource>
2016
<packageSource key="{PR_HIVE}">
2117
<package pattern="Aspire*" />
2218
<package pattern="*" />

tests/Aspire.Cli.Tests/Snapshots/Merge_ExtraPatternOnDailyFeedWhenOnPrFeedGetsConsolidatedWithOtherPatterns_ProducesExpectedXml.stable.verified.xml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
<add key="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" />
55
<add key="https://api.nuget.org/v3/index.json" value="https://api.nuget.org/v3/index.json" />
66
<add key="mycompany" value="http://mycompany.com/feed" />
7-
<add key="C:\Users\midenn\.aspire\hives\pr-11275" value="C:\Users\midenn\.aspire\hives\pr-11275" />
87
</packageSources>
98
<packageSourceMapping>
109
<packageSource key="https://api.nuget.org/v3/index.json">
@@ -13,9 +12,6 @@
1312
<packageSource key="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json">
1413
<package pattern="Microsoft.Extensions.HelperStuff*" />
1514
</packageSource>
16-
<packageSource key="C:\Users\midenn\.aspire\hives\pr-11275">
17-
<package pattern="Microsoft.Extensions.ServiceDiscovery*" />
18-
</packageSource>
1915
<packageSource key="mycompany">
2016
<package pattern="*" />
2117
</packageSource>

tests/Aspire.Cli.Tests/Snapshots/Merge_WithBrokenSdkState_ProducesExpectedXml.daily.verified.xml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,11 @@
33
<clear />
44
<add key="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json" />
55
<add key="https://api.nuget.org/v3/index.json" value="https://api.nuget.org/v3/index.json" />
6-
<add key="C:\Users\davifowl\.aspire\hives\pr-11227" value="C:\Users\davifowl\.aspire\hives\pr-11227" />
76
</packageSources>
87
<packageSourceMapping>
98
<packageSource key="https://api.nuget.org/v3/index.json">
109
<package pattern="*" />
1110
</packageSource>
12-
<packageSource key="C:\Users\davifowl\.aspire\hives\pr-11227">
13-
<package pattern="Microsoft.Extensions.ServiceDiscovery*" />
14-
</packageSource>
1511
<packageSource key="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json">
1612
<package pattern="Aspire*" />
1713
</packageSource>

tests/Aspire.Cli.Tests/Snapshots/Merge_WithBrokenSdkState_ProducesExpectedXml.pr-1234.verified.xml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,12 @@
22
<packageSources>
33
<clear />
44
<add key="https://api.nuget.org/v3/index.json" value="https://api.nuget.org/v3/index.json" />
5-
<add key="C:\Users\davifowl\.aspire\hives\pr-11227" value="C:\Users\davifowl\.aspire\hives\pr-11227" />
65
<add key="{PR_HIVE}" value="{PR_HIVE}" />
76
</packageSources>
87
<packageSourceMapping>
98
<packageSource key="https://api.nuget.org/v3/index.json">
109
<package pattern="*" />
1110
</packageSource>
12-
<packageSource key="C:\Users\davifowl\.aspire\hives\pr-11227">
13-
<package pattern="Microsoft.Extensions.ServiceDiscovery*" />
14-
</packageSource>
1511
<packageSource key="{PR_HIVE}">
1612
<package pattern="Aspire*" />
1713
<package pattern="*" />

tests/Aspire.Cli.Tests/Snapshots/Merge_WithBrokenSdkState_ProducesExpectedXml.stable.verified.xml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,10 @@
22
<packageSources>
33
<clear />
44
<add key="https://api.nuget.org/v3/index.json" value="https://api.nuget.org/v3/index.json" />
5-
<add key="C:\Users\davifowl\.aspire\hives\pr-11227" value="C:\Users\davifowl\.aspire\hives\pr-11227" />
65
</packageSources>
76
<packageSourceMapping>
87
<packageSource key="https://api.nuget.org/v3/index.json">
98
<package pattern="*" />
109
</packageSource>
11-
<packageSource key="C:\Users\davifowl\.aspire\hives\pr-11227">
12-
<package pattern="Microsoft.Extensions.ServiceDiscovery*" />
13-
</packageSource>
1410
</packageSourceMapping>
1511
</configuration>

tests/Aspire.Cli.Tests/Snapshots/Merge_WithDailyFeedWithExtraMappingsIsPreserved_ProducesExpectedXml.daily.verified.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
<packageSourceMapping>
99
<packageSource key="dotnet9">
1010
<package pattern="Aspire*" />
11-
<package pattern="Microsoft.Extensions.ServiceDiscovery*" />
1211
<package pattern="Microsoft.Extensions.SpecialPackage*" />
1312
</packageSource>
1413
<packageSource key="NuGet">

0 commit comments

Comments
 (0)