You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Bundled Aspire CLI operations no longer need to start aspire-managed to search for, restore, or inspect NuGet packages. The Native AOT aspire.exe now calls NuGet.Client APIs in-process, reducing process boundaries while preserving the existing CLI behavior. Non-bundled package search continues to use dotnet package search.
This adds an in-process NuGetClient, rewires the bundle NuGet service and cache, and removes the superseded aspire-managed nuget implementations. Package source mapping, signature verification, dependency resolution, extraction, and manifest generation remain supported.
The CLI consumes the official NuGet.Client 7.12.0-rc.25 packages from the dotnet11 feed, pinned through the NuGetPackageVersionForCli property in eng/Versions.props. The temporary locally-built packages and the repository-local dist package source have been removed. The upstream Native AOT work is tracked by NuGet/Home#14913, with the downstream-visible suppression fix in NuGet/NuGet.Client#7404.
dotnet11 carries only prerelease NuGet.* versions while dotnet-public carries only the stable ones the rest of the repository uses, so NuGet.config maps NuGet.* to both sources at equal specificity. Giving exact patterns to just one source makes it beat the other's wildcard and breaks that consumer with NU1103.
The aspire-managed helper never set up NuGet's credential service, so bundled search and restore could only authenticate with credentials stored in nuget.config; feeds that rely on a credential provider plugin, such as Azure Artifacts, returned 401. The in-process client now initializes the credential service in non-interactive mode, so installed credential providers are used.
This is the only intended behavior change. Credential provider diagnostics go only to the debug log, so they cannot change the failure messages described below.
Behavior parity
This change is meant to move the logic between processes, not change it, so the in-process client was compared line by line against the helper and brought back in line wherever it had diverged:
Restore: the package spec, restore arguments, settings loading, and source resolution are identical to the helper's.
Search: one page per source, deduplicated to one entry per package ID by the highest version string, sorted with the default comparer, and capped at the requested count. Failed sources are reported and skipped rather than failing the search. A --nuget-config path that does not exist falls back to normal discovery.
Exact-match lookups (GetPackageVersionsAsync) are an ordinary search whose result with an ordinally matching ID supplies the versions, rather than a package-metadata query merged across sources.
Failure messages keep the helper-era text: Package restore failed: …, Manifest creation failed: …, and the localized search failure message. The embedded detail is the helper's stderr, reconstructed with the same prefixes, verbose filtering, and trailing error lines.
Logging: NuGet's output is logged at Debug, as the helper's stderr was.
DOTNET_NUGET_SIGNATURE_VERIFICATION is set only for the duration of a restore, as the helper only ever received it itself, instead of leaking into every child process the CLI starts afterwards.
Trust store: an initialization failure is reported and the restore continues.
Process state: NuGet keeps process-wide state between operations: the credential service with its cached credentials, credential provider plugin processes, the HTTP throttle, and other caches. The helper discarded it by exiting after every operation, so the client raises NuGet's own end-of-build reset when the last overlapping operation ends.
Restore cache key: sources are sorted, so their order does not force a new restore, and the key fingerprints the binary that performs the restore. A settings fingerprint added earlier in this PR was removed: it hashed the per-invocation temporary config path, so the cache never hit.
Bundle extraction for aspire doctor: on main, bundled NuGet search extracted the bundle before launching aspire-managed, and the background CLI update check runs that search on startup, so the bundle was on disk by the time aspire doctor looked for DCP. In-process NuGet no longer extracts it, so the DCP health check now does: it asks layout discovery first, exactly as on main, so an ASPIRE_DCP_PATH override or an already extracted bundle still wins, and extracts the bundle only when discovery finds nothing. This was the only code relying on NuGet search having extracted the bundle.
A few differences are inherent to running in-process under Native AOT: NuGet moves from 7.9.0 to 7.12.0-rc.25; NuGet's System.Text.Json deserialization is enabled and Newtonsoft's serialization, component-model, and dynamic features are disabled (see below); the Linux trust store is initialized through X509TrustStore.InitializeForDotNetSdk instead of DispatchProxy, using the same embedded SDK certificate bundles; and NuGet operations no longer require an extracted bundle layout or hold a bundle lease. Per-source search failures log the exception type rather than its message, because NuGet formats feed URLs, including credentials, into those messages.
Native AOT and Newtonsoft.Json
7.12.0-rc.25 still reaches Newtonsoft.Json in places, so Aspire.Cli.csproj disables Newtonsoft's serialization, component-model, and dynamic feature switches. The dynamic-dispatch warnings that remain surface from Microsoft.CSharp and System.Linq.Expressions, which are collapsed to one warning per assembly. Both can be removed once NuGet drops Newtonsoft.Json entirely (NuGet/NuGet.Client#7601).
User-facing usage
Existing commands continue to work without starting aspire-managed for bundled NuGet operations:
aspire integration list
Validation:
Repository restore completed successfully, with the CLI resolving 7.12.0-rc.25 from dotnet11 and Aspire.RuntimeIdentifier.Tool resolving stable 7.9.0 from dotnet-public on a cold package cache.
NuGet client, bundle service, package-cache, signature-verification, DCP health check, and PrebuiltAppHostServerTests passed: 246 succeeded.
The tests that run real restores, manifests, and searches also passed (170 succeeded) with the CLI's exact runtime switches applied, confirming none of the disabled Newtonsoft paths are reached at runtime.
Native AOT win-x64 publish completed with no ILC diagnostics, using the feature switches and per-assembly warning collapsing described above.
A dogfood build of an earlier commit created and ran TypeScript AppHosts end to end: aspire new, aspire add, and aspire run for both aspire-ts-empty and aspire-ts-starter, including a cold-cache restore that produced byte-identical generated modules.
Fixes # (issue)
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?
Check in the patched NuGet.Client packages and restore them from the repository-local dist feed. Reference the packages from Aspire.Cli so Native AOT can compile the in-process NuGet implementation without requiring a sibling NuGet.Client checkout.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: baa86aab-4a9f-44c7-a92e-34f6498c9e4e
This exception embeds the source URL verbatim. Since feed URLs may carry credentials in user-info or query parameters and the exception is displayed to users, pass it through PackageSourceRedactor first.
$"NuGet source '{source.PackageSource.Source}' does not support package downloads.");
src/Aspire.Cli/NuGet/NuGetClient.cs:421
A failed download exposes the complete feed URL through ex.Message, including any embedded PAT or SAS token. Use the existing display redactor for the source portion of this message.
$"Unable to download NuGet package '{package.Id}' version '{package.Version}' from '{source.PackageSource.Source}'.");
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
The in-process client has unresolved feed-failure, RID fallback, cache correctness, and credential-exposure issues.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (7)
src/Aspire.Cli/NuGet/NuGetClient.cs:231
A failure from any one configured feed now faults Task.WhenAll and aborts the entire bundled search. The removed helper isolated per-source failures, so an unavailable private feed did not prevent results from healthy feeds such as NuGet.org. Catch failures per source (while preserving caller cancellation), log a redacted warning, and return an empty result for that source.
Deriving fallbacks by removing the final - segment is not equivalent to NuGet's RID graph. For example, linux-x64 can fall back to unix-x64, but this list never includes that RID, so packages that provide only runtimes/unix-x64 assets produce an incomplete probe manifest. Resolve fallbacks from the runtime graph, as the removed asset resolver did.
var separatorIndex = effectiveRuntimeIdentifier.LastIndexOf('-');
var platform = separatorIndex > 0
? effectiveRuntimeIdentifier[..separatorIndex]
: effectiveRuntimeIdentifier;
src/Aspire.Cli/NuGet/NuGetClient.cs:309
This exception embeds the raw package source, which may contain user-info credentials or SAS query tokens and can flow into CLI error output or logs. Use the existing package-source redactor before including the source in the message.
var dependencyResource = await repository
.GetResourceAsync<DependencyInfoResource>(cancellationToken)
.ConfigureAwait(false)
?? throw new InvalidOperationException(
$"NuGet source '{repository.PackageSource.Source}' does not support dependency resolution.");
src/Aspire.Cli/NuGet/NuGetClient.cs:409
This failure message exposes the raw source URL even though sources can carry credentials or SAS tokens. Redact it with PackageSourceRedactor before surfacing the exception.
var downloadResource = await source
.GetResourceAsync<DownloadResource>(cancellationToken)
.ConfigureAwait(false)
?? throw new InvalidOperationException(
$"NuGet source '{source.PackageSource.Source}' does not support package downloads.");
src/Aspire.Cli/NuGet/NuGetClient.cs:421
The download-unavailable error also includes the unredacted source URL, so credentialed feed URLs can leak through user-visible failure handling. Apply the existing source redactor here as well.
if (downloadResult.Status != DownloadResourceResultStatus.Available ||
downloadResult.PackageStream is null)
{
throw new InvalidOperationException(
$"Unable to download NuGet package '{package.Id}' version '{package.Version}' from '{source.PackageSource.Source}'.");
Exceptions from the in-process search now escape as arbitrary NuGet/configuration exceptions, but template creation and initialization explicitly catch NuGetPackageCacheException to present feed failures as normal CLI errors. Wrap non-cancellation search failures in NuGetPackageCacheException here to preserve that contract.
var results = await nuGetClient.SearchAsync(
query,
exactMatch,
prerelease,
SearchPageSize,
This exact-match search has the same exception-contract regression: callers expect feed failures as NuGetPackageCacheException, while NuGetClient can throw configuration, protocol, and I/O exceptions directly. Preserve cancellation, but translate other failures so existing CLI error handling remains effective.
var results = await nuGetClient.SearchAsync(
exactPackageId,
exactMatch: true,
prerelease,
SearchPageSize,
Map the temporary NuGet package IDs to dotnet-public as well as the local dist source so projects that still consume the stable repository versions can restore alongside Aspire.Cli's local development packages.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: baa86aab-4a9f-44c7-a92e-34f6498c9e4e
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
The new client has package-cache concurrency, RID fallback, asset selection, and error-handling regressions.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (5)
src/Aspire.Cli/NuGet/NuGetClient.cs:439
InstallFromSourceAsync returning false means another process already installed the package while this caller waited for NuGet's package lock; it is not an installation failure. Two workspaces restoring the same uncached package concurrently can therefore make the second CLI fail. Treat this result as a cache hit and verify the package marker after the call.
if (!installed)
{
throw new InvalidOperationException(
$"NuGet package '{package.Id}' version '{package.Version}' could not be installed.");
}
src/Aspire.Cli/NuGet/NuGetClient.cs:657
This fallback construction drops the architecture when stripping a RID. For example, linux-musl-x64 yields linux-musl, linux, unix, and any, but never linux-x64; the repository's RID tests explicitly require linux-musl-x64 and distro-specific RIDs to fall back to linux-x64 (tests/Aspire.Hosting.Sdk.Tests/NuGetUtils.Tests.cs:21-35). Packages that only ship runtimes/linux-x64 assets will therefore produce an incomplete manifest. Use the bundled runtime graph and RuntimeGraph.ExpandRuntime rather than deriving fallbacks from string prefixes.
var separatorIndex = effectiveRuntimeIdentifier.LastIndexOf('-');
var platform = separatorIndex > 0
? effectiveRuntimeIdentifier[..separatorIndex]
: effectiveRuntimeIdentifier;
src/Aspire.Cli/NuGet/NuGetClient.cs:551
runtimeGroup includes culture subdirectories, so this dictionary also receives entries such as fr/Foo.resources.dll and de/Foo.resources.dll. Those entries have the same filename and make ToDictionary throw; with only one culture, the later runtime loop still emits the satellite with Culture = null, causing AssemblyLoader to treat it as a neutral assembly. Exclude satellites from the override map and preserve GetResourceCulture when emitting runtime-group assets.
A failure from any configured source now faults Task.WhenAll and discards successful results from every other source. The removed bundled SearchCommand caught failures per source, so an unavailable secondary feed did not break searches against healthy feeds. Preserve cancellation, but isolate/log individual source failures and continue collecting the remaining results.
var sourceSearches = packageSources.Select(source => exactMatch
? GetPackageMetadataAsync(source, query, prerelease, useCache, cancellationToken)
: SearchSourceAsync(
source,
query,
new global::NuGet.Protocol.Core.Types.SearchFilter(prerelease),
take,
cancellationToken));
var sourceResults = await Task.WhenAll(sourceSearches).ConfigureAwait(false);
Exceptions from the in-process client now escape directly, but InitCommand, NewCommand, and DotNetTemplateFactory only translate NuGetPackageCacheException into the established user-facing package-search error. A malformed config or NuGet protocol failure will therefore reach the top-level unexpected-error path instead of preserving the prior CLI behavior. Wrap non-cancellation failures from both bundled search paths in NuGetPackageCacheException.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (6)
src/Aspire.Cli/NuGet/BundleNuGetService.cs:147
Source order is semantically significant here: ResolveDependencyCandidatesAsync stops after an exact match from the first source, but the cache key sorts sources. Calls with [override, fallback] and [fallback, override] therefore collide and the second call can reuse a manifest restored from the wrong feed. Preserve source order in the fingerprint.
A failure from any configured source now faults Task.WhenAll and aborts the entire bundled search. The removed helper isolated source failures and returned results from the remaining feeds, so one stale or unavailable source now prevents integration/package listing even when another source succeeds. Restore per-source failure isolation while still propagating cancellation.
var sourceResults = await Task.WhenAll(sourceSearches).ConfigureAwait(false);
src/Aspire.Cli/NuGet/NuGetClient.cs:142
Directory.Exists is not NuGet's completion marker. A canceled or crashed extraction can leave this directory behind without .nupkg.metadata; this branch will then treat the partial package as restored and generate an incomplete manifest instead of letting PackageExtractor clean and repair it. Check pathResolver.GetNupkgMetadataPath(...) for existence before skipping the download.
var installPath = pathResolver.GetInstallPath(identity.Id, identity.Version);
if (installPath is null || !Directory.Exists(installPath))
src/Aspire.Cli/NuGet/NuGetClient.cs:312
This raw package source can contain user info or SAS-style query credentials, and the exception reaches user output through PrebuiltAppHostServer.PrepareAsync. The previous subprocess path explicitly redacted source URLs before logging. Redact the source here as well to avoid exposing credentials in logs and CI transcripts.
?? throw new InvalidOperationException(
$"NuGet source '{repository.PackageSource.Source}' does not support dependency resolution.");
src/Aspire.Cli/NuGet/NuGetClient.cs:422
This error interpolates the unredacted package source, which may include user info or SAS query credentials, and the message is surfaced to the user by PrebuiltAppHostServer.PrepareAsync. Apply PackageSourceRedactor before including it in the exception.
?? throw new InvalidOperationException(
$"NuGet source '{source.PackageSource.Source}' does not support package downloads.");
src/Aspire.Cli/NuGet/NuGetClient.cs:434
A credential-bearing feed URL is copied verbatim into this user-visible failure message. This can leak user info or SAS query tokens into diagnostic logs and CI output; redact the source before formatting it.
throw new InvalidOperationException(
$"Unable to download NuGet package '{package.Id}' version '{package.Version}' from '{source.PackageSource.Source}'.");
- Reset NuGet's process-wide state when the last overlapping NuGet operation
ends. NuGet keeps the credential service with its cached credentials,
credential provider plugin processes, and several caches between operations.
The aspire-managed helper discarded all of it by exiting after every
operation, but the CLI can live for an entire aspire run. Operations are now
counted, the credential service is set up for each one, and NuGet's own
StaticState.RaiseBuildEnded reset runs when the count reaches zero, so one
operation ending cannot reset state another is still using.
- Fingerprint the CLI assembly, rather than Environment.ProcessPath, in the
restore cache key for managed launches. Under `dotnet aspire.dll` the process
path is the dotnet host, which does not change when the CLI is updated, so a
stale cached manifest could be reused. Native AOT still fingerprints the
executable, which is where the implementation lives there.
- Remove the runtime identifier graph embedded in aspire-managed. Its only
reader was the helper's restore command, which this change deleted.
- Link the Native AOT workarounds in Aspire.Cli.csproj to #20342, which tracks
removing them once NuGet drops its Newtonsoft.Json dependency.
Adds process-isolated tests for the signature verification scope (explicit
false is preserved, the previous value including unset is restored, overlapping
scopes, repeated disposal, and restore success, failure, and cancellation), the
operation reset, and the restore cache fingerprint.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: baa86aab-4a9f-44c7-a92e-34f6498c9e4e
The DCP health check only asked layout discovery for the DCP path, which finds a
bundle that is already on disk. On main that bundle was extracted by the time
doctor ran because bundled NuGet search, which the background CLI update check
runs on startup, extracted it before launching aspire-managed. In-process NuGet
no longer does, so on a fresh install doctor reported "DCP bundle not found" and
DoctorCommand_WithSslCertDir_ShowsTrustedAndDcpConnectionHealthy failed.
An earlier commit on this branch fixed that by extracting first, but preferred
the extracted layout over layout discovery and so ignored an ASPIRE_DCP_PATH
override; that fix was reverted during review. Discovery now runs first,
exactly as on main, so an override or an already extracted bundle still wins.
Only when it finds nothing does the check extract the bundle, and it holds the
layout lease while the connection probes run DCP from it.
This was the only consumer relying on NuGet search having extracted the bundle;
every other layout consumer already extracts it itself.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: baa86aab-4a9f-44c7-a92e-34f6498c9e4e
Several aspire-managed helper and BundleNuGetService tests were deleted
without in-process replacements. Restore the behavior they covered:
- An invalid cached manifest triggers restore and manifest regeneration.
- A lowercase package request writes the package's canonical ID.
- AppHosts in one workspace share a restore cache entry, and different
package sets do not collide.
- A locked legacy libs directory does not block restore.
- Explicit sources are added to the configured sources.
- The globalPackagesFolder setting in nuget.config is honored.
- Writing the manifest does not create a libs directory.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: baa86aab-4a9f-44c7-a92e-34f6498c9e4e
This comment narrates the implementation change from the old helper path rather than documenting the enduring constraint, so it becomes stale once the PR is merged. Keep only the current invariant: a fresh bundle has no discoverable layout until extraction, and the lease must cover the probes.
Title: Move bundled NuGet operations into Aspire CLI
Head Commit:8be64f3a4e0a623c08b5eecba9b017b83374737a
Tested At: 2026-09-23 (Windows 11 x64 host, plus the repo's Ubuntu 24.04 container runner)
Scope: the highest-priority subset of the proposed checklist
Artifact Version Verification
Expected Commit:8be64f3a
Installed Version:13.6.0-pr.19847.g8be64f3a, installed with get-aspire-cli-pr.ps1 19847 into an isolated folder, using the PR hive from the same CI run (35811857819)
Baseline for comparisons: the latest main daily build, 14.0.0-preview.1.26473.3+0e7147e35a, which still runs NuGet operations in aspire-managed
Status: ✅ Verified
Changes Analyzed
CLI: in-process NuGetClient, BundleNuGetService, BundleNuGetPackageCache, signature-verification scope, and DCP extraction for aspire doctor
Removed: the aspire-managed nuget restore, manifest and search commands
Build and restore configuration:NuGet.config and eng/Versions.props, both already exercised by passing CI on every platform
Tests: unit tests updated and restored
Not changed: hosting, dashboard, templates, VS Code extension and CI workflows
Every scenario ran in a new project in a temporary folder, with its own NUGET_PACKAGES folder for anything that needed a cold package cache.
aspire new aspire-empty --language csharp produced a file-based apphost.cs on Aspire.AppHost.Sdk@13.6.0-pr.19847.g8be64f3a, with a nuget.config that maps Aspire* to the PR hive.
aspire integration list: the PR and main output was identical, with the same 164 packages at the same versions.
aspire integration search redis: identical output. An unknown term returned an empty list with exit code 0 on both.
aspire add:
by short name (redis) and by full ID (Aspire.Hosting.PostgreSQL), each wrote the PR-version directive;
with --version 13.3.0, the directive kept 13.3.0 rather than the latest 13.5.0;
an unknown package failed with exit code 5 and a clear "No exact match" message, leaving the file unchanged.
aspire update:
after downgrading Redis to 13.5.3, it bumped Redis to the PR version and set the channel;
a second run reported "up to date" and changed no files.
aspire start built and started the AppHost (SDK 13.6.0-pr), the dashboard returned HTTP 302, and aspire stop exited cleanly.
Cold cache:aspire new aspire-ts-empty with an empty NUGET_PACKAGES:
downloaded 75 packages into that folder, and the assets file's packageFolders points at it;
wrote a manifest with 126 managed assemblies and 4 native libraries, all resolved from the custom folder;
resolved Aspire.* from the PR hive.
Warm cache: a repeat aspire start logged Using cached package manifest, created no new cache entry, and left the manifest untouched.
Cache-key parity:
aspire new --source <hive> --version <v> followed by aspire start creates two cache entries. That is because the inputs differ: new restores exact versions through a temporary config, while start uses the project config and minimum versions.
Running the same sequence on main also produces two entries, and without --source both CLIs share a single entry. The key behaves the same as on main.
aspire add worked by short name, by full ID, and with a pinned version (CommunityToolkit Ollama 13.3.0). Each add regenerated .aspire/modules/aspire.mts. Calling addRedis, addPostgres and addOllama from apphost.mts started all three resources.
aspire update bumped a downgraded Redis to the PR version and regenerated the SDK code. The app then started, and a second run reported "up to date" with no file changes.
Failure path: a version that the source mapping can't satisfy failed clearly (Unable to find a stable package Aspire.Hosting.Redis with version (>= 13.5.3)), and start exited with code 2 rather than reporting the server as started.
3. Authenticated feed (the one intentional behavior change) ✅
The test used the Azure Artifacts feed dnceng/aspire-msft and the package Aspire.Hosting.Ev2 13.5.0-preview.1.26472.3, with the Azure Artifacts credential provider installed. The feed returns 401 to anonymous requests.
Restore:
The PR CLI got a bearer token from the credential provider in non-interactive mode, downloaded Ev2 and its internal dependencies from the feed, wrote a 195-entry manifest that includes the Ev2 packages, and started the AppHost.
With an isolated HTTP cache, main fails with Unable to load the service index … 401 (Unauthorized).
Search:
The PR authenticated and resolved the service index, but the feed's own search endpoint timed out after about 100 seconds.
dotnet package search against the same feed fails the same way, so this is a problem with the feed, not the CLI.
The search could not be validated end to end; authentication was.
Secrets: a scan of 95 CLI log and stderr files from these runs found no JWTs, Bearer/Basic headers, password fields or ClearTextPassword values. The only strings shaped like tokens were public NuGet package content hashes.
4. Package source mapping ✅
The generated Aspire* → PR hive mapping stopped Redis 13.5.3 from being restored from nuget.org, even though nuget.org has that version.
A more specific pattern (Aspire.Hosting.Ev2 → aspire-msft) overrode Aspire*, as did Aspire.Hosting.Azure* → nuget.org.
5. Partial feed failure during search ✅
The nuget.config listed the PR hive, nuget.org and an unreachable feed (http://127.0.0.1:9).
aspire integration search redis returned identical JSON on the PR and main: 9 results with exit code 0, skipping the broken feed.
The debug-level warning names only the exception type, as the PR description documents.
6. RID-specific, native and satellite assets ✅
I added Aspire.Hosting.SqlServer (which brings in Microsoft.Data.SqlClient 7.0.2) to a new TypeScript AppHost:
Microsoft.Data.SqlClient resolved to runtimes\win\lib\net9.0, not the generic lib\ build.
The native libraries include Microsoft.Data.SqlClient.SNI.dll (win-x64) and the native Hex1b binaries.
Satellite assemblies were listed for 13 cultures.
Parity with main: I restored the same package set with the main build into a separate packages folder. After stripping the package root from the paths:
both manifests have 162 entries with 0 differences;
the generated aspire.mts, base.mts and transport.mts are byte-identical. The baseline copy was deleted and regenerated by the main build first.
The AppHost started with the sql resource.
7. aspire doctor right after a fresh install (Linux container) ✅
The PR CLI was installed in a new Ubuntu 24.04 container with no .NET SDK.
Before doctor, no bundle had been extracted.
aspire doctor extracted the bundle itself, then reported ✅ Developer Control Plane (DCP) connection … succeeded and ended with 5 passed, 3 certificate warnings (expected in a new container), and 0 failed.
The false "DCP bundle not found" result that failed CI on an earlier commit did not occur.
Observations (not regressions from this PR)
Observation
Why it isn't from this PR
TypeScript aspire update on a PR-hive channel proposes the SDK version for CommunityToolkit packages (for example Ollama 13.3.0 → 13.6.0-pr...), the restore then fails, and the command still exits with code 0.
PR-hive channels are pinned, and PackageChannel.GetPackagesAsync returns the pinned version for every package ID. Neither PackageChannel.cs nor PackagingService.cs is changed by this PR. On the unpinned daily channel, main correctly offers 13.5.1-beta.767.
When another authenticated tool has warmed NuGet's shared HTTP cache (%LOCALAPPDATA%\NuGet\v3-cache), main can appear to restore from an authenticated feed.
This is existing NuGet cache behavior. I cleared it for the authenticated-feed comparison above.
aspire update for .NET leaves non-Aspire.* packages alone.
This is by design (ProjectUpdater.IsUpdatablePackage), and the PR doesn't change it.
eng/scripts/aspire-pr-container/run-aspire-pr-container.ps1 fails under PowerShell 7 with Cannot overwrite variable IsWindows.
This is a bug in the test tooling, not this PR. It works under Windows PowerShell 5.1.
PR restores; main gets 401. Search blocked by the feed's search endpoint
Package source mapping
✅ Passed
mapping honored, and more specific patterns take precedence
Partial feed failure (search)
✅ Passed
results identical to main
RID-specific, native and satellite assets
✅ Passed
manifest and generated modules identical to main
aspire doctor right after a fresh install
✅ Passed
bundle extracted, DCP check passes
Overall Result
✅ PR VERIFIED. Where both CLIs could run the same scenario, the PR produced the same search results, restore cache behavior, manifests and generated TypeScript code as main. The one intended difference, support for NuGet credential providers, works as described.
Recommendations
Consider a separate issue for TypeScript aspire update on pinned PR-hive channels: it proposes nonexistent versions for community packages and exits with code 0 when the restore fails. This behavior predates this PR.
Consider fixing run-aspire-pr-container.ps1 so it works under PowerShell 7.
Eric Erhardt (@eerhardt) backporting to release/13.6 failed, the patch most likely resulted in conflicts. Please backport manually!
git am output
$ git am --3way --empty=keep --ignore-whitespace --keep-non-patch changes.patchApplying: Move bundled NuGet operations into Aspire CLIUsing index info to reconstruct a base tree...M Directory.Packages.propsM docs/specs/bundle.mdM src/Aspire.Cli/Aspire.Cli.csprojM src/Aspire.Cli/DotNet/DotNetCliRunner.csM src/Aspire.Cli/Program.csM src/Aspire.Managed/Aspire.Managed.csprojA src/Aspire.Managed/NuGet/Commands/LayoutCommand.csM src/Aspire.Managed/Program.csM tests/Aspire.Cli.Tests/Projects/AppHostServerSessionTests.csM tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.csM tests/Aspire.Cli.Tests/Utils/CliTestHelper.csA tests/Aspire.Hosting.RemoteHost.Tests/LayoutCommandTests.csM tests/Aspire.Managed.Tests/Aspire.Managed.Tests.csprojM tests/Aspire.Managed.Tests/TerminalHostSignalTests.csFalling back to patching base and 3-way merge...Auto-merging Directory.Packages.propsCONFLICT (content): Merge conflict in Directory.Packages.propsAuto-merging docs/specs/bundle.mdCONFLICT (content): Merge conflict in docs/specs/bundle.mdAuto-merging src/Aspire.Cli/Aspire.Cli.csprojAuto-merging src/Aspire.Cli/DotNet/DotNetCliRunner.csAuto-merging src/Aspire.Cli/Program.csAuto-merging src/Aspire.Managed/Aspire.Managed.csprojCONFLICT (content): Merge conflict in src/Aspire.Managed/Aspire.Managed.csprojAuto-merging src/Aspire.Managed/Program.csCONFLICT (content): Merge conflict in src/Aspire.Managed/Program.csAuto-merging tests/Aspire.Cli.Tests/Projects/AppHostServerSessionTests.csAuto-merging tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.csCONFLICT (content): Merge conflict in tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.csAuto-merging tests/Aspire.Cli.Tests/Utils/CliTestHelper.csCONFLICT (rename/delete): tests/Aspire.Hosting.RemoteHost.Tests/LayoutCommandTests.cs renamed to tests/Aspire.Hosting.RemoteHost.Tests/ManifestCommandTests.cs in HEAD, but deleted in Move bundled NuGet operations into Aspire CLI.CONFLICT (modify/delete): tests/Aspire.Hosting.RemoteHost.Tests/ManifestCommandTests.cs deleted in Move bundled NuGet operations into Aspire CLI and modified in HEAD. Version HEAD of tests/Aspire.Hosting.RemoteHost.Tests/ManifestCommandTests.cs left in tree.Auto-merging tests/Aspire.Managed.Tests/Aspire.Managed.Tests.csprojCONFLICT (content): Merge conflict in tests/Aspire.Managed.Tests/Aspire.Managed.Tests.csprojAuto-merging tests/Aspire.Managed.Tests/TerminalHostSignalTests.csCONFLICT (content): Merge conflict in tests/Aspire.Managed.Tests/TerminalHostSignalTests.cserror: Failed to merge in the changes.hint: Use 'git am --show-current-patch=diff' to see the failed patchhint: When you have resolved this problem, run "git am --continue".hint: If you prefer to skip this patch, run "git am --skip" instead.hint: To restore the original branch and stop patching, run "git am --abort".hint: Disable this message with "git config set advice.mergeConflict false"Patch failed at 0001 Move bundled NuGet operations into Aspire CLIError: The process '/usr/bin/git' failed with exit code 128
Documented the CLI behavior change from #19847: bundled NuGet operations moved in-process and now support NuGet credential provider plugins (fixing Azure Artifacts 401 errors).
Step 5 decision: recommendation == "docs_required" (2 triggered signals: pr_body_has_cli_flag_mention, pr_body_has_user_facing_section). No existing docs mentioned the --nuget-config fallback behavior or the in-process credential provider fix by name, so drafted the docs PR.
Changes:
whats-new/aspire-13-6.mdx: added a new "In-process NuGet operations, with credential provider support" subsection under CLI enhancements, describing the removal of the aspire-managed subprocess hop and the new credential provider plugin support (e.g., Azure Artifacts). Updated the release summary bullet to mention it.
get-started/troubleshooting.mdx: added a note to the existing "Azure Artifacts feed" troubleshooting entry clarifying that 13.6+ initializes the NuGet credential service, and how to further diagnose a persistent 401.
Evidence: PR body "User-facing usage" section and the "Intentional behavior change: NuGet credential providers" section describing the 401 fix for Azure Artifacts-style feeds.
Backport of #19847 to release/13.6
/cc @eerhardt
## Customer Impact
Bundled (Native AOT) Aspire CLI installs run NuGet search, restore, and
manifest generation through the `aspire-managed` helper, which pollutes
the dependencies in `aspire-managed`. As a result, user's integrations
dependencies can get mangled with NuGet's dependencies. This change
moves those operations into the CLI process, which calls NuGet.Client
directly with credential providers enabled. It also removes a
helper-process launch from every NuGet operation. Everything else
behaves as before.
## Testing
- Unit tests for the in-process client and its callers:
search/restore/manifest parity, source mapping, `NUGET_PACKAGES` and
`globalPackagesFolder`, signature-verification scoping, and
restore-cache reuse. On release/13.6, all 221 tests in the affected
`Aspire.Cli.Tests` classes pass (2 skipped), and `Aspire.Managed.Tests`
and `Aspire.Hosting.RemoteHost.Tests` pass.
- Native AOT publish on release/13.6 produces no IL diagnostics.
- Full CI passed on main.
- Manual end-to-end validation of the PR build against the main daily
build
([report](#19847 (comment)))
covered:
- .NET and TypeScript create, search, add, update, and start
- cold and warm restore
- an authenticated Azure Artifacts feed
- package source mapping, and search with one feed unreachable
- RID-specific, native, and satellite assets
- `aspire doctor` right after a fresh install
Search results, manifests, and generated TypeScript were identical to
main.
## Risk
Medium. The change is large (42 files) and replaces the NuGet search,
restore, and manifest code that every bundled CLI uses, and it moves the
CLI to NuGet.Client 7.12.0-rc.25. It was checked line by line against
the helper and compared end to end with main. The only intended behavior
change is credential-provider support. One side effect: a feed that
can't be authenticated non-interactively now waits on the credential
provider, as `dotnet restore` does, instead of failing immediately with
401.
## Regression?
No.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: baa86aab-4a9f-44c7-a92e-34f6498c9e4e
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Bundled Aspire CLI operations no longer need to start
aspire-managedto search for, restore, or inspect NuGet packages. The Native AOTaspire.exenow calls NuGet.Client APIs in-process, reducing process boundaries while preserving the existing CLI behavior. Non-bundled package search continues to usedotnet package search.This adds an in-process
NuGetClient, rewires the bundle NuGet service and cache, and removes the supersededaspire-managed nugetimplementations. Package source mapping, signature verification, dependency resolution, extraction, and manifest generation remain supported.The CLI consumes the official NuGet.Client
7.12.0-rc.25packages from thedotnet11feed, pinned through theNuGetPackageVersionForCliproperty ineng/Versions.props. The temporary locally-built packages and the repository-localdistpackage source have been removed. The upstream Native AOT work is tracked by NuGet/Home#14913, with the downstream-visible suppression fix in NuGet/NuGet.Client#7404.dotnet11carries only prereleaseNuGet.*versions whiledotnet-publiccarries only the stable ones the rest of the repository uses, soNuGet.configmapsNuGet.*to both sources at equal specificity. Giving exact patterns to just one source makes it beat the other's wildcard and breaks that consumer withNU1103.Intentional behavior change: NuGet credential providers
The
aspire-managedhelper never set up NuGet's credential service, so bundled search and restore could only authenticate with credentials stored innuget.config; feeds that rely on a credential provider plugin, such as Azure Artifacts, returned 401. The in-process client now initializes the credential service in non-interactive mode, so installed credential providers are used.This is the only intended behavior change. Credential provider diagnostics go only to the debug log, so they cannot change the failure messages described below.
Behavior parity
This change is meant to move the logic between processes, not change it, so the in-process client was compared line by line against the helper and brought back in line wherever it had diverged:
--nuget-configpath that does not exist falls back to normal discovery.GetPackageVersionsAsync) are an ordinary search whose result with an ordinally matching ID supplies the versions, rather than a package-metadata query merged across sources.Package restore failed: …,Manifest creation failed: …, and the localized search failure message. The embedded detail is the helper's stderr, reconstructed with the same prefixes, verbose filtering, and trailing error lines.DOTNET_NUGET_SIGNATURE_VERIFICATIONis set only for the duration of a restore, as the helper only ever received it itself, instead of leaking into every child process the CLI starts afterwards.aspire doctor: onmain, bundled NuGet search extracted the bundle before launchingaspire-managed, and the background CLI update check runs that search on startup, so the bundle was on disk by the timeaspire doctorlooked for DCP. In-process NuGet no longer extracts it, so the DCP health check now does: it asks layout discovery first, exactly as onmain, so anASPIRE_DCP_PATHoverride or an already extracted bundle still wins, and extracts the bundle only when discovery finds nothing. This was the only code relying on NuGet search having extracted the bundle.A few differences are inherent to running in-process under Native AOT: NuGet moves from
7.9.0to7.12.0-rc.25; NuGet's System.Text.Json deserialization is enabled and Newtonsoft's serialization, component-model, and dynamic features are disabled (see below); the Linux trust store is initialized throughX509TrustStore.InitializeForDotNetSdkinstead ofDispatchProxy, using the same embedded SDK certificate bundles; and NuGet operations no longer require an extracted bundle layout or hold a bundle lease. Per-source search failures log the exception type rather than its message, because NuGet formats feed URLs, including credentials, into those messages.Native AOT and Newtonsoft.Json
7.12.0-rc.25still reaches Newtonsoft.Json in places, soAspire.Cli.csprojdisables Newtonsoft's serialization, component-model, and dynamic feature switches. The dynamic-dispatch warnings that remain surface fromMicrosoft.CSharpandSystem.Linq.Expressions, which are collapsed to one warning per assembly. Both can be removed once NuGet drops Newtonsoft.Json entirely (NuGet/NuGet.Client#7601).User-facing usage
Existing commands continue to work without starting
aspire-managedfor bundled NuGet operations:Validation:
7.12.0-rc.25fromdotnet11andAspire.RuntimeIdentifier.Toolresolving stable7.9.0fromdotnet-publicon a cold package cache.PrebuiltAppHostServerTestspassed: 246 succeeded.win-x64publish completed with no ILC diagnostics, using the feature switches and per-assembly warning collapsing described above.aspire new,aspire add, andaspire runfor bothaspire-ts-emptyandaspire-ts-starter, including a cold-cache restore that produced byte-identical generated modules.Fixes # (issue)
Checklist
<remarks />and<code />elements on your triple slash comments?