diff --git a/Aspire.slnx b/Aspire.slnx
index 52300ace4ae..1075a042c85 100644
--- a/Aspire.slnx
+++ b/Aspire.slnx
@@ -394,6 +394,7 @@
+
diff --git a/src/Aspire.Managed/NuGet/Commands/RestoreCommand.cs b/src/Aspire.Managed/NuGet/Commands/RestoreCommand.cs
index 0507b06af52..14d72f7bb59 100644
--- a/src/Aspire.Managed/NuGet/Commands/RestoreCommand.cs
+++ b/src/Aspire.Managed/NuGet/Commands/RestoreCommand.cs
@@ -3,7 +3,6 @@
using System.Collections.Immutable;
using System.CommandLine;
-using System.Globalization;
using NuGet.Commands;
using NuGet.Configuration;
using NuGet.Frameworks;
@@ -100,13 +99,6 @@ public static Command Create()
var noNugetOrg = parseResult.GetValue(noNugetOrgOption);
var verbose = parseResult.GetValue(verboseOption);
- // Validate that both nuget-config and sources aren't provided together
- if (!string.IsNullOrEmpty(nugetConfigPath) && sources.Length > 0)
- {
- Console.Error.WriteLine("Error: Cannot specify both --nuget-config and --source. Use one or the other.");
- return 1;
- }
-
// Parse packages (format: PackageId,Version)
var packages = new List<(string Id, string Version)>();
foreach (var pkgArg in packageArgs)
@@ -124,18 +116,18 @@ public static Command Create()
packages.Add((parts[0], parts[1]));
}
- return await ExecuteRestoreAsync([.. packages], framework, output, packagesDir, sources, nugetConfigPath, workingDir, noNugetOrg, verbose).ConfigureAwait(false);
+ return await ExecuteRestoreAsync(packages, framework, output, packagesDir, sources, nugetConfigPath, workingDir, noNugetOrg, verbose).ConfigureAwait(false);
});
return command;
}
private static async Task ExecuteRestoreAsync(
- (string Id, string Version)[] packages,
+ List<(string Id, string Version)> packages,
string framework,
string output,
string? packagesDir,
- string[] sources,
+ string[] cliSources,
string? nugetConfigPath,
string? workingDir,
bool noNugetOrg,
@@ -144,19 +136,16 @@ private static async Task ExecuteRestoreAsync(
var outputPath = Path.GetFullPath(output);
Directory.CreateDirectory(outputPath);
- packagesDir ??= Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
- ".nuget", "packages");
-
var logger = new NuGetLogger(verbose);
try
{
- var nugetFramework = NuGetFramework.Parse(framework);
+ // Load NuGet settings once — handles working dir, config file, and machine-wide settings.
+ var settings = Settings.LoadDefaultSettings(workingDir, nugetConfigPath, new XPlatMachineWideSetting());
if (verbose)
{
- Console.WriteLine(string.Format(CultureInfo.InvariantCulture, "Restoring {0} packages for {1}", packages.Length, framework));
+ Console.WriteLine($"Restoring {packages.Count} packages for {framework}");
Console.WriteLine($"Output: {outputPath}");
Console.WriteLine($"Packages: {packagesDir}");
if (workingDir is not null)
@@ -169,45 +158,43 @@ private static async Task ExecuteRestoreAsync(
}
}
- // Load package sources
- var packageSources = LoadPackageSources(sources, nugetConfigPath, workingDir, noNugetOrg, verbose);
+ // Resolve the default packages path from settings (env var, config, or ~/.nuget/packages).
+ // If --packages-dir is provided, RestoreArgs.GlobalPackagesFolder overrides this.
+ var defaultPackagesPath = SettingsUtility.GetGlobalPackagesFolder(settings);
- // Build PackageSpec
- var packageSpec = BuildPackageSpec(packages, nugetFramework, outputPath, packagesDir, packageSources);
+ // Resolve package sources using NuGet's PackageSourceProvider
+ var packageSources = ResolvePackageSources(settings, cliSources, noNugetOrg);
+
+ var nugetFramework = NuGetFramework.Parse(framework);
+
+ // Build PackageSpec and DependencyGraphSpec
+ var packageSpec = BuildPackageSpec(packages, nugetFramework, outputPath, defaultPackagesPath, packageSources, settings);
- // Create DependencyGraphSpec
var dgSpec = new DependencyGraphSpec();
dgSpec.AddProject(packageSpec);
dgSpec.AddRestore(packageSpec.RestoreMetadata.ProjectUniqueName);
- // Setup providers
+ // Pass settings to the provider so it reuses our pre-loaded settings
var providerCache = new RestoreCommandProvidersCache();
- var providers = new List
- {
- new DependencyGraphSpecRequestProvider(providerCache, dgSpec)
- };
+ var dgProvider = new DependencyGraphSpecRequestProvider(providerCache, dgSpec, settings);
- // Run restore
+ // Run restore — let NuGet handle source credentials, parallel execution, etc.
using var cacheContext = new SourceCacheContext();
- var restoreContext = new RestoreArgs
+ var restoreArgs = new RestoreArgs
{
CacheContext = cacheContext,
Log = logger,
- PreLoadedRequestProviders = providers,
+ PreLoadedRequestProviders = [dgProvider],
DisableParallel = Environment.ProcessorCount == 1,
AllowNoOp = false,
- GlobalPackagesFolder = packagesDir
+ GlobalPackagesFolder = packagesDir,
+ MachineWideSettings = new XPlatMachineWideSetting(),
};
- if (verbose)
- {
- Console.WriteLine("Running restore...");
- }
-
- var results = await RestoreRunner.RunAsync(restoreContext).ConfigureAwait(false);
+ var results = await RestoreRunner.RunAsync(restoreArgs).ConfigureAwait(false);
var summary = results.Count > 0 ? results[0] : null;
- if (summary == null)
+ if (summary is null)
{
Console.Error.WriteLine("Error: Restore returned no results");
return 1;
@@ -238,70 +225,18 @@ private static async Task ExecuteRestoreAsync(
}
}
- private static List LoadPackageSources(string[] sources, string? nugetConfigPath, string? workingDir, bool noNugetOrg, bool verbose)
+ private static List ResolvePackageSources(ISettings settings, string[] cliSources, bool noNugetOrg)
{
- var packageSources = new List();
+ // Load enabled sources from NuGet config
+ var provider = new PackageSourceProvider(settings);
+ var sources = provider.LoadPackageSources().Where(s => s.IsEnabled).ToList();
- // Add explicit sources first (they get priority)
- foreach (var source in sources)
+ // Append CLI --source values (matching NuGet's behavior of merging, not replacing)
+ foreach (var cliSource in cliSources)
{
- packageSources.Add(new PackageSource(source));
- }
-
- // Load from specific config file if specified
- if (!string.IsNullOrEmpty(nugetConfigPath) && File.Exists(nugetConfigPath))
- {
- var configDir = Path.GetDirectoryName(nugetConfigPath)!;
- var configFile = Path.GetFileName(nugetConfigPath);
- var settings = Settings.LoadSpecificSettings(configDir, configFile);
- var provider = new PackageSourceProvider(settings);
-
- foreach (var source in provider.LoadPackageSources())
+ if (!sources.Any(s => s.Source.Equals(cliSource, StringComparison.OrdinalIgnoreCase)))
{
- if (source.IsEnabled && !packageSources.Any(s => s.Source == source.Source))
- {
- packageSources.Add(source);
- }
- }
- }
- // Auto-discover nuget.config from working directory if specified
- else if (!string.IsNullOrEmpty(workingDir) && Directory.Exists(workingDir))
- {
- try
- {
- // LoadDefaultSettings walks up the directory tree looking for nuget.config files
- var settings = Settings.LoadDefaultSettings(workingDir);
- var provider = new PackageSourceProvider(settings);
-
- if (verbose)
- {
- // Show the config file paths that were loaded
- var configPaths = settings.GetConfigFilePaths();
- Console.WriteLine($"Discovering NuGet config from: {workingDir}");
- foreach (var configPath in configPaths)
- {
- Console.WriteLine($" Loaded config: {configPath}");
- }
- }
-
- foreach (var source in provider.LoadPackageSources())
- {
- if (source.IsEnabled && !packageSources.Any(s => s.Source == source.Source))
- {
- if (verbose)
- {
- Console.WriteLine($" Discovered source: {source.Name ?? source.Source}");
- }
- packageSources.Add(source);
- }
- }
- }
- catch (Exception ex)
- {
- if (verbose)
- {
- Console.WriteLine($"Warning: Failed to load NuGet config from {workingDir}: {ex.ToString()}");
- }
+ sources.Add(new PackageSource(cliSource));
}
}
@@ -309,37 +244,28 @@ private static List LoadPackageSources(string[] sources, string?
if (!noNugetOrg)
{
const string nugetOrgUrl = "https://api.nuget.org/v3/index.json";
- if (!packageSources.Any(s => s.Source.Equals(nugetOrgUrl, StringComparison.OrdinalIgnoreCase)))
+ if (!sources.Any(s => s.Source.Equals(nugetOrgUrl, StringComparison.OrdinalIgnoreCase)))
{
Console.WriteLine("Note: Adding nuget.org as fallback package source. Use --no-nuget-org to disable.");
- packageSources.Add(new PackageSource(nugetOrgUrl, "nuget.org"));
- }
- }
-
- if (verbose)
- {
- Console.WriteLine(string.Format(CultureInfo.InvariantCulture, "Using {0} package sources:", packageSources.Count));
- foreach (var source in packageSources)
- {
- Console.WriteLine($" - {source.Name ?? source.Source}");
+ sources.Add(new PackageSource(nugetOrgUrl, "nuget.org"));
}
}
- return packageSources;
+ return sources;
}
private static PackageSpec BuildPackageSpec(
- (string Id, string Version)[] packages,
+ List<(string Id, string Version)> packages,
NuGetFramework framework,
string outputPath,
string packagesPath,
- List sources)
+ List sources,
+ ISettings settings)
{
var projectName = "AspireRestore";
var projectPath = Path.Combine(outputPath, "project.json");
var tfmShort = framework.GetShortFolderName();
- // Build dependencies
var dependencies = packages.Select(p => new LibraryDependency
{
LibraryRange = new LibraryRange(
@@ -348,7 +274,6 @@ private static PackageSpec BuildPackageSpec(
LibraryDependencyTarget.Package)
}).ToImmutableArray();
- // Build target framework info
var tfInfo = new TargetFrameworkInformation
{
FrameworkName = framework,
@@ -356,7 +281,6 @@ private static PackageSpec BuildPackageSpec(
Dependencies = dependencies
};
- // Build restore metadata
var restoreMetadata = new ProjectRestoreMetadata
{
ProjectUniqueName = projectName,
@@ -366,15 +290,14 @@ private static PackageSpec BuildPackageSpec(
OutputPath = outputPath,
PackagesPath = packagesPath,
OriginalTargetFrameworks = [tfmShort],
+ ConfigFilePaths = settings.GetConfigFilePaths().ToList(),
};
- // Add sources
foreach (var source in sources)
{
restoreMetadata.Sources.Add(source);
}
- // Add target framework
restoreMetadata.TargetFrameworks.Add(new ProjectRestoreMetadataFrameworkInfo(framework)
{
TargetAlias = tfmShort
diff --git a/src/Aspire.Managed/Properties/launchSettings.json b/src/Aspire.Managed/Properties/launchSettings.json
new file mode 100644
index 00000000000..b8ed589cd2f
--- /dev/null
+++ b/src/Aspire.Managed/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "profiles": {
+ "Aspire.Managed": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:50500;http://localhost:50501"
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Aspire.Managed.Tests/Aspire.Managed.Tests.csproj b/tests/Aspire.Managed.Tests/Aspire.Managed.Tests.csproj
new file mode 100644
index 00000000000..4ab6bd8607a
--- /dev/null
+++ b/tests/Aspire.Managed.Tests/Aspire.Managed.Tests.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net10.0
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/Aspire.Managed.Tests/NuGet/RestoreCommandTests.cs b/tests/Aspire.Managed.Tests/NuGet/RestoreCommandTests.cs
new file mode 100644
index 00000000000..bcc2dff6bed
--- /dev/null
+++ b/tests/Aspire.Managed.Tests/NuGet/RestoreCommandTests.cs
@@ -0,0 +1,121 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Managed.NuGet.Commands;
+using Microsoft.DotNet.RemoteExecutor;
+using Xunit;
+
+namespace Aspire.Managed.Tests.NuGet;
+
+public class RestoreCommandTests : IDisposable
+{
+ private readonly TestTempDirectory _tempDir = new();
+
+ public void Dispose() => _tempDir.Dispose();
+
+ [Fact]
+ public void RestoreCommand_RespectsNuGetConfigGlobalPackagesFolder()
+ {
+ var customPackagesDir = Path.GetFullPath(Path.Combine(_tempDir.Path, "custom-packages"));
+ var nugetConfigPath = Path.Combine(_tempDir.Path, "NuGet.config");
+
+ File.WriteAllText(nugetConfigPath, $"""
+
+
+
+
+
+
+ """);
+
+ // Run in a separate process so NUGET_PACKAGES env var from the parent
+ // doesn't interfere. The env var takes precedence over config files
+ // in NuGet's resolution order.
+ var options = new RemoteInvokeOptions();
+ options.StartInfo.Environment.Remove("NUGET_PACKAGES");
+
+ RemoteExecutor.Invoke(static async (tempDirPath) =>
+ {
+ var command = RestoreCommand.Create();
+ var outputDir = Path.Combine(tempDirPath, "obj");
+
+ await command.Parse(["--package", "Fake.Package,1.0.0", "--no-nuget-org", "--output", outputDir, "--working-dir", tempDirPath]).InvokeAsync();
+ }, _tempDir.Path, options).Dispose();
+
+ // NuGet writes packageFolders into project.assets.json with the resolved packages directory.
+ var assetsContent = File.ReadAllText(Path.Combine(_tempDir.Path, "obj", "project.assets.json"));
+ Assert.Contains(JsonEncodedPath(customPackagesDir), assetsContent);
+ }
+
+ [Fact]
+ public void RestoreCommand_RespectsNuGetPackagesEnvironmentVariable()
+ {
+ var customPackagesDir = Path.GetFullPath(Path.Combine(_tempDir.Path, "env-packages"));
+
+ // Run in a separate process with NUGET_PACKAGES set to the custom directory.
+ // The env var takes priority over all config file settings.
+ var options = new RemoteInvokeOptions();
+ options.StartInfo.Environment["NUGET_PACKAGES"] = customPackagesDir;
+
+ RemoteExecutor.Invoke(static async (tempDirPath) =>
+ {
+ var command = RestoreCommand.Create();
+ var outputDir = Path.Combine(tempDirPath, "obj");
+
+ await command.Parse(["--package", "Fake.Package,1.0.0", "--no-nuget-org", "--output", outputDir, "--working-dir", tempDirPath]).InvokeAsync();
+ }, _tempDir.Path, options).Dispose();
+
+ // NuGet writes packageFolders into project.assets.json with the resolved packages directory.
+ var assetsContent = File.ReadAllText(Path.Combine(_tempDir.Path, "obj", "project.assets.json"));
+ Assert.Contains(JsonEncodedPath(customPackagesDir), assetsContent);
+ }
+
+ [Fact]
+ public void RestoreCommand_CliSourcesAreAppendedToConfigSources()
+ {
+ var nugetConfigPath = Path.Combine(_tempDir.Path, "NuGet.config");
+ var configSourcePath = Path.Combine(_tempDir.Path, "config-source");
+ var cliSourcePath = Path.Combine(_tempDir.Path, "cli-source");
+
+ File.WriteAllText(nugetConfigPath, $"""
+
+
+
+
+
+
+
+ """);
+
+ // Run in a separate process so the parent's NuGet config doesn't interfere.
+ var options = new RemoteInvokeOptions();
+ options.StartInfo.Environment.Remove("NUGET_PACKAGES");
+
+ RemoteExecutor.Invoke(static async (nugetConfig, cliSourcePath, tempDirPath) =>
+ {
+ var command = RestoreCommand.Create();
+ var outputDir = Path.Combine(tempDirPath, "obj");
+
+ // Pass --source in addition to the config source. Both should be used.
+ await command.Parse([
+ "--package", "Fake.Package,1.0.0",
+ "--no-nuget-org",
+ "--nuget-config", nugetConfig,
+ "--source", cliSourcePath,
+ "--output", outputDir,
+ "--working-dir", tempDirPath]).InvokeAsync();
+ }, nugetConfigPath, cliSourcePath, _tempDir.Path, options).Dispose();
+
+ // NuGet writes the resolved sources into project.assets.json regardless of
+ // whether the restore succeeds. Verify both sources are present.
+ var assetsContent = File.ReadAllText(Path.Combine(_tempDir.Path, "obj", "project.assets.json"));
+ Assert.Contains(JsonEncodedPath(configSourcePath), assetsContent);
+ Assert.Contains(JsonEncodedPath(cliSourcePath), assetsContent);
+ }
+
+ ///
+ /// Converts a file path to its JSON-escaped representation (e.g. backslashes doubled).
+ ///
+ private static string JsonEncodedPath(string path) =>
+ path.Replace(@"\", @"\\");
+}