Skip to content

Fix #16725: aspire run honours dev.localhost URLs from aspire.config.json#16828

Merged
mitchdenny merged 1 commit into
mainfrom
fix/16725-aspire-run-config-profiles
May 7, 2026
Merged

Fix #16725: aspire run honours dev.localhost URLs from aspire.config.json#16828
mitchdenny merged 1 commit into
mainfrom
fix/16725-aspire-run-config-profiles

Conversation

@mitchdenny
Copy link
Copy Markdown
Member

Description

Fix #16725: aspire run ignored *.dev.localhost URLs configured by aspire.config.json. After aspire new aspire-empty --localhost-tld --name test1, aspire run was bringing the dashboard up at https://localhost:17193/... instead of https://test1.dev.localhost:17159/....

There were two independent gaps causing this:

  1. Writer (template) gap. The empty-apphost C# template emits aspire.config.json but never emitted apphost.run.json. With no launch profile on disk, dotnet run apphost.cs had nothing to read, and the CLI fell through to the hardcoded localhost:17193 defaults in DotNetAppHostProject.ApplyDefaultSingleFileEndpoints. (PR Fix #15986: emit apphost.run.json from aspire init single-file skeleton #16812 closed the analogous gap for aspire init, but aspire new goes through CliTemplateFactory and was untouched.)
  2. Reader gap. DotNetAppHostProject.ConfigureSingleFile{Run,Publish}Environment only ever read apphost.run.json and never looked at the profiles section of aspire.config.json — even though GuestAppHostProject.ReadProfileFromAspireConfig already implements exactly this fallback for TS/Python/Go.

This PR fixes both, defense-in-depth:

(b) Template — emit apphost.run.json alongside aspire.config.json

Added src/Aspire.Cli/Templating/Templates/empty-apphost/apphost.run.json using the same {{hostName}}/{{httpsPort}}/{{httpPort}}/{{otlpHttpsPort}}/{{otlpHttpPort}}/{{resourceHttpsPort}}/{{resourceHttpPort}} tokens that aspire.config.json already uses. CopyTemplateTreeToDiskAsync performs the substitution uniformly so the two files cannot drift in URLs.

(c) Reader — fall back to aspire.config.json profiles when apphost.run.json is missing

DotNetAppHostProject now mirrors GuestAppHostProject's precedence:

  1. apphost.run.json (existing)
  2. aspire.config.json profiles (new)
  3. Hardcoded localhost:17193 defaults (existing)

The new TryApplyAspireConfigProfile helper:

  • Loads aspire.config.json from the AppHost's directory only (no upward walk).
  • Validates appHost.path resolves to the AppHost being launched (or is null) — protects multi-AppHost layouts.
  • Prefers the https profile, falls back to the first one.
  • Only applies if applicationUrl is non-empty.
  • Catches JsonException and falls through silently (matches existing apphost.run.json behavior on malformed JSON).

For aspire run, env vars are copied verbatim. For aspire publish, env-name vars (DOTNET_ENVIRONMENT/ASPNETCORE_ENVIRONMENT/ASPIRE_ENVIRONMENT) are filtered out so ApplyEffectiveEnvironment(Production) still wins. ApplyEffectiveEnvironment is always called last so explicit --environment Foo arguments still take precedence over what's in the profile.

Verification

End-to-end with a locally-built native AOT CLI on a fresh aspire new aspire-empty --name test1 --localhost-tld --suppress-agent-init --non-interactive project:

   Dashboard:  https://test1.dev.localhost:17159/login?t=...

Bonus: deleted apphost.run.json after generation and re-ran aspire run — fix C alone (reader fallback to aspire.config.json) still produces the correct dev.localhost URL.

Tests

8 new tests in DotNetAppHostProjectTests:

  • ConfigureSingleFileRunEnvironment_AppliesProfileFromAspireConfigJson
  • ConfigureSingleFilePublishEnvironment_AppliesProfileFromAspireConfigJson
  • ConfigureSingleFilePublishEnvironment_AppHostRunJsonWinsOverAspireConfigJson
  • ConfigureSingleFileRunEnvironment_FallsBackToDefaultsWhenAspireConfigJsonHasNoProfiles
  • ConfigureSingleFileRunEnvironment_FallsBackToDefaultsWhenProfileLacksApplicationUrl
  • ConfigureSingleFileRunEnvironment_SkipsAspireConfigWhenAppHostPathMismatches
  • ConfigureSingleFileRunEnvironment_FallsBackToDefaultsWhenAspireConfigJsonIsMalformed
  • ConfigureSingleFileRunEnvironment_EnvironmentArgumentOverridesProfileDotNetEnvironment

2 new tests in NewCommandTests:

  • NewCommandWithCSharpEmptyTemplateAndPlainLocalhostEmitsAppHostRunJsonMatchingAspireConfigJson
  • NewCommandWithCSharpEmptyTemplateAndLocalhostTldEmitsAppHostRunJsonWithDevLocalhostUrls

Full Aspire.Cli.Tests suite: 2564/2575 passed, 0 failures (11 OS-conditional skips).

Follow-ups (intentionally out of scope)

  • InitCommand.DropAppHostRunJson still constructs the JSON literal in C# rather than sharing the template. Both code paths produce byte-identical shapes today, but a future cleanup could route aspire init through the same template renderer to eliminate the drift surface entirely.

Fixes #16725

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
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
    • No
  • Does the change require an update in our Aspire docs?
    • Yes
    • No

…json

Two-sided gap meant `aspire new aspire-empty --localhost-tld` produced an
`aspire.config.json` with `*.dev.localhost` URLs but `aspire run` showed
the dashboard at hardcoded `localhost:17193`:

- Writer side: the empty-apphost C# template never emitted `apphost.run.json`,
  so dotnet's file-based runner had no launch profile and fell through to CLI
  defaults.
- Reader side: `DotNetAppHostProject` only ever read `apphost.run.json` and
  never looked at the `profiles` section of `aspire.config.json` -- even
  though `GuestAppHostProject` already does this for TS/Python/Go.

Defense in depth:

(b) Add `apphost.run.json` to the empty-apphost template, using the same
    `{{hostName}}`/`{{httpsPort}}`/etc. tokens as `aspire.config.json` so
    they cannot drift. `CopyTemplateTreeToDiskAsync` picks it up automatically.

(c) When `apphost.run.json` is missing, `ConfigureSingleFileRunEnvironment`
    and `ConfigureSingleFilePublishEnvironment` now fall back to the
    `profiles` section of `aspire.config.json` before applying the hardcoded
    defaults. Profile selection prefers `https`, validates `appHost.path`
    against the actual file, only applies if `applicationUrl` is non-empty,
    and silently falls back on `JsonException`. Run path copies env vars
    verbatim; publish path strips `DOTNET_ENVIRONMENT`/`ASPNETCORE_ENVIRONMENT`/
    `ASPIRE_ENVIRONMENT` so `ApplyEffectiveEnvironment(Production)` wins.

Tests: 8 new `DotNetAppHostProjectTests` cover happy path (run + publish),
`apphost.run.json` precedence, missing profiles, missing applicationUrl,
`appHost.path` mismatch, malformed JSON, and `--environment` arg precedence.
2 new `NewCommandTests` assert the template emits `apphost.run.json` whose
`https.applicationUrl` matches `aspire.config.json` for both `--localhost-tld`
and plain-localhost cases.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 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/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 16828

Or

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

🎬 CLI E2E Test Recordings — 77 recordings uploaded (commit 23133af)

View all recordings
Status Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View Recording
AddPackageWhileAppHostRunningDetached ▶️ View Recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View Recording
AgentInitCommand_DefaultSelection_InstallsSkillOnly ▶️ View Recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View Recording
AspireAddPackageVersionToDirectoryPackagesProps ▶️ View Recording
AspireInitSingleFileAppHostRunsViaDotnetRunAppHost ▶️ View Recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View Recording
Banner_DisplayedOnFirstRun ▶️ View Recording
Banner_DisplayedWithExplicitFlag ▶️ View Recording
Banner_NotDisplayedWithNoLogoFlag ▶️ View Recording
CertificatesClean_RemovesCertificates ▶️ View Recording
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate ▶️ View Recording
CertificatesTrust_WithUntrustedCert_TrustsCertificate ▶️ View Recording
ConfigSetGet_CreatesNestedJsonFormat ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View Recording
CreateAndRunEmptyAppHostProject ▶️ View Recording
CreateAndRunJavaEmptyAppHostProject ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateAndRunTypeScriptEmptyAppHostProject ▶️ View Recording
CreateAndRunTypeScriptStarterProject ▶️ View Recording
CreateJavaAppHostWithViteApp ▶️ View Recording
CreateTypeScriptAppHostWithViteApp_UsesConfiguredToolchain ▶️ View Recording
DashboardRunWithOtelTracesReturnsNoTraces ▶️ View Recording
DeployK8sBasicApiService ▶️ View Recording
DeployK8sWithGarnet ▶️ View Recording
DeployK8sWithMongoDB ▶️ View Recording
DeployK8sWithMySql ▶️ View Recording
DeployK8sWithPostgres ▶️ View Recording
DeployK8sWithRabbitMQ ▶️ View Recording
DeployK8sWithRedis ▶️ View Recording
DeployK8sWithSqlServer ▶️ View Recording
DeployK8sWithValkey ▶️ View Recording
DeployTypeScriptAppToKubernetes ▶️ View Recording
DescribeCommandResolvesReplicaNames ▶️ View Recording
DescribeCommandShowsRunningResources ▶️ View Recording
DetachFormatJsonProducesValidJson ▶️ View Recording
DetachFormatJsonProducesValidJsonWhenRestartingExistingInstance ▶️ View Recording
DoListStepsShowsPipelineSteps ▶️ View Recording
DocsCommand_RendersInteractiveMarkdownFromLocalSource ▶️ View Recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View Recording
DoctorCommand_TypeScriptAppHostReportsMissingConfiguredToolchain ▶️ View Recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View Recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View Recording
GlobalMigration_HandlesCommentsAndTrailingCommas ▶️ View Recording
GlobalMigration_HandlesMalformedLegacyJson ▶️ View Recording
GlobalMigration_PreservesAllValueTypes ▶️ View Recording
GlobalMigration_SkipsWhenNewConfigExists ▶️ View Recording
GlobalSettings_MigratedFromLegacyFormat ▶️ View Recording
InitTypeScriptAppHost_AugmentsExistingViteRepoAtRoot ▶️ View Recording
InteractiveCSharpInitCreatesExpectedFiles ▶️ View Recording
InvalidAppHostPathWithComments_IsHealedOnRun ▶️ View Recording
LatestCliCanStartStableChannelAppHost ▶️ View Recording
LatestCliCanStartStableChannelTypeScriptAppHost ▶️ View Recording
LegacySettingsMigration_AdjustsRelativeAppHostPath ▶️ View Recording
LogsCommandShowsResourceLogs ▶️ View Recording
OtelLogsReturnsStructuredLogsFromStarterAppCore ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View Recording
PublishWithConfigureEnvFileUpdatesEnvOutput ▶️ View Recording
PublishWithDockerComposeServiceCallbackSucceeds ▶️ View Recording
PublishWithoutOutputPathUsesAppHostDirectoryDefault ▶️ View Recording
RestoreGeneratesSdkFiles ▶️ View Recording
RestoreGeneratesSdkFiles_WithConfiguredToolchain ▶️ View Recording
RestoreRefreshesGeneratedSdkAfterAddingIntegration ▶️ View Recording
RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes ▶️ View Recording
RunFromParentDirectory_UsesExistingConfigNearAppHost ▶️ View Recording
SecretCrudOnDotNetAppHost ▶️ View Recording
SecretCrudOnTypeScriptAppHost ▶️ View Recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View Recording
StartAndWaitForTypeScriptSqlServerAppHostWithNativeAssets ▶️ View Recording
StopAllAppHostsFromAppHostDirectory ▶️ View Recording
StopNonInteractiveSingleAppHost ▶️ View Recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View Recording
UnAwaitedChainsCompileWithAutoResolvePromises ▶️ View Recording

📹 Recordings uploaded automatically from CI run #25421875752

@mitchdenny mitchdenny marked this pull request as ready for review May 6, 2026 08:38
@mitchdenny mitchdenny requested a review from JamesNK as a code owner May 6, 2026 08:38
Copilot AI review requested due to automatic review settings May 6, 2026 08:38
@mitchdenny mitchdenny requested a review from davidfowl as a code owner May 6, 2026 08:38
Copy link
Copy Markdown
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

Fixes aspire run for single-file C# AppHosts so that *.dev.localhost URLs (and other launch-profile settings) configured via aspire.config.json are honored instead of falling back to hardcoded localhost:* defaults. This closes the gap both by emitting an apphost.run.json from the aspire-empty template and by teaching the .NET single-file launcher path to fall back to aspire.config.json profiles when apphost.run.json is missing.

Changes:

  • Add apphost.run.json to the empty-apphost CLI template so dotnet run --file apphost.cs and aspire run share the same URLs/env-vars.
  • Update DotNetAppHostProject to apply launch settings from aspire.config.json profiles when apphost.run.json isn’t present (run/publish paths), preserving existing precedence.
  • Add targeted unit tests for the new fallback/precedence behavior and template emission.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
tests/Aspire.Cli.Tests/Projects/DotNetAppHostProjectTests.cs Adds coverage for applying aspire.config.json profiles, fallback behavior, and precedence over defaults/run.json.
tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs Verifies aspire new aspire-empty emits apphost.run.json and that its https profile applicationUrl matches aspire.config.json (including *.dev.localhost).
src/Aspire.Cli/Templating/Templates/empty-apphost/apphost.run.json New template file that mirrors aspire.config.json profile URLs/env-vars via shared token substitution.
src/Aspire.Cli/Projects/DotNetAppHostProject.cs Implements aspire.config.json profile fallback and shared profile-application logic for single-file run/publish env setup.

Comment on lines +1191 to +1192
Assert.Contains("://localhost:", appHostRunJson);
Assert.Contains("\"commandName\": \"Project\"", appHostRunJson);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Parse to JsonNode and assert exact property rather than contains.

Comment on lines +1221 to +1222
Assert.Contains("testapp.dev.localhost", appHostRunJson);
Assert.DoesNotContain("://localhost", appHostRunJson);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Parse to JsonNode and assert exact property rather than contains.

Comment on lines +434 to +443
try
{
config = AspireConfigFile.Load(directoryName);
}
catch (JsonException)
{
// Malformed aspire.config.json — fall back to the next source rather than failing
// the run/publish. This mirrors what happens when apphost.run.json is malformed.
return false;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ensure there is a test for invalid JSON

@mitchdenny mitchdenny merged commit 3d03e7c into main May 7, 2026
570 of 575 checks passed
@github-actions github-actions Bot added this to the 13.4 milestone May 7, 2026
nellshamrell pushed a commit to nellshamrell/aspire that referenced this pull request May 18, 2026
…e.config.json (microsoft#16828)

Two-sided gap meant `aspire new aspire-empty --localhost-tld` produced an
`aspire.config.json` with `*.dev.localhost` URLs but `aspire run` showed
the dashboard at hardcoded `localhost:17193`:

- Writer side: the empty-apphost C# template never emitted `apphost.run.json`,
  so dotnet's file-based runner had no launch profile and fell through to CLI
  defaults.
- Reader side: `DotNetAppHostProject` only ever read `apphost.run.json` and
  never looked at the `profiles` section of `aspire.config.json` -- even
  though `GuestAppHostProject` already does this for TS/Python/Go.

Defense in depth:

(b) Add `apphost.run.json` to the empty-apphost template, using the same
    `{{hostName}}`/`{{httpsPort}}`/etc. tokens as `aspire.config.json` so
    they cannot drift. `CopyTemplateTreeToDiskAsync` picks it up automatically.

(c) When `apphost.run.json` is missing, `ConfigureSingleFileRunEnvironment`
    and `ConfigureSingleFilePublishEnvironment` now fall back to the
    `profiles` section of `aspire.config.json` before applying the hardcoded
    defaults. Profile selection prefers `https`, validates `appHost.path`
    against the actual file, only applies if `applicationUrl` is non-empty,
    and silently falls back on `JsonException`. Run path copies env vars
    verbatim; publish path strips `DOTNET_ENVIRONMENT`/`ASPNETCORE_ENVIRONMENT`/
    `ASPIRE_ENVIRONMENT` so `ApplyEffectiveEnvironment(Production)` wins.

Tests: 8 new `DotNetAppHostProjectTests` cover happy path (run + publish),
`apphost.run.json` precedence, missing profiles, missing applicationUrl,
`appHost.path` mismatch, malformed JSON, and `--environment` arg precedence.
2 new `NewCommandTests` assert the template emits `apphost.run.json` whose
`https.applicationUrl` matches `aspire.config.json` for both `--localhost-tld`
and plain-localhost cases.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

[13.3] aspire run ignores *.dev.localhost url in aspire.config.json

3 participants