Skip to content

aspire add picker displays wrong version for C# AppHosts pinned to a non-default channel (pin ignored, installed version differs from displayed) #17294

@radical

Description

@radical

What's broken

For a C# AppHost (*.csproj with <IsAspireHost>true</IsAspireHost>),
aspire add and aspire integration list ignore the channel pin in
aspire.config.json. The picker fans out to every available channel
(Implicit / Stable / Daily / Staging / hives) and dedupes by package id
with Implicit always winning over any Explicit channel. The result: for
any integration that's available from both the user's ambient
nuget.config (Implicit) and the project's pinned channel, the stable
version
is displayed in the picker.

On aspire add <integration>, the project's nuget.config (which is
written with the matching channel's packageSource + packageSource Mapping) takes over and dotnet add package resolves the PR /
localhive version
at restore time. The displayed version is therefore
different from the version that actually gets installed.

Polyglot AppHosts (TypeScript / Python / Go / Java / Rust) do NOT have
this bug: they honor aspire.config.json#channel and display the
pinned-channel version. So a user can see the asymmetry inside a single
install simply by switching the AppHost language.

Repro

Repro reproduces on the current main daily/dev build as well as on
PR #17105 — see "Reproduces on" below.

Setup a PR-acquired install:

./eng/scripts/get-aspire-cli-pr.sh 17105 \
    --install-path /tmp/aspire-issue-val --skip-extension --skip-path
CLI=/tmp/aspire-issue-val/dogfood/pr-17105/bin/aspire

# Two projects with the same channel pin — only the language differs.
mkdir -p /tmp/aspire-issue-val/cs-demo/cs.AppHost /tmp/aspire-issue-val/ts-demo

cat > /tmp/aspire-issue-val/cs-demo/cs.AppHost/cs.AppHost.csproj <<'EOF'
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <IsAspireHost>true</IsAspireHost>
  </PropertyGroup>
</Project>
EOF
cat > /tmp/aspire-issue-val/cs-demo/aspire.config.json <<'EOF'
{ "channel": "pr-17105" }
EOF

cat > /tmp/aspire-issue-val/ts-demo/apphost.ts <<'EOF'
// stub
EOF
cat > /tmp/aspire-issue-val/ts-demo/aspire.config.json <<'EOF'
{ "channel": "pr-17105" }
EOF

# What version of Redis does each project's picker show?
echo "=== C# AppHost ==="
$CLI integration list --apphost /tmp/aspire-issue-val/cs-demo/cs.AppHost/cs.AppHost.csproj \
    --format json | jq '.[] | select(.package == "Aspire.Hosting.Redis")'

echo "=== TypeScript AppHost ==="
$CLI integration list --apphost /tmp/aspire-issue-val/ts-demo/apphost.ts \
    --format json | jq '.[] | select(.package == "Aspire.Hosting.Redis")'

Expected

Both AppHosts honor aspire.config.json#channel. The picker displays
the same channel-pinned version that dotnet add package will actually
install. A C# AppHost pinned to pr-17105 displays
Aspire.Hosting.Redis 13.4.0-pr.17105.<sha>, not a stable version.

Actual

=== C# AppHost ===
{ "name": "redis", "package": "Aspire.Hosting.Redis", "version": "13.3.4" }

=== TypeScript AppHost ===
{ "name": "redis", "package": "Aspire.Hosting.Redis", "version": "13.4.0-pr.17105.g93e48e16" }

Same install, same hive, same channel pin in aspire.config.json
different version displayed depending only on AppHost language. On
aspire add redis, the C# project resolves the PR version at restore
time anyway (via the nuget.config package-source-mapping written
during scaffolding), so the displayed 13.3.4 is a lie about what the
user actually ends up with.

Reproduces on

  • PR feat(cli): aspire doctor lists every Aspire CLI install on the machine #17105 install (13.4.0-pr.17105.g93e48e16): C# returns 118 rows
    including stable Redis; polyglot returns 58 rows including PR Redis.
  • Daily/dev install (13.4.0-preview.1.26269.7, installed via
    https://aka.ms/aspire/get/install.sh --quality dev): same shape.
    With a nonexistent pr-99999 pin in aspire.config.json, C# returns
    128 rows (pin completely ignored); polyglot returns 0 rows (pin
    honored, filters to nonexistent channel). The asymmetry is intrinsic
    to the code path, not the install route.

Why

src/Aspire.Cli/Commands/IntegrationPackageSearchService.cs:88-91:

if (project.LanguageId == KnownLanguageId.CSharp)
{
    return (ConfiguredChannel: null, ExitCode: null);
}

This short-circuit was written under the assumption that C# projects
express their channel via nuget.config rather than
aspire.config.json. But aspire add's picker is a discovery surface,
not a restore mechanism — the user needs to see the version that's
about to be installed. The two concerns ("which source does dotnet add package use?" vs. "which version does the picker display?") were
conflated.

The downstream dedup at SelectPreferredIntegrationPackage
(line 131-137) then prefers Implicit:

return packages
    .OrderByDescending(p => p.Channel.Type is PackageChannelType.Implicit)
    .ThenByDescending(p => SemVersion.Parse(p.Package.Version),)
    .First();

So even when the PR hive contributes a higher semver, Implicit's lower
semver wins for any shared id.

Scope

Affects:

  • aspire add interactive picker
  • aspire integration list
  • aspire integration search
  • MCP list-integrations tool

…all only for C# AppHosts with a non-default aspire.config.json# channel pin. Polyglot AppHosts work correctly.

Does not affect:

  • Restore-time resolution (the project's nuget.config correctly
    routes Aspire* to the hive via packageSourceMapping). The actual
    installed package is correct; only the picker's displayed version is
    wrong.
  • Default-channel C# AppHosts (no other channel competes for dedup).

Possible directions

  1. Honor aspire.config.json#channel for C# AppHosts too — read the
    pin and use it as the channel filter, same way polyglot does.
  2. Add a per-row channel column to the picker so the user can see that
    the displayed version came from default while a pr-17105 row
    also exists at a different version.
  3. Reverse the dedup preference so a configured-channel match beats
    Implicit when the project is pinned to that channel.

Notes

Related to #17225 (about which channel gets used during scaffolding) and
#17292 (about which packages are eligible for the picker after PR #17105
aligned hive-channel filtering with non-hive). This issue is about what
the picker displays for an already-pinned C# project.

Counterpart: #17295 — polyglot AppHosts have the opposite
problem (the pin is honored too strictly and removes all off-channel
integrations from the picker).

Metadata

Metadata

Assignees

Labels

Type

No fields configured for Bug.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions