Skip to content

Fix config discovery to search from apphost directory and add aspire.config.json to .NET templates#15423

Merged
joperezr merged 7 commits intorelease/13.2from
fix/config-discovery-from-apphost
Mar 20, 2026
Merged

Fix config discovery to search from apphost directory and add aspire.config.json to .NET templates#15423
joperezr merged 7 commits intorelease/13.2from
fix/config-discovery-from-apphost

Conversation

@mitchdenny
Copy link
Member

@mitchdenny mitchdenny commented Mar 20, 2026

Description

When a user runs aspire new followed by aspire run from the parent directory (without cd-ing into the created project), the CLI creates a spurious aspire.config.json in the current working directory instead of using the one adjacent to the discovered apphost. This happens because config discovery searches upward from CWD rather than from the apphost's directory.

Changes:

  1. Fix config discovery to search from apphost directory — Added ConfigurationHelper.FindNearestConfigFilePath() helper that searches upward from a given directory. Updated ProjectLocator.CreateSettingsFileAsync and GuestAppHostProject.GetConfigDirectory to search from the apphost's directory rather than CWD.

  2. Handle stale config paths — Only skip config creation when the stored appHost.path resolves to the same apphost that was discovered. If the path is stale/invalid, fall through so the config gets healed.

  3. Add aspire.config.json to all .NET apphost templates — The CLI's TypeScript templates already included aspire.config.json, but the .NET SDK templates did not. Added minimal aspire.config.json (with appHost.path) to all five .NET templates:

    • aspire-apphost (csproj)
    • aspire-apphost-singlefile
    • aspire-empty (solution)
    • aspire-starter (solution)
    • aspire-ts-cs-starter (solution)

    The template engine's sourceName substitution ensures the path matches the actual project name chosen by the user.

  4. E2E testConfigDiscoveryTests creates a TypeScript Empty AppHost via aspire new, stays in the parent directory, runs aspire run, and asserts that no config leaks to CWD and the subdirectory config is preserved.

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

Adds a test that verifies aspire.config.json is discovered adjacent
to the apphost file when running from a parent directory, rather than
being recreated in the current working directory.

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

github-actions bot commented Mar 20, 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 -- 15423

Or

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

Mitch Denny and others added 2 commits March 20, 2026 14:57
When an apphost is found via recursive search, use its directory as the
search root for aspire.config.json (walking upward) instead of defaulting
to CWD. This prevents creating a duplicate config when the user runs
'aspire run' from a parent directory after 'aspire new'.

Changes:
- Add ConfigurationHelper.FindNearestConfigFilePath helper
- ProjectLocator.CreateSettingsFileAsync: skip creation when config
  already exists near the apphost with valid appHost.path
- GuestAppHostProject.GetConfigDirectory: search from apphost directory
  so launch profiles are read from the correct config

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The previous check skipped creation whenever any appHost.path existed,
which broke config healing when the path was stale/invalid. Now we
resolve the stored path and only skip if it points to the same apphost
that was discovered via recursive search.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny mitchdenny marked this pull request as ready for review March 20, 2026 04:52
Copilot AI review requested due to automatic review settings March 20, 2026 04:52
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

Fixes aspire.config.json discovery so running aspire run from a parent directory uses the config adjacent to the discovered apphost instead of creating a new config in the current working directory.

Changes:

  • Adds a helper to locate the nearest aspire.config.json (or legacy .aspire/settings.json) by walking upward from a specified directory.
  • Updates config discovery logic in ProjectLocator and GuestAppHostProject to start searching from the apphost directory.
  • Adds an end-to-end test to cover the “run from parent directory” scenario.

Reviewed changes

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

File Description
tests/Aspire.Cli.EndToEnd.Tests/ConfigDiscoveryTests.cs Adds E2E coverage for discovering existing config near the apphost when running from a parent directory.
src/Aspire.Cli/Utils/ConfigurationHelper.cs Introduces upward-walking helper to find the nearest config (new or legacy).
src/Aspire.Cli/Projects/ProjectLocator.cs Changes settings/config creation behavior to consult config near the apphost first.
src/Aspire.Cli/Projects/GuestAppHostProject.cs Aligns config directory selection with “search from apphost directory” behavior.

Mitch Denny and others added 2 commits March 20, 2026 16:04
Include aspire.config.json with appHost.path in the aspire-apphost
(csproj) and aspire-apphost-singlefile templates so that aspire run
from a parent directory finds the config adjacent to the apphost
instead of creating a spurious one in the working directory.

The csproj template uses sourceName 'Aspire.AppHost1' so the template
engine substitutes the actual project name automatically.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add aspire.config.json at the solution root for aspire-empty,
aspire-starter, and aspire-ts-cs-starter templates. Each points to
the AppHost csproj via a relative path. The template engine
substitutes the sourceName so the path matches the actual project
name chosen by the user.

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

@JamesNK JamesNK left a comment

Choose a reason for hiding this comment

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

Overall the approach is sound — searching from the apphost directory upward instead of CWD is the right fix. A few issues noted inline, the most significant being that the CreateSettingsFileAsync early-return doesn't handle the legacy .aspire/settings.json case correctly.

@JamesNK
Copy link
Member

JamesNK commented Mar 20, 2026

I thought creating a file like this (at least when it was ./aspire/settings.json) was intentional behavior so aspire didn't have to search for the app host on additional runs.

Also, is creating this file skipped when there are multiple app hosts and one has to be picked? I thought it was created then to avoid having to select the app host you wanted each time you aspire run/start.

@mitchdenny mitchdenny changed the title Fix aspire.config.json discovery to search from apphost directory Fix config discovery to search from apphost directory and add aspire.config.json to .NET templates Mar 20, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny
Copy link
Member Author

cc @DamianEdwards @maddymontaquila @joperezr @radical — looking for your input on the template changes specifically.

Why we're adding aspire.config.json to the .NET templates

Today, when a user runs aspire new and picks a .NET-based template (e.g. aspire-starter, aspire-apphost, etc.), the template output does not include an aspire.config.json file. The TypeScript/CLI templates already do.

This creates a problem with a common workflow:

mkdir myproject && cd myproject
aspire new          # creates e.g. MyApp/ subdirectory with the solution
aspire run          # runs from the parent directory

Because there's no aspire.config.json next to the apphost, the CLI's config discovery walks upward from the discovered apphost, finds nothing, and then falls back to creating a new aspire.config.json in the current working directory — which is the parent, not the project directory. This is the spurious config file the rest of this PR fixes at the discovery layer.

Adding a minimal aspire.config.json (containing just appHost.path) to each .NET template means:

  1. Config discovery finds the file adjacent to the apphost immediately — no upward walk, no CWD fallback
  2. The appHost.path property lets the CLI know exactly which file is the apphost without re-scanning
  3. Parity with the TypeScript templates which already ship aspire.config.json
  4. The .NET template engine's sourceName substitution handles renaming the path automatically (e.g. Aspire.AppHost1.csprojMyApp.AppHost.csproj)

For standalone templates (aspire-apphost, aspire-apphost-singlefile, aspire-py-starter) the config sits next to the apphost file. For solution templates (aspire-empty, aspire-starter, aspire-ts-cs-starter) it sits at the solution root and uses a relative path to the AppHost subdirectory.

@mitchdenny
Copy link
Member Author

I thought creating a file like this (at least when it was ./aspire/settings.json) was intentional behavior so aspire didn't have to search for the app host on additional runs.

Also, is creating this file skipped when there are multiple app hosts and one has to be picked? I thought it was created then to avoid having to select the app host you wanted each time you aspire run/start.

Just but I think with the shift to aspire.config.json some of our original assumptions are changing. I think aspire.config.json is becoming more of a persistent wart because it contains information which is essential for successful execution in the polyglot world.

- Fix legacy .aspire/settings.json path handling in ProjectLocator:
  resolve config root to parent of .aspire/ directory
- Update GetConfigDirectory XML doc to reflect new behavior
- Remove unused _configurationService field and constructor parameter
- Assert originalContent == currentContent in E2E test

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mitchdenny
Copy link
Member Author

Addressed all review feedback from @JamesNK and Copilot in 46e5a59:

  1. originalContent unused in test — Added Assert.Equal(originalContent, currentContent) to verify the config wasn't modified by the run.

  2. XML doc on GetConfigDirectory is stale — Updated the <summary> to say "by searching upward from appHostDirectory" instead of referencing IConfigurationService.

  3. _configurationService is dead code — Removed the field, the constructor assignment, and the constructor parameter itself. DI uses ActivatorUtilities.CreateInstance so it just stops resolving it. Updated the test helper accordingly.

  4. Legacy .aspire/settings.json path handling in ProjectLocator — Good catch. When FindNearestConfigFilePath returns a legacy path, we now resolve configDir to the parent of .aspire/ before calling AspireConfigFile.Load. Same pattern already used in GuestAppHostProject.GetConfigDirectory.

All 1,701 CLI unit tests pass locally.

@github-actions
Copy link
Contributor

🎬 CLI E2E Test Recordings — 54 recordings uploaded (commit 46e5a59)

View recordings
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
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View Recording
Banner_DisplayedOnFirstRun ▶️ View Recording
Banner_DisplayedWithExplicitFlag ▶️ View Recording
CertificatesClean_RemovesCertificates ▶️ View Recording
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate ▶️ View Recording
CertificatesTrust_WithUntrustedCert_TrustsCertificate ▶️ View Recording
ConfigSetGet_CreatesNestedJsonFormat ▶️ View Recording
CreateAndDeployToDockerCompose ▶️ View Recording
CreateAndDeployToDockerComposeInteractive ▶️ View Recording
CreateAndPublishToKubernetes ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View Recording
CreateAndRunEmptyAppHostProject ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateAndRunTypeScriptEmptyAppHostProject ▶️ View Recording
CreateAndRunTypeScriptStarterProject ▶️ View Recording
CreateStartAndStopAspireProject ▶️ 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 ▶️ View Recording
GlobalMigration_HandlesCommentsAndTrailingCommas ▶️ View Recording
GlobalMigration_HandlesMalformedLegacyJson ▶️ View Recording
GlobalMigration_PreservesAllValueTypes ▶️ View Recording
GlobalMigration_SkipsWhenNewConfigExists ▶️ View Recording
GlobalSettings_MigratedFromLegacyFormat ▶️ View Recording
InvalidAppHostPathWithComments_IsHealedOnRun ▶️ View Recording
LegacySettingsMigration_AdjustsRelativeAppHostPath ▶️ View Recording
LogsCommandShowsResourceLogs ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View Recording
PublishWithDockerComposeServiceCallbackSucceeds ▶️ View Recording
RestoreGeneratesSdkFiles ▶️ View Recording
RunFromParentDirectory_UsesExistingConfigNearAppHost ▶️ View Recording
RunWithMissingAwaitShowsHelpfulError ▶️ View Recording
SecretCrudOnDotNetAppHost ▶️ View Recording
SecretCrudOnTypeScriptAppHost ▶️ View Recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View Recording
StopAllAppHostsFromAppHostDirectory ▶️ View Recording
StopAllAppHostsFromUnrelatedDirectory ▶️ View Recording
StopNonInteractiveMultipleAppHostsShowsError ▶️ View Recording
StopNonInteractiveSingleAppHost ▶️ View Recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View Recording
TypeScriptAppHostWithProjectReferenceIntegration ▶️ View Recording

📹 Recordings uploaded automatically from CI run #23332903707

@DamianEdwards
Copy link
Member

@mitchdenny we should make aspire update add the file if it's missing to ensure folks moving from prior versions get setup the same way a new app does.

Copy link
Member

@joperezr joperezr left a comment

Choose a reason for hiding this comment

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

Validated this works. Thanks @mitchdenny!

@joperezr joperezr merged commit e8bb007 into release/13.2 Mar 20, 2026
259 checks passed
@joperezr joperezr deleted the fix/config-discovery-from-apphost branch March 20, 2026 17:06
@dotnet-policy-service dotnet-policy-service bot modified the milestone: 13.2 Mar 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants