From 6541acb3920dfd98e60d0516b8b97a768ca2749c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 9 Oct 2025 08:24:17 +0000
Subject: [PATCH 1/5] Initial plan
From 335b328734c916fa2d74262ac569387be4140d3f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 9 Oct 2025 08:33:24 +0000
Subject: [PATCH 2/5] Add NuGet.config creation to
InitializeExistingSolutionAsync method
Co-authored-by: mitchdenny <513398+mitchdenny@users.noreply.github.com>
---
src/Aspire.Cli/Commands/InitCommand.cs | 53 ++++++++++++++++++++++++++
1 file changed, 53 insertions(+)
diff --git a/src/Aspire.Cli/Commands/InitCommand.cs b/src/Aspire.Cli/Commands/InitCommand.cs
index ec46dc15f2e..f67ac2cd750 100644
--- a/src/Aspire.Cli/Commands/InitCommand.cs
+++ b/src/Aspire.Cli/Commands/InitCommand.cs
@@ -461,6 +461,9 @@ ServiceDefaults project contains helper code to make it easier
await _certificateService.EnsureCertificatesTrustedAsync(_runner, cancellationToken);
+ // Create or update NuGet.config for explicit channels
+ await CreateOrUpdateNuGetConfigAsync(initContext.SolutionDirectory, selectedTemplateDetails.Channel, cancellationToken);
+
InteractionService.DisplaySuccess(InitCommandStrings.AspireInitializationComplete);
return ExitCodeConstants.Success;
}
@@ -597,6 +600,56 @@ private static bool IsSupportedTfm(string tfm)
};
}
+ ///
+ /// Creates or updates a NuGet.config file in the solution directory for explicit channels.
+ ///
+ private async Task CreateOrUpdateNuGetConfigAsync(DirectoryInfo solutionDirectory, PackageChannel channel, CancellationToken cancellationToken)
+ {
+ if (channel.Type is not PackageChannelType.Explicit)
+ {
+ return;
+ }
+
+ var mappings = channel.Mappings;
+ if (mappings is null || mappings.Length == 0)
+ {
+ return;
+ }
+
+ var hasConfigInSolutionDir = NuGetConfigMerger.TryFindNuGetConfigInDirectory(solutionDirectory, out var nugetConfigFile);
+ var hasMissingSources = hasConfigInSolutionDir && NuGetConfigMerger.HasMissingSources(solutionDirectory, channel);
+
+ if (!hasConfigInSolutionDir)
+ {
+ // Ask for confirmation before creating the file
+ var choice = await InteractionService.PromptForSelectionAsync(
+ TemplatingStrings.CreateNugetConfigConfirmation,
+ [TemplatingStrings.Yes, TemplatingStrings.No],
+ c => c,
+ cancellationToken);
+
+ if (string.Equals(choice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput))
+ {
+ await NuGetConfigMerger.CreateOrUpdateAsync(solutionDirectory, channel, cancellationToken: cancellationToken);
+ InteractionService.DisplayMessage("package", TemplatingStrings.NuGetConfigCreatedConfirmationMessage);
+ }
+ }
+ else if (hasMissingSources)
+ {
+ var updateChoice = await InteractionService.PromptForSelectionAsync(
+ "Update NuGet.config to add missing package sources for the selected channel?",
+ [TemplatingStrings.Yes, TemplatingStrings.No],
+ c => c,
+ cancellationToken);
+
+ if (string.Equals(updateChoice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput))
+ {
+ await NuGetConfigMerger.CreateOrUpdateAsync(solutionDirectory, channel, cancellationToken: cancellationToken);
+ InteractionService.DisplayMessage("package", "Updated NuGet.config with required package sources.");
+ }
+ }
+ }
+
private async Task<(NuGetPackage Package, PackageChannel Channel)> GetProjectTemplatesVersionAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
var channels = await _packagingService.GetChannelsAsync(cancellationToken);
From 872cc3017068df6883ce07bb93bc941960905619 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 9 Oct 2025 12:47:42 +0000
Subject: [PATCH 3/5] Refactor NuGet.config prompting logic into shared
NuGetConfigPrompter class
Co-authored-by: mitchdenny <513398+mitchdenny@users.noreply.github.com>
---
src/Aspire.Cli/Commands/InitCommand.cs | 53 +--------
.../Packaging/NuGetConfigPrompter.cs | 105 ++++++++++++++++++
.../Templating/DotNetTemplateFactory.cs | 51 +--------
3 files changed, 112 insertions(+), 97 deletions(-)
create mode 100644 src/Aspire.Cli/Packaging/NuGetConfigPrompter.cs
diff --git a/src/Aspire.Cli/Commands/InitCommand.cs b/src/Aspire.Cli/Commands/InitCommand.cs
index f67ac2cd750..700611b3cac 100644
--- a/src/Aspire.Cli/Commands/InitCommand.cs
+++ b/src/Aspire.Cli/Commands/InitCommand.cs
@@ -462,7 +462,8 @@ ServiceDefaults project contains helper code to make it easier
await _certificateService.EnsureCertificatesTrustedAsync(_runner, cancellationToken);
// Create or update NuGet.config for explicit channels
- await CreateOrUpdateNuGetConfigAsync(initContext.SolutionDirectory, selectedTemplateDetails.Channel, cancellationToken);
+ var nugetConfigPrompter = new NuGetConfigPrompter(InteractionService);
+ await nugetConfigPrompter.PromptToCreateOrUpdateAsync(initContext.SolutionDirectory, selectedTemplateDetails.Channel, cancellationToken);
InteractionService.DisplaySuccess(InitCommandStrings.AspireInitializationComplete);
return ExitCodeConstants.Success;
@@ -600,56 +601,6 @@ private static bool IsSupportedTfm(string tfm)
};
}
- ///
- /// Creates or updates a NuGet.config file in the solution directory for explicit channels.
- ///
- private async Task CreateOrUpdateNuGetConfigAsync(DirectoryInfo solutionDirectory, PackageChannel channel, CancellationToken cancellationToken)
- {
- if (channel.Type is not PackageChannelType.Explicit)
- {
- return;
- }
-
- var mappings = channel.Mappings;
- if (mappings is null || mappings.Length == 0)
- {
- return;
- }
-
- var hasConfigInSolutionDir = NuGetConfigMerger.TryFindNuGetConfigInDirectory(solutionDirectory, out var nugetConfigFile);
- var hasMissingSources = hasConfigInSolutionDir && NuGetConfigMerger.HasMissingSources(solutionDirectory, channel);
-
- if (!hasConfigInSolutionDir)
- {
- // Ask for confirmation before creating the file
- var choice = await InteractionService.PromptForSelectionAsync(
- TemplatingStrings.CreateNugetConfigConfirmation,
- [TemplatingStrings.Yes, TemplatingStrings.No],
- c => c,
- cancellationToken);
-
- if (string.Equals(choice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput))
- {
- await NuGetConfigMerger.CreateOrUpdateAsync(solutionDirectory, channel, cancellationToken: cancellationToken);
- InteractionService.DisplayMessage("package", TemplatingStrings.NuGetConfigCreatedConfirmationMessage);
- }
- }
- else if (hasMissingSources)
- {
- var updateChoice = await InteractionService.PromptForSelectionAsync(
- "Update NuGet.config to add missing package sources for the selected channel?",
- [TemplatingStrings.Yes, TemplatingStrings.No],
- c => c,
- cancellationToken);
-
- if (string.Equals(updateChoice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput))
- {
- await NuGetConfigMerger.CreateOrUpdateAsync(solutionDirectory, channel, cancellationToken: cancellationToken);
- InteractionService.DisplayMessage("package", "Updated NuGet.config with required package sources.");
- }
- }
- }
-
private async Task<(NuGetPackage Package, PackageChannel Channel)> GetProjectTemplatesVersionAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
var channels = await _packagingService.GetChannelsAsync(cancellationToken);
diff --git a/src/Aspire.Cli/Packaging/NuGetConfigPrompter.cs b/src/Aspire.Cli/Packaging/NuGetConfigPrompter.cs
new file mode 100644
index 00000000000..5e0ce63a038
--- /dev/null
+++ b/src/Aspire.Cli/Packaging/NuGetConfigPrompter.cs
@@ -0,0 +1,105 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Cli.Interaction;
+using Aspire.Cli.Resources;
+
+namespace Aspire.Cli.Packaging;
+
+///
+/// Handles prompting users to create or update NuGet.config files for explicit package channels.
+///
+internal class NuGetConfigPrompter
+{
+ private readonly IInteractionService _interactionService;
+
+ public NuGetConfigPrompter(IInteractionService interactionService)
+ {
+ ArgumentNullException.ThrowIfNull(interactionService);
+ _interactionService = interactionService;
+ }
+
+ ///
+ /// Prompts to create or update a NuGet.config for explicit channels.
+ /// Always prompts the user before creating or updating the file.
+ ///
+ /// The directory where the NuGet.config should be created or updated.
+ /// The package channel providing mapping information.
+ /// A cancellation token to observe while waiting for the task to complete.
+ public async Task PromptToCreateOrUpdateAsync(DirectoryInfo targetDirectory, PackageChannel channel, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(targetDirectory);
+ ArgumentNullException.ThrowIfNull(channel);
+
+ if (channel.Type is not PackageChannelType.Explicit)
+ {
+ return;
+ }
+
+ var mappings = channel.Mappings;
+ if (mappings is null || mappings.Length == 0)
+ {
+ return;
+ }
+
+ var hasConfigInTargetDir = NuGetConfigMerger.TryFindNuGetConfigInDirectory(targetDirectory, out var nugetConfigFile);
+ var hasMissingSources = hasConfigInTargetDir && NuGetConfigMerger.HasMissingSources(targetDirectory, channel);
+
+ if (!hasConfigInTargetDir)
+ {
+ // Ask for confirmation before creating the file
+ var choice = await _interactionService.PromptForSelectionAsync(
+ TemplatingStrings.CreateNugetConfigConfirmation,
+ [TemplatingStrings.Yes, TemplatingStrings.No],
+ c => c,
+ cancellationToken);
+
+ if (string.Equals(choice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput))
+ {
+ await NuGetConfigMerger.CreateOrUpdateAsync(targetDirectory, channel, cancellationToken: cancellationToken);
+ _interactionService.DisplayMessage("package", TemplatingStrings.NuGetConfigCreatedConfirmationMessage);
+ }
+ }
+ else if (hasMissingSources)
+ {
+ var updateChoice = await _interactionService.PromptForSelectionAsync(
+ "Update NuGet.config to add missing package sources for the selected channel?",
+ [TemplatingStrings.Yes, TemplatingStrings.No],
+ c => c,
+ cancellationToken);
+
+ if (string.Equals(updateChoice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput))
+ {
+ await NuGetConfigMerger.CreateOrUpdateAsync(targetDirectory, channel, cancellationToken: cancellationToken);
+ _interactionService.DisplayMessage("package", "Updated NuGet.config with required package sources.");
+ }
+ }
+ }
+
+ ///
+ /// Creates or updates a NuGet.config for explicit channels without prompting.
+ /// This is used when creating projects in subdirectories where the behavior is expected.
+ ///
+ /// The directory where the NuGet.config should be created or updated.
+ /// The package channel providing mapping information.
+ /// A cancellation token to observe while waiting for the task to complete.
+ public async Task CreateOrUpdateWithoutPromptAsync(DirectoryInfo targetDirectory, PackageChannel channel, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(targetDirectory);
+ ArgumentNullException.ThrowIfNull(channel);
+
+ if (channel.Type is not PackageChannelType.Explicit)
+ {
+ return;
+ }
+
+ var mappings = channel.Mappings;
+ if (mappings is null || mappings.Length == 0)
+ {
+ return;
+ }
+
+ await NuGetConfigMerger.CreateOrUpdateAsync(targetDirectory, channel, cancellationToken: cancellationToken);
+ _interactionService.DisplayMessage("package", "Created or updated NuGet.config in the project directory with required package sources.");
+ }
+}
diff --git a/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs b/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs
index a73df537306..40e31325f4a 100644
--- a/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs
+++ b/src/Aspire.Cli/Templating/DotNetTemplateFactory.cs
@@ -480,60 +480,19 @@ private async Task PromptToCreateOrUpdateNuGetConfigAsync(PackageChannel channel
var normalizedWorkingPath = workingDir.FullName;
var isInPlaceCreation = string.Equals(normalizedOutputPath, normalizedWorkingPath, StringComparison.OrdinalIgnoreCase);
+ var nugetConfigPrompter = new NuGetConfigPrompter(interactionService);
+
if (!isInPlaceCreation)
{
// For subdirectory creation, always create/update NuGet.config in the output directory only
// and ignore any existing NuGet.config in the working directory
- await NuGetConfigMerger.CreateOrUpdateAsync(outputDir, channel, cancellationToken: cancellationToken);
- interactionService.DisplayMessage("package", "Created or updated NuGet.config in the project directory with required package sources.");
+ await nugetConfigPrompter.CreateOrUpdateWithoutPromptAsync(outputDir, channel, cancellationToken);
return;
}
// In-place creation: preserve existing behavior
- // Check if we need to create or update a NuGet.config in the working directory
- var hasConfigInWorkingDir = TryFindNuGetConfigInDirectory(workingDir, out var nugetConfigFile);
- var hasMissingSources = hasConfigInWorkingDir && NuGetConfigMerger.HasMissingSources(workingDir, channel);
-
- if (!hasConfigInWorkingDir)
- {
- // Ask for confirmation before creating the file
- var choice = await interactionService.PromptForSelectionAsync(
- TemplatingStrings.CreateNugetConfigConfirmation,
- [TemplatingStrings.Yes, TemplatingStrings.No],
- c => c,
- cancellationToken);
-
- if (string.Equals(choice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput))
- {
- await NuGetConfigMerger.CreateOrUpdateAsync(outputDir, channel, cancellationToken: cancellationToken);
- interactionService.DisplayMessage("package", TemplatingStrings.NuGetConfigCreatedConfirmationMessage);
- }
- }
- else if (hasMissingSources)
- {
- var updateChoice = await interactionService.PromptForSelectionAsync(
- "Update NuGet.config to add missing package sources for the selected channel?",
- [TemplatingStrings.Yes, TemplatingStrings.No],
- c => c,
- cancellationToken);
-
- if (string.Equals(updateChoice, TemplatingStrings.Yes, StringComparisons.CliInputOrOutput))
- {
- await NuGetConfigMerger.CreateOrUpdateAsync(workingDir, channel, cancellationToken: cancellationToken);
- interactionService.DisplayMessage("package", "Updated NuGet.config with required package sources.");
- }
- }
- }
-
- private static bool TryFindNuGetConfigInDirectory(DirectoryInfo directory, out FileInfo? nugetConfigFile)
- {
- ArgumentNullException.ThrowIfNull(directory);
-
- // Search only the specified directory for a file named "nuget.config", ignoring case
- nugetConfigFile = directory
- .EnumerateFiles("*", SearchOption.TopDirectoryOnly)
- .FirstOrDefault(f => string.Equals(f.Name, "nuget.config", StringComparison.OrdinalIgnoreCase));
- return nugetConfigFile is not null;
+ // Prompt user before creating or updating NuGet.config
+ await nugetConfigPrompter.PromptToCreateOrUpdateAsync(workingDir, channel, cancellationToken);
}
}
From 8fc619b8f28d6383abe4f9e94991af17cf5aaede Mon Sep 17 00:00:00 2001
From: Mitch Denny
Date: Mon, 13 Oct 2025 14:50:59 +1100
Subject: [PATCH 4/5] Move nuget prompting earlier to make sure it is available
when apphost is added to solution.
---
src/Aspire.Cli/Commands/InitCommand.cs | 36 +++++++++++++++++++++++---
1 file changed, 32 insertions(+), 4 deletions(-)
diff --git a/src/Aspire.Cli/Commands/InitCommand.cs b/src/Aspire.Cli/Commands/InitCommand.cs
index 700611b3cac..a4900551bee 100644
--- a/src/Aspire.Cli/Commands/InitCommand.cs
+++ b/src/Aspire.Cli/Commands/InitCommand.cs
@@ -249,6 +249,10 @@ ServiceDefaults project contains helper code to make it easier
// Get template version/channel selection using the same logic as NewCommand
var selectedTemplateDetails = await GetProjectTemplatesVersionAsync(parseResult, cancellationToken);
+
+ // Create or update NuGet.config for explicit channels in the solution directory
+ // This matches the behavior of 'aspire new' when creating in-place
+ await PromptToCreateOrUpdateNuGetConfigAsync(selectedTemplateDetails.Channel, ExecutionContext.WorkingDirectory, cancellationToken);
// Create a temporary directory for the template output
var tempProjectDir = Path.Combine(Path.GetTempPath(), $"aspire-init-{Guid.NewGuid()}");
@@ -461,10 +465,6 @@ ServiceDefaults project contains helper code to make it easier
await _certificateService.EnsureCertificatesTrustedAsync(_runner, cancellationToken);
- // Create or update NuGet.config for explicit channels
- var nugetConfigPrompter = new NuGetConfigPrompter(InteractionService);
- await nugetConfigPrompter.PromptToCreateOrUpdateAsync(initContext.SolutionDirectory, selectedTemplateDetails.Channel, cancellationToken);
-
InteractionService.DisplaySuccess(InitCommandStrings.AspireInitializationComplete);
return ExitCodeConstants.Success;
}
@@ -601,6 +601,34 @@ private static bool IsSupportedTfm(string tfm)
};
}
+ ///
+ /// Prompts to create or update a NuGet.config for explicit channels in the target directory.
+ /// This ensures consistent behavior with the DotNetTemplateFactory approach.
+ ///
+ /// The package channel.
+ /// The target directory where NuGet.config should be created or updated.
+ /// A cancellation token.
+ private async Task PromptToCreateOrUpdateNuGetConfigAsync(PackageChannel channel, DirectoryInfo targetDirectory, CancellationToken cancellationToken)
+ {
+ if (channel.Type is not PackageChannelType.Explicit)
+ {
+ return;
+ }
+
+ var mappings = channel.Mappings;
+ if (mappings is null || mappings.Length == 0)
+ {
+ return;
+ }
+
+ var nugetConfigPrompter = new NuGetConfigPrompter(InteractionService);
+
+ // For solution-based init, we're creating projects in the solution directory
+ // This is equivalent to "in-place creation" in DotNetTemplateFactory terms
+ // We should prompt the user before creating/updating the NuGet.config
+ await nugetConfigPrompter.PromptToCreateOrUpdateAsync(targetDirectory, channel, cancellationToken);
+ }
+
private async Task<(NuGetPackage Package, PackageChannel Channel)> GetProjectTemplatesVersionAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
var channels = await _packagingService.GetChannelsAsync(cancellationToken);
From e55b10f1f4e30f32945329a6f2a9dc565a37a22c Mon Sep 17 00:00:00 2001
From: Mitch Denny
Date: Mon, 13 Oct 2025 17:56:02 +1100
Subject: [PATCH 5/5] Reduce duplication.
---
src/Aspire.Cli/Commands/InitCommand.cs | 34 ++++----------------------
1 file changed, 5 insertions(+), 29 deletions(-)
diff --git a/src/Aspire.Cli/Commands/InitCommand.cs b/src/Aspire.Cli/Commands/InitCommand.cs
index a4900551bee..cf21112410d 100644
--- a/src/Aspire.Cli/Commands/InitCommand.cs
+++ b/src/Aspire.Cli/Commands/InitCommand.cs
@@ -252,7 +252,11 @@ ServiceDefaults project contains helper code to make it easier
// Create or update NuGet.config for explicit channels in the solution directory
// This matches the behavior of 'aspire new' when creating in-place
- await PromptToCreateOrUpdateNuGetConfigAsync(selectedTemplateDetails.Channel, ExecutionContext.WorkingDirectory, cancellationToken);
+ var nugetConfigPrompter = new NuGetConfigPrompter(InteractionService);
+ await nugetConfigPrompter.PromptToCreateOrUpdateAsync(
+ ExecutionContext.WorkingDirectory,
+ selectedTemplateDetails.Channel,
+ cancellationToken);
// Create a temporary directory for the template output
var tempProjectDir = Path.Combine(Path.GetTempPath(), $"aspire-init-{Guid.NewGuid()}");
@@ -601,34 +605,6 @@ private static bool IsSupportedTfm(string tfm)
};
}
- ///
- /// Prompts to create or update a NuGet.config for explicit channels in the target directory.
- /// This ensures consistent behavior with the DotNetTemplateFactory approach.
- ///
- /// The package channel.
- /// The target directory where NuGet.config should be created or updated.
- /// A cancellation token.
- private async Task PromptToCreateOrUpdateNuGetConfigAsync(PackageChannel channel, DirectoryInfo targetDirectory, CancellationToken cancellationToken)
- {
- if (channel.Type is not PackageChannelType.Explicit)
- {
- return;
- }
-
- var mappings = channel.Mappings;
- if (mappings is null || mappings.Length == 0)
- {
- return;
- }
-
- var nugetConfigPrompter = new NuGetConfigPrompter(InteractionService);
-
- // For solution-based init, we're creating projects in the solution directory
- // This is equivalent to "in-place creation" in DotNetTemplateFactory terms
- // We should prompt the user before creating/updating the NuGet.config
- await nugetConfigPrompter.PromptToCreateOrUpdateAsync(targetDirectory, channel, cancellationToken);
- }
-
private async Task<(NuGetPackage Package, PackageChannel Channel)> GetProjectTemplatesVersionAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
var channels = await _packagingService.GetChannelsAsync(cancellationToken);