Skip to content

Commit 239cf6d

Browse files
mitchdennyCopilot
andcommitted
Prefer identity channel for DotNet template resolution
The cherry-picks of #17564 and #17573 alone do not address the `aspire new` + C# Blazor scenario from #17596 because that path runs through `TemplateNuGetConfigService.ResolveTemplatePackageAsync` (DotNet templates), not `NewCommand.ResolveCliTemplateVersionAsync` (CLI-runtime templates) where #17564's `TryGetCurrentCliTemplateVersionPackage` synthesis lives. Teach `ResolveTemplatePackageAsync` to mirror the identity-channel preference that `ResolveCliTemplateVersionAsync` (NewCommand.cs:376-389) already does for CLI templates: when no `--channel` is supplied and no PR hives exist, prefer the channel whose name matches the running CLI's `CliExecutionContext.IdentityChannel` before falling back to Implicit. Without this, a daily 13.5 CLI on a clean `~/.aspire` silently restricts the template search to nuget.org and returns the shipped stable `Aspire.ProjectTemplates 13.3.5` instead of the matching `13.5.0-preview.1.*` daily prerelease from the `dotnet9` feed. The Implicit fallback preserves prior behavior when the identity channel isn't a registered channel (e.g. a "local" CLI without a corresponding local-build hive). See #17596 for full repro. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 58b3c85 commit 239cf6d

2 files changed

Lines changed: 101 additions & 9 deletions

File tree

src/Aspire.Cli/Templating/TemplateNuGetConfigService.cs

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -224,13 +224,44 @@ public async Task<TemplatePackageSelection> ResolveTemplatePackageAsync(Template
224224
$"{string.Join(", ", allChannels.Select(c => c.Name))}");
225225
channels = [matchingChannel];
226226
}
227+
else if (hasPrHives)
228+
{
229+
// If there are hives (PR build directories), include all channels so a
230+
// PR-installed CLI can find its matching local-hive templates.
231+
channels = allChannels;
232+
}
227233
else
228234
{
229-
// If there are hives (PR build directories), include all channels.
230-
// Otherwise, only use the implicit/default channel to avoid prompting.
231-
channels = hasPrHives
232-
? allChannels
233-
: allChannels.Where(c => c.Type is PackageChannelType.Implicit);
235+
// Prefer the channel whose name matches the running CLI's identity
236+
// (CliExecutionContext.IdentityChannel — stable, staging, daily, local,
237+
// or pr-<N>). This keeps DotNet templates (C# Blazor / aspire-starter)
238+
// lock-stepped with the CLI a developer just installed: a daily 13.5
239+
// CLI resolves Aspire.ProjectTemplates from the daily feed, not from
240+
// whatever stable version happens to be live on nuget.org.
241+
//
242+
// Without this, `TemplateNuGetConfigService` only queried the Implicit
243+
// (nuget.org) channel when neither --channel was passed nor PR hives
244+
// existed, so a clean ~/.aspire on a daily CLI silently resolved the
245+
// shipped stable Aspire.ProjectTemplates (e.g. 13.3.5) while the daily
246+
// 13.5.0-preview.1.* package on the dotnet9 feed was never seen. See
247+
// https://github.com/microsoft/aspire/issues/17596 for the full repro.
248+
//
249+
// ResolveCliTemplateVersionAsync (NewCommand.cs) already does the same
250+
// identity-channel preference for CLI-runtime templates; this brings
251+
// DotNet templates in line. The Implicit fallback preserves the prior
252+
// behavior when the identity channel isn't a registered channel (e.g.
253+
// a "local" CLI without a corresponding local-build hive).
254+
PackageChannel? identityChannelMatch = null;
255+
if (!string.IsNullOrWhiteSpace(executionContext.IdentityChannel))
256+
{
257+
identityChannelMatch = allChannels.FirstOrDefault(c =>
258+
string.Equals(c.Name, executionContext.IdentityChannel, StringComparison.OrdinalIgnoreCase));
259+
}
260+
261+
var implicitChannel = allChannels.FirstOrDefault(c => c.Type is PackageChannelType.Implicit);
262+
channels = identityChannelMatch is not null
263+
? [identityChannelMatch]
264+
: implicitChannel is not null ? [implicitChannel] : [];
234265
}
235266

236267
var packagesFromChannels = await interactionService.ShowStatusAsync(Resources.TemplatingStrings.SearchingForAvailableTemplateVersions, async () =>

tests/Aspire.Cli.Tests/Templating/TemplateNuGetConfigServiceTests.cs

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -212,11 +212,15 @@ public async Task CreateOrUpdateNuGetConfigWithoutPromptAsync_NullChannelName_Sh
212212
}
213213

214214
[Fact]
215-
public async Task ResolveTemplatePackageAsync_NullRequestedChannel_UsesImplicitChannelOnly()
215+
public async Task ResolveTemplatePackageAsync_NullRequestedChannel_NoIdentityChannelMatch_UsesImplicitChannelOnly()
216216
{
217-
// No explicit RequestedChannel: the resolver picks the implicit channel only.
218-
// We exercise the production codepath with a tracking packaging service so the
219-
// assertion is that the resolver completes (no exception is thrown by
217+
// No explicit RequestedChannel and the identity channel ("local") does not match any
218+
// registered Explicit channel: the resolver falls back to the implicit channel only.
219+
// This is the preserved-behavior path from before
220+
// https://github.com/microsoft/aspire/issues/17596 — a "local" CLI without a matching
221+
// local-build hive must not silently route DotNet templates through a stable feed
222+
// it doesn't recognize. We exercise the production codepath with a tracking packaging
223+
// service so the assertion is that the resolver completes (no exception is thrown by
220224
// an unexpected channel-lookup path) and only the implicit channel is in play.
221225
var requestedChannels = new List<PackageChannelType>();
222226
var packagingService = new TestPackagingService
@@ -247,6 +251,63 @@ public async Task ResolveTemplatePackageAsync_NullRequestedChannel_UsesImplicitC
247251
async () => await service.ResolveTemplatePackageAsync(query, CancellationToken.None));
248252
}
249253

254+
[Fact]
255+
public async Task ResolveTemplatePackageAsync_NullRequestedChannel_PrefersIdentityChannelMatch()
256+
{
257+
// Regression test for https://github.com/microsoft/aspire/issues/17596: a daily CLI
258+
// running `aspire new` for a DotNet template (e.g. C# Blazor) with no --channel flag
259+
// and an empty ~/.aspire must resolve Aspire.ProjectTemplates from the daily channel,
260+
// not from the implicit nuget.org channel. Previously this path restricted the search
261+
// to the implicit channel only, so the latest shipped stable (e.g. 13.3.5) would mask
262+
// the daily prerelease (e.g. 13.5.0-preview.1.*) that the matching CLI needs.
263+
var packagingService = new TestPackagingService
264+
{
265+
GetChannelsAsyncCallback = _ =>
266+
{
267+
var implicitCh = PackageChannel.CreateImplicitChannel(new FakeNuGetPackageCache
268+
{
269+
GetTemplatePackagesAsyncCallback = (_, _, _, _) => Task.FromResult<IEnumerable<Aspire.Shared.NuGetPackageCli>>(
270+
[
271+
new Aspire.Shared.NuGetPackageCli { Id = TemplateNuGetConfigService.TemplatesPackageName, Version = "13.3.5", Source = "nuget.org" }
272+
])
273+
}, new TestFeatures());
274+
var dailyCh = PackageChannel.CreateExplicitChannel(
275+
"daily",
276+
PackageChannelQuality.Prerelease,
277+
[new PackageMapping("Aspire*", "daily-src")],
278+
new FakeNuGetPackageCache
279+
{
280+
GetTemplatePackagesAsyncCallback = (_, _, _, _) => Task.FromResult<IEnumerable<Aspire.Shared.NuGetPackageCli>>(
281+
[
282+
new Aspire.Shared.NuGetPackageCli { Id = TemplateNuGetConfigService.TemplatesPackageName, Version = "13.5.0-preview.1.26277.21", Source = "daily-src" }
283+
])
284+
},
285+
features: new TestFeatures());
286+
return Task.FromResult<IEnumerable<PackageChannel>>([implicitCh, dailyCh]);
287+
}
288+
};
289+
290+
var executionContext = TestExecutionContextHelper.CreateExecutionContext(
291+
rootDirectory: new DirectoryInfo(Path.GetTempPath()),
292+
identityChannel: "daily");
293+
294+
var service = CreateService(packagingService: packagingService, executionContext: executionContext);
295+
296+
var query = new TemplatePackageQuery(
297+
RequestedChannel: null,
298+
VersionOverride: null,
299+
SourceOverride: null,
300+
IncludePrHives: false);
301+
302+
var selection = await service.ResolveTemplatePackageAsync(query, CancellationToken.None);
303+
304+
// The daily channel must be selected (matches identity) and its prerelease package
305+
// returned — not the stable 13.3.5 from the implicit channel.
306+
Assert.Equal("daily", selection.Channel.Name);
307+
Assert.Equal(PackageChannelType.Explicit, selection.Channel.Type);
308+
Assert.Equal("13.5.0-preview.1.26277.21", selection.Package.Version);
309+
}
310+
250311
[Fact]
251312
public async Task ResolveTemplatePackage_RequestedChannel_NotFound_Throws()
252313
{

0 commit comments

Comments
 (0)