@@ -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}
0 commit comments