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
60 changes: 43 additions & 17 deletions src/Aspire.Cli/Commands/AddCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,6 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
}
}

var source = parseResult.GetValue(s_sourceOption);

// For non-.NET projects, read the channel from the local Aspire configuration if available.
// Unlike .NET projects which have a nuget.config, polyglot apphosts persist the channel
// in aspire.config.json (or the legacy settings.json during migration).
Expand Down Expand Up @@ -162,8 +160,6 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) =>
throw new EmptyChoicesException(AddCommandStrings.NoIntegrationPackagesFound);
}

var version = parseResult.GetValue(s_versionOption);

var packagesWithShortName = packagesWithChannels.Select(GenerateFriendlyName).OrderBy(p => p.FriendlyName, new CommunityToolkitFirstComparer());

if (!packagesWithShortName.Any())
Expand All @@ -172,7 +168,8 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) =>
return ExitCodeConstants.FailedToAddPackage;
}

var filteredPackagesWithShortName = packagesWithShortName.Where(p => p.FriendlyName == integrationName || p.Package.Id == integrationName);
var filteredPackagesWithShortName = packagesWithShortName
.Where(p => p.FriendlyName == integrationName || p.Package.Id == integrationName);

if (!filteredPackagesWithShortName.Any() && integrationName is not null)
{
Expand All @@ -194,19 +191,20 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) =>
.ToList();
}

var version = parseResult.GetValue(s_versionOption);

// If we didn't match any, show a complete list. If we matched one, and its
// an exact match, then we still prompt, but it will only prompt for
// the version. If there is more than one match then we prompt.
var selectedNuGetPackage = filteredPackagesWithShortName.Count() switch
{
0 => await GetPackageByInteractiveFlowWithNoMatchesMessage(packagesWithShortName, integrationName, cancellationToken),
1 => filteredPackagesWithShortName.First().Package.Version == version
? filteredPackagesWithShortName.First()
: await GetPackageByInteractiveFlow(filteredPackagesWithShortName, null, cancellationToken),
> 1 => await GetPackageByInteractiveFlow(filteredPackagesWithShortName, version, cancellationToken),
_ => throw new InvalidOperationException(AddCommandStrings.UnexpectedNumberOfPackagesFound)
0 => await GetPackageByInteractiveFlowWithNoMatchesMessage(effectiveAppHostProjectFile.Directory!, packagesWithShortName, integrationName, cancellationToken),
1 when filteredPackagesWithShortName.First().Package.Version == version
=> filteredPackagesWithShortName.First(),
_ => await GetPackageByInteractiveFlow(effectiveAppHostProjectFile.Directory!, filteredPackagesWithShortName, version, cancellationToken)
};

var source = parseResult.GetValue(s_sourceOption);
// Add the package using the appropriate project handler
context = new AddPackageContext
{
Expand Down Expand Up @@ -280,7 +278,24 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) =>
}
}

private async Task<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> GetPackageByInteractiveFlow(IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> possiblePackages, string? preferredVersion, CancellationToken cancellationToken)
private static async Task<IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)>> GetAllPackageVersions(DirectoryInfo workingDirectory, IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> possiblePackages, CancellationToken cancellationToken)
{
var distinctPackageIds = possiblePackages.DistinctBy(package => package.Package.Id);
var channels = possiblePackages.Select(package => package.Channel);

var versions = new List<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)>();
foreach (var channel in channels)
{
foreach (var package in distinctPackageIds)
{
var packages = await channel.GetPackageVersionsAsync(package.Package.Id, workingDirectory, cancellationToken);
versions.AddRange(packages.Select(p => (FriendlyName: package.FriendlyName, Package: p, Channel: channel)));
}
}
return versions;
}

private async Task<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> GetPackageByInteractiveFlow(DirectoryInfo workingDirectory, IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> possiblePackages, string? preferredVersion, CancellationToken cancellationToken)
{
var distinctPackages = possiblePackages.DistinctBy(p => p.Package.Id);

Expand All @@ -298,10 +313,21 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) =>

// If any of the package versions are an exact match for the preferred version
// then we can skip the version prompt and just use that version.
if (packageVersions.Any(p => p.Package.Version == preferredVersion))
if (!string.IsNullOrEmpty(preferredVersion))
{
var preferredVersionPackage = packageVersions.First(p => p.Package.Version == preferredVersion);
return preferredVersionPackage;
if (packageVersions.Any(p => p.Package.Version == preferredVersion))
{
var preferredVersionPackage = packageVersions.First(p => p.Package.Version == preferredVersion);
return preferredVersionPackage;
}
else // search all versions of the selected package for a match
{
var allVersions = await GetAllPackageVersions(workingDirectory, possiblePackages, cancellationToken);
if (allVersions.Any(packageVersion => packageVersion.Package.Version == preferredVersion))
{
return allVersions.First(package => package.Package.Version == preferredVersion);
}
}
}

// In non-interactive mode, prefer the implicit/default channel first to keep
Expand All @@ -321,14 +347,14 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) =>
return version;
}

private async Task<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> GetPackageByInteractiveFlowWithNoMatchesMessage(IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> possiblePackages, string? searchTerm, CancellationToken cancellationToken)
private async Task<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> GetPackageByInteractiveFlowWithNoMatchesMessage(DirectoryInfo workingDirectory, IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> possiblePackages, string? searchTerm, CancellationToken cancellationToken)
{
if (searchTerm is not null)
{
InteractionService.DisplaySubtleMessage(string.Format(CultureInfo.CurrentCulture, AddCommandStrings.NoPackagesMatchedSearchTerm, searchTerm));
}

return await GetPackageByInteractiveFlow(possiblePackages, null, cancellationToken);
return await GetPackageByInteractiveFlow(workingDirectory, possiblePackages, null, cancellationToken);
}

internal static (string FriendlyName, NuGetPackage Package, PackageChannel Channel) GenerateFriendlyName((NuGetPackage Package, PackageChannel Channel) packageWithChannel)
Expand Down
28 changes: 19 additions & 9 deletions src/Aspire.Cli/DotNet/DotNetCliRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ internal interface IDotNetCliRunner
Task<int> BuildAsync(FileInfo projectFilePath, bool noRestore, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
Task<int> AddPackageAsync(FileInfo projectFilePath, string packageName, string packageVersion, string? nugetSource, bool noRestore, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
Task<int> AddProjectToSolutionAsync(FileInfo solutionFile, FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool exactMatch, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
Task<(int ExitCode, string[] ConfigPaths)> GetNuGetConfigPathsAsync(DirectoryInfo workingDirectory, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
Task<(int ExitCode, IReadOnlyList<FileInfo> Projects)> GetSolutionProjectsAsync(FileInfo solutionFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
Task<int> AddProjectReferenceAsync(FileInfo projectFile, FileInfo referencedProject, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken);
Expand Down Expand Up @@ -316,7 +316,7 @@ private async Task StartBackchannelAsync(IDotNetCliExecution? execution, string
using var activity = telemetry.StartDiagnosticActivity();

var isSingleFileAppHost = projectFile.Name.Equals("apphost.cs", StringComparison.OrdinalIgnoreCase);

// If we are a single file app host then we use the build command instead of msbuild command.
var cliArgsList = new List<string> { isSingleFileAppHost ? "build" : "msbuild" };

Expand Down Expand Up @@ -826,7 +826,7 @@ public async Task<string> ComputeNuGetConfigHierarchySha256Async(DirectoryInfo w
return result;
}

public async Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
public async Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool exactMatch, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken)
{
using var activity = telemetry.StartDiagnosticActivity();

Expand All @@ -851,7 +851,7 @@ public async Task<string> ComputeNuGetConfigHierarchySha256Async(DirectoryInfo w

// Build a cache key using the main discriminators, including CLI version.
var cliVersion = VersionHelper.GetDefaultTemplateVersion();
rawKey = $"query={query}|prerelease={prerelease}|take={take}|skip={skip}|nugetConfigHash={nugetConfigHash}|cliVersion={cliVersion}";
rawKey = $"query={query}|exactMatch={exactMatch}|prerelease={prerelease}|take={take}|skip={skip}|nugetConfigHash={nugetConfigHash}|cliVersion={cliVersion}";
var cached = await _diskCache.GetAsync(rawKey, cancellationToken).ConfigureAwait(false);
if (cached is not null)
{
Expand All @@ -878,14 +878,24 @@ public async Task<string> ComputeNuGetConfigHierarchySha256Async(DirectoryInfo w
"package",
"search",
query,
"--take",
take.ToString(CultureInfo.InvariantCulture),
"--skip",
skip.ToString(CultureInfo.InvariantCulture),
"--format",
"json"
];

if (exactMatch) // search for all versions that match the query exactly
{
cliArgs.Add("--exact-match");
}
else // 'exaxt-match' flag causes the take and skip arguments to be ignored
{
cliArgs.AddRange([
"--take",
take.ToString(CultureInfo.InvariantCulture),
"--skip",
skip.ToString(CultureInfo.InvariantCulture),
]);
}

if (nugetConfigFile is not null)
{
cliArgs.Add("--configfile");
Expand Down Expand Up @@ -1073,7 +1083,7 @@ public async Task<string> ComputeNuGetConfigHierarchySha256Async(DirectoryInfo w
// Parse output - skip header lines (Project(s) and ----------)
var projects = new List<FileInfo>();
var startParsing = false;

foreach (var line in stdoutLines)
{
if (string.IsNullOrWhiteSpace(line))
Expand Down
61 changes: 52 additions & 9 deletions src/Aspire.Cli/NuGet/BundleNuGetPackageCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ public async Task<IEnumerable<NuGetPackage>> GetTemplatePackagesAsync(
{
var packages = await SearchPackagesInternalAsync(
workingDirectory,
"Aspire.ProjectTemplates",
query: "Aspire.ProjectTemplates",
exactMatch: false,
prerelease,
nugetConfigFile,
cancellationToken).ConfigureAwait(false);
Expand All @@ -55,7 +56,8 @@ public async Task<IEnumerable<NuGetPackage>> GetIntegrationPackagesAsync(
{
var packages = await SearchPackagesInternalAsync(
workingDirectory,
"Aspire.Hosting",
query: "Aspire.Hosting",
exactMatch: false,
prerelease,
nugetConfigFile,
cancellationToken).ConfigureAwait(false);
Expand All @@ -71,7 +73,8 @@ public async Task<IEnumerable<NuGetPackage>> GetCliPackagesAsync(
{
var packages = await SearchPackagesInternalAsync(
workingDirectory,
"Aspire.Cli",
query: "Aspire.Cli",
exactMatch: false,
prerelease,
nugetConfigFile,
cancellationToken).ConfigureAwait(false);
Expand All @@ -90,17 +93,39 @@ public async Task<IEnumerable<NuGetPackage>> GetPackagesAsync(
{
var packages = await SearchPackagesInternalAsync(
workingDirectory,
packageId,
query: packageId,
exactMatch: false,
prerelease,
nugetConfigFile,
cancellationToken).ConfigureAwait(false);

return FilterPackages(packages, filter);
}

public async Task<IEnumerable<NuGetPackage>> GetPackageVersionsAsync(
DirectoryInfo workingDirectory,
string exactPackageId,
bool prerelease,
FileInfo? nugetConfigFile,
bool useCache,
CancellationToken cancellationToken)
{
var packages = await SearchPackagesInternalAsync(
workingDirectory,
query: exactPackageId,
exactMatch: true,
prerelease,
nugetConfigFile,
cancellationToken).ConfigureAwait(false);

bool FilterExactIdMatch(string? id) => string.Equals(id, exactPackageId, StringComparison.Ordinal);
return FilterPackages(packages, FilterExactIdMatch);
}

private async Task<IEnumerable<NuGetPackage>> SearchPackagesInternalAsync(
DirectoryInfo workingDirectory,
string query,
bool exactMatch,
bool prerelease,
FileInfo? nugetConfigFile,
CancellationToken cancellationToken)
Expand Down Expand Up @@ -192,12 +217,30 @@ private async Task<IEnumerable<NuGetPackage>> SearchPackagesInternalAsync(
}

// Convert to NuGetPackage format
return result.Packages.Select(p => new NuGetPackage
if (!exactMatch)
{
return result.Packages.Select(p => new NuGetPackage
{
Id = p.Id,
Version = p.Version,
Source = p.Source ?? string.Empty
}).ToList();
}
else
{
Id = p.Id,
Version = p.Version,
Source = p.Source ?? string.Empty
}).ToList();
var exactMatchResultPackage = result.Packages
.FirstOrDefault(p => p.Id.Equals(query, StringComparison.Ordinal));
if (exactMatchResultPackage is null || exactMatchResultPackage.AllVersions is null)
{
return [];
}
return exactMatchResultPackage.AllVersions.Select(packageVersion => new NuGetPackage
{
Id = exactMatchResultPackage.Id,
Version = packageVersion,
Source = exactMatchResultPackage.Source ?? string.Empty
}).ToList();
}
}
catch (JsonException ex)
{
Expand Down
Loading
Loading