Fix #16725: aspire run honours dev.localhost URLs from aspire.config.json#16828
Conversation
…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>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 16828Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 16828" |
|
🎬 CLI E2E Test Recordings — 77 recordings uploaded (commit View all recordings
📹 Recordings uploaded automatically from CI run #25421875752 |
There was a problem hiding this comment.
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.jsonto theempty-apphostCLI template sodotnet run --file apphost.csandaspire runshare the same URLs/env-vars. - Update
DotNetAppHostProjectto apply launch settings fromaspire.config.jsonprofiles whenapphost.run.jsonisn’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. |
| Assert.Contains("://localhost:", appHostRunJson); | ||
| Assert.Contains("\"commandName\": \"Project\"", appHostRunJson); |
There was a problem hiding this comment.
Parse to JsonNode and assert exact property rather than contains.
| Assert.Contains("testapp.dev.localhost", appHostRunJson); | ||
| Assert.DoesNotContain("://localhost", appHostRunJson); |
There was a problem hiding this comment.
Parse to JsonNode and assert exact property rather than contains.
| 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; | ||
| } |
There was a problem hiding this comment.
Ensure there is a test for invalid JSON
…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>
Description
Fix #16725:
aspire runignored*.dev.localhostURLs configured byaspire.config.json. Afteraspire new aspire-empty --localhost-tld --name test1,aspire runwas bringing the dashboard up athttps://localhost:17193/...instead ofhttps://test1.dev.localhost:17159/....There were two independent gaps causing this:
aspire.config.jsonbut never emittedapphost.run.json. With no launch profile on disk,dotnet run apphost.cshad nothing to read, and the CLI fell through to the hardcodedlocalhost:17193defaults inDotNetAppHostProject.ApplyDefaultSingleFileEndpoints. (PR Fix #15986: emit apphost.run.json from aspire init single-file skeleton #16812 closed the analogous gap foraspire init, butaspire newgoes throughCliTemplateFactoryand was untouched.)DotNetAppHostProject.ConfigureSingleFile{Run,Publish}Environmentonly ever readapphost.run.jsonand never looked at theprofilessection ofaspire.config.json— even thoughGuestAppHostProject.ReadProfileFromAspireConfigalready implements exactly this fallback for TS/Python/Go.This PR fixes both, defense-in-depth:
(b) Template — emit
apphost.run.jsonalongsideaspire.config.jsonAdded
src/Aspire.Cli/Templating/Templates/empty-apphost/apphost.run.jsonusing the same{{hostName}}/{{httpsPort}}/{{httpPort}}/{{otlpHttpsPort}}/{{otlpHttpPort}}/{{resourceHttpsPort}}/{{resourceHttpPort}}tokens thataspire.config.jsonalready uses.CopyTemplateTreeToDiskAsyncperforms the substitution uniformly so the two files cannot drift in URLs.(c) Reader — fall back to
aspire.config.jsonprofiles whenapphost.run.jsonis missingDotNetAppHostProjectnow mirrorsGuestAppHostProject's precedence:apphost.run.json(existing)aspire.config.jsonprofiles(new)localhost:17193defaults (existing)The new
TryApplyAspireConfigProfilehelper:aspire.config.jsonfrom the AppHost's directory only (no upward walk).appHost.pathresolves to the AppHost being launched (or is null) — protects multi-AppHost layouts.httpsprofile, falls back to the first one.applicationUrlis non-empty.JsonExceptionand falls through silently (matches existingapphost.run.jsonbehavior on malformed JSON).For
aspire run, env vars are copied verbatim. Foraspire publish, env-name vars (DOTNET_ENVIRONMENT/ASPNETCORE_ENVIRONMENT/ASPIRE_ENVIRONMENT) are filtered out soApplyEffectiveEnvironment(Production)still wins.ApplyEffectiveEnvironmentis always called last so explicit--environment Fooarguments 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-interactiveproject:Bonus: deleted
apphost.run.jsonafter generation and re-ranaspire run— fix C alone (reader fallback toaspire.config.json) still produces the correct dev.localhost URL.Tests
8 new tests in
DotNetAppHostProjectTests:ConfigureSingleFileRunEnvironment_AppliesProfileFromAspireConfigJsonConfigureSingleFilePublishEnvironment_AppliesProfileFromAspireConfigJsonConfigureSingleFilePublishEnvironment_AppHostRunJsonWinsOverAspireConfigJsonConfigureSingleFileRunEnvironment_FallsBackToDefaultsWhenAspireConfigJsonHasNoProfilesConfigureSingleFileRunEnvironment_FallsBackToDefaultsWhenProfileLacksApplicationUrlConfigureSingleFileRunEnvironment_SkipsAspireConfigWhenAppHostPathMismatchesConfigureSingleFileRunEnvironment_FallsBackToDefaultsWhenAspireConfigJsonIsMalformedConfigureSingleFileRunEnvironment_EnvironmentArgumentOverridesProfileDotNetEnvironment2 new tests in
NewCommandTests:NewCommandWithCSharpEmptyTemplateAndPlainLocalhostEmitsAppHostRunJsonMatchingAspireConfigJsonNewCommandWithCSharpEmptyTemplateAndLocalhostTldEmitsAppHostRunJsonWithDevLocalhostUrlsFull
Aspire.Cli.Testssuite: 2564/2575 passed, 0 failures (11 OS-conditional skips).Follow-ups (intentionally out of scope)
InitCommand.DropAppHostRunJsonstill constructs the JSON literal in C# rather than sharing the template. Both code paths produce byte-identical shapes today, but a future cleanup could routeaspire initthrough the same template renderer to eliminate the drift surface entirely.Fixes #16725
Checklist