Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Aspire.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@
<Project Path="tests/Aspire.Deployment.EndToEnd.Tests/Aspire.Deployment.EndToEnd.Tests.csproj" />
<Project Path="tests/Aspire.EndToEnd.Tests/Aspire.EndToEnd.Tests.csproj" />
<Project Path="tests/Aspire.Hosting.Sdk.Tests/Aspire.Hosting.Sdk.Tests.csproj" />
<Project Path="tests/Aspire.Managed.Tests/Aspire.Managed.Tests.csproj" />
<Project Path="tests/Aspire.Playground.Tests/Aspire.Playground.Tests.csproj" />
<Project Path="tests/Aspire.Templates.Tests/Aspire.Templates.Tests.csproj" />
<Project Path="tests/Aspire.TestUtilities/Aspire.TestUtilities.csproj" />
Expand Down
157 changes: 40 additions & 117 deletions src/Aspire.Managed/NuGet/Commands/RestoreCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

using System.Collections.Immutable;
using System.CommandLine;
using System.Globalization;
using NuGet.Commands;
using NuGet.Configuration;
using NuGet.Frameworks;
Expand Down Expand Up @@ -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)
Expand All @@ -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<int> 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,
Expand All @@ -144,19 +136,16 @@ private static async Task<int> 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)
Expand All @@ -169,45 +158,43 @@ private static async Task<int> 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Above we are printing packagesDir for verbose logging but we are not printing defaultPackagesPath which will be the one we want most of the time.


// 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<IPreLoadedRestoreRequestProvider>
{
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(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: I believe you call this above, so perhaps we can just reuse.

};

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;
Expand Down Expand Up @@ -238,108 +225,47 @@ private static async Task<int> ExecuteRestoreAsync(
}
}

private static List<PackageSource> LoadPackageSources(string[] sources, string? nugetConfigPath, string? workingDir, bool noNugetOrg, bool verbose)
private static List<PackageSource> ResolvePackageSources(ISettings settings, string[] cliSources, bool noNugetOrg)
{
var packageSources = new List<PackageSource>();
// 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));
}
}

// Add nuget.org as a fallback source unless opted out
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<PackageSource> sources)
List<PackageSource> 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(
Expand All @@ -348,15 +274,13 @@ private static PackageSpec BuildPackageSpec(
LibraryDependencyTarget.Package)
}).ToImmutableArray();

// Build target framework info
var tfInfo = new TargetFrameworkInformation
{
FrameworkName = framework,
TargetAlias = tfmShort,
Dependencies = dependencies
};

// Build restore metadata
var restoreMetadata = new ProjectRestoreMetadata
{
ProjectUniqueName = projectName,
Expand All @@ -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
Expand Down
12 changes: 12 additions & 0 deletions src/Aspire.Managed/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"profiles": {
"Aspire.Managed": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:50500;http://localhost:50501"
}
}
}
17 changes: 17 additions & 0 deletions tests/Aspire.Managed.Tests/Aspire.Managed.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<Compile Include="$(TestsSharedDir)TempDirectory.cs" Link="shared/TempDirectory.cs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Aspire.Managed\Aspire.Managed.csproj" />

<PackageReference Include="Microsoft.DotNet.RemoteExecutor" />
</ItemGroup>

</Project>
Loading
Loading