Skip to content

Refactor aspire new layering for polyglot templates#14677

Merged
davidfowl merged 8 commits intorelease/13.2from
davidfowl/fix-polyglot-new
Feb 28, 2026
Merged

Refactor aspire new layering for polyglot templates#14677
davidfowl merged 8 commits intorelease/13.2from
davidfowl/fix-polyglot-new

Conversation

@davidfowl
Copy link
Member

@davidfowl davidfowl commented Feb 25, 2026

Description

This PR reworks aspire new into a template-first model with two execution paths and consistent language-aware selection behavior.

Template architecture

  • DotNet-based templates (TemplateRuntime.DotNet): executed through dotnet new flows.
  • CLI-based templates (TemplateRuntime.Cli): CLI templates are embedded resources copied to disk (with token replacement) by CLI callbacks.
  • NewCommand now resolves template + language intent first, then dispatches to the template runtime, instead of hardcoding behavior in command branches.

Template discovery and filtering

  • Template discovery APIs were made async so each factory can self-filter based on runtime availability.
  • DotNetTemplateFactory now owns .NET SDK availability filtering; if SDK is unavailable, .NET templates are not offered.
  • new and init surfaces are separated cleanly in factory output (e.g., singlefile remains on init surface).

Language capability model

  • Templates can now declare supported AppHost languages in metadata (SelectableAppHostLanguages).
  • A template can also declare fixed language support (SupportsLanguage) for filtering when --language is provided.
  • aspire-empty declares selectable AppHost languages (C# and TypeScript), while language-specific templates stay fixed.

Prompting and selection flow

  • Template selection happens first (explicit template arg or template picker).
  • Language prompting happens after template selection, and only for templates that declare selectable languages.
  • Language resolution order is:
    1. explicit --language
    2. saved local config (language)
    3. interactive language prompt
  • The chosen language is persisted to local config.
  • --language on new is recursive so both forms work with template commands.

CLI template implementation updates

  • Introduced a mechanical embedded-resource tree copy helper for CLI templates.
  • Empty AppHost C# generation is now CLI-driven via embedded empty-apphost resources.
  • TypeScript starter generation was switched to embedded-tree copy with token replacement.
  • Added debug logging in CLI template generation paths to diagnose resource resolution/copy issues.

Embedded resource reliability

  • Added <WithCulture>false</WithCulture> for embedded template resources to prevent *.run.json from being treated as culture resources.
  • Verified expected embedded names are available (including empty-apphost.apphost.run.json and ts-starter.apphost.run.json).

Validation

  • dotnet test tests/Aspire.Cli.Tests/Aspire.Cli.Tests.csproj -- --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"
  • Targeted regressions for NewCommand/DotNetTemplateFactory and language/subcommand parsing paths.

Fixes #14676

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No
  • Does the change require an update in our Aspire docs?

@github-actions
Copy link
Contributor

github-actions bot commented Feb 25, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 14677

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 14677"

@davidfowl davidfowl force-pushed the davidfowl/fix-polyglot-new branch from 93dc534 to 24df40f Compare February 25, 2026 08:45
@github-actions
Copy link
Contributor

github-actions bot commented Feb 25, 2026

🎬 CLI E2E Test Recordings

The following terminal recordings are available for commit 5beeac5:

Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View Recording
AddPackageWhileAppHostRunningDetached ▶️ View Recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View Recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View Recording
AgentInitCommand_WithMalformedMcpJson_ShowsErrorAndExitsNonZero ▶️ View Recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View Recording
Banner_DisplayedOnFirstRun ▶️ View Recording
Banner_DisplayedWithExplicitFlag ▶️ View Recording
CreateAndDeployToDockerCompose ▶️ View Recording
CreateAndDeployToDockerComposeInteractive ▶️ View Recording
CreateAndPublishToKubernetes ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateEmptyAppHostProject ▶️ View Recording
CreateStartAndStopAspireProject ▶️ View Recording
CreateStartWaitAndStopAspireProject ▶️ View Recording
CreateTypeScriptAppHostWithViteApp ▶️ View Recording
DescribeCommandResolvesReplicaNames ▶️ View Recording
DescribeCommandShowsRunningResources ▶️ View Recording
DetachFormatJsonProducesValidJson ▶️ View Recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View Recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View Recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ❌ Upload failed
LogsCommandShowsResourceLogs ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View Recording
SecretCrudOnDotNetAppHost ▶️ View Recording
SecretCrudOnTypeScriptAppHost ❌ Upload failed
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View Recording
StopAllAppHostsFromAppHostDirectory ▶️ View Recording
StopAllAppHostsFromUnrelatedDirectory ▶️ View Recording
StopNonInteractiveMultipleAppHostsShowsError ▶️ View Recording
StopNonInteractiveSingleAppHost ▶️ View Recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View Recording

📹 Recordings uploaded automatically from CI run #22523674347

@davidfowl davidfowl force-pushed the davidfowl/fix-polyglot-new branch 2 times, most recently from 102143d to 696aca4 Compare February 27, 2026 02:52
@davidfowl davidfowl marked this pull request as ready for review February 27, 2026 05:39
Copilot AI review requested due to automatic review settings February 27, 2026 05:39
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the aspire new command to separate .NET and polyglot template creation through cleaner abstractions. Instead of branching at the command level, templates now declare their runtime model (DotNet/Polyglot) and route through appropriate factory implementations.

Changes:

  • Introduced TemplateRuntime enum and ITemplateVersionPrompter interface to separate template-specific concerns from command-level logic
  • Added PolyglotTemplateFactory with support for generic polyglot AppHost creation and a TypeScript starter template (Node.js + React)
  • Refactored NewCommand to resolve language once and route to the appropriate template runtime
  • Moved .NET SDK installation gating from NewCommand into DotNetTemplateFactory for better separation of concerns

Reviewed changes

Copilot reviewed 24 out of 24 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
src/Aspire.Cli/Templating/TemplateRuntime.cs New enum defining DotNet and Polyglot runtime models
src/Aspire.Cli/Templating/PolyglotTemplateFactory.cs New factory for polyglot templates with embedded TypeScript starter template
src/Aspire.Cli/Templating/ITemplate.cs Added Runtime and IncludeAsSubcommand properties for template routing
src/Aspire.Cli/Templating/ITemplateProvider.cs Added GetInitTemplates() method for init-specific templates
src/Aspire.Cli/Templating/TemplateProvider.cs Implemented GetInitTemplates() to delegate to factories
src/Aspire.Cli/Templating/TemplateInputs.cs Added Language property for language selection
src/Aspire.Cli/Templating/CallbackTemplate.cs Updated constructor to accept runtime and includeAsSubcommand parameters
src/Aspire.Cli/Templating/DotNetTemplateFactory.cs Moved SDK installation check into template application; uses ITemplateVersionPrompter
src/Aspire.Cli/Commands/NewCommand.cs Refactored to resolve language first, then route to appropriate template; split ITemplateVersionPrompter from INewCommandPrompter
src/Aspire.Cli/Commands/InitCommand.cs Updated to use ITemplateVersionPrompter and ITemplateProvider
src/Aspire.Cli/Program.cs Registered PolyglotTemplateFactory and ITemplateVersionPrompter in DI container
src/Aspire.Cli/Aspire.Cli.csproj Added embedded resources for TypeScript starter template files
src/Aspire.Cli/Templating/Templates/ts-starter/* TypeScript starter template files (apphost, package.json, tsconfig, API, settings)
tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs Updated test service registration for new interfaces
tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs Updated tests for new DotNetTemplateFactory constructor parameters
tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs Added tests for polyglot template selection and language routing
localhive.ps1 Improved CLI installation robustness with backup/restore logic and channel configuration

}

// NOTE: I am using Single(...) here because if we get to this point and we are not running the 'aspire new' without a template
// specified then we should have errored out with the help text. If we get there then someting is really wrong and we should
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

Typo in comment: "someting" should be "something".

Suggested change
// specified then we should have errored out with the help text. If we get there then someting is really wrong and we should
// specified then we should have errored out with the help text. If we get there then something is really wrong and we should

Copilot uses AI. Check for mistakes.
Copy link
Member

@mitchdenny mitchdenny left a comment

Choose a reason for hiding this comment

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

Overall this is a clean refactoring that properly separates .NET and polyglot template concerns behind the ITemplate/ITemplateFactory/ITemplateProvider abstractions. A few issues noted inline.

var outputTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
var errorTask = process.StandardError.ReadToEndAsync(cancellationToken);

await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

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

If the cancellation token fires during WaitForExitAsync, an OperationCanceledException propagates but the spawned process (npm/vite) continues running as an orphan. The using disposal on Process does not terminate the OS process.

Consider wrapping this in a try/catch to terminate the process tree on cancellation via process.TERMINATE(entireProcessTree: true).


private async Task<ITemplate> GetProjectTemplateAsync(ParseResult parseResult, string selectedLanguageId, CancellationToken cancellationToken)
{
if (!selectedLanguageId.Equals(KnownLanguageId.CSharp, StringComparison.OrdinalIgnoreCase))
Copy link
Member

Choose a reason for hiding this comment

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

Should the language option type be an enum? Commandline help would then automatically include available values to the user.

Options.Add(_languageOption);

_templates = templateProvider.GetTemplates();
_templates = templateProvider.GetTemplatesAsync(CancellationToken.None).GetAwaiter().GetResult().ToArray();
Copy link
Member

Choose a reason for hiding this comment

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

Blocking inside the constructor delays all of CLI startup.

Copy link
Member Author

Choose a reason for hiding this comment

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

This one is hard to fix.

return (true, selectedLanguageId);
}

private Task<ITemplate[]> GetTemplatesForPromptAsync(ParseResult parseResult, CancellationToken cancellationToken)
Copy link
Member

Choose a reason for hiding this comment

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

Doesn't need to be async

private static bool ShouldResolveCliTemplateVersion(ITemplate template)
{
return template.Runtime is TemplateRuntime.Cli &&
!template.Name.Equals("aspire-empty", StringComparison.OrdinalIgnoreCase);
Copy link
Member

Choose a reason for hiding this comment

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

"aspire-empty" should be a const somewhere. Also replace other places in CLI that hardcodes the string

- Add CliTemplateFactory with TypeScript starter and Empty AppHost templates
- Add embedded template resources with token replacement (ports, hostName, project name)
- Refactor NewCommand to route to .NET or CLI template runtime
- Restore templates as subcommands for proper option scoping
- Move .NET SDK gating into DotNetTemplateFactory
- Split template version prompting into ITemplateVersionPrompter
- Add localhive.ps1 improvements (backup/rename, locked DLLs, channel config)
- Use workspace directory name as default project name

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@davidfowl davidfowl force-pushed the davidfowl/fix-polyglot-new branch from 360f0b1 to 47828b2 Compare February 28, 2026 00:37
Copy link
Member

@mitchdenny mitchdenny left a comment

Choose a reason for hiding this comment

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

I think this is a good change overall. Obviously the tests need to pass :) And agree with some of the nits that James has mentioned but assuming those get addressed good to merge. The Git template spike I am doing will be made better by this PR.

};
Options.Add(_languageOption);
}
Description = "The programming language for the AppHost.",
Copy link
Member

Choose a reason for hiding this comment

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

UI text in resource files

davidfowl and others added 4 commits February 27, 2026 21:58
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Make polyglot behavior default-on in CLI commands and tests, and remove obsolete config/schema/workflow/docs references.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update empty apphost and secret E2E flows to account for template language prompt interactions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Prevent CliTemplateFactory from prompting for localhost TLD when interactive input is unavailable, while preserving explicit option behavior and interactive prompts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
davidfowl and others added 3 commits February 28, 2026 06:48
Introduce TemplateNuGetConfigService and use it from DotNetTemplateFactory and CliTemplateFactory (empty C# apphost path). Also resolve CLI template version for all CLI templates, including aspire-empty, and update tests to cover package version resolution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Embed a full React frontend in the ts-starter template, update API payload/health endpoint, and remove runtime vite scaffolding. Also make template resource extraction binary-safe by stream-copying binary assets so files like Aspire.png are not corrupted.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove ASPIRE_SHOW_DASHBOARD_RESOURCES from ts-starter apphost.run.json profiles to match current template defaults.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@davidfowl davidfowl merged commit e1de0eb into release/13.2 Feb 28, 2026
345 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants