File: NuGet\BundleNuGetPackageCache.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Globalization;
using Aspire.Cli.Configuration;
using Aspire.Cli.Resources;
using Microsoft.Extensions.Logging;
using NuGetPackage = Aspire.Shared.NuGetPackageCli;
 
namespace Aspire.Cli.NuGet;
 
/// <summary>
/// NuGet package cache implementation for bundled CLIs, which cannot rely on the .NET SDK's
/// <c>dotnet package search</c> command.
/// </summary>
internal sealed class BundleNuGetPackageCache(
    INuGetClient nuGetClient,
    ILogger<BundleNuGetPackageCache> logger,
    IFeatures features) : INuGetPackageCache
{
    // The aspire-managed helper was always invoked with --take 1000.
    private const int SearchTake = 1000;
 
    public async Task<IEnumerable<NuGetPackage>> GetTemplatePackagesAsync(
        DirectoryInfo workingDirectory,
        bool prerelease,
        FileInfo? nugetConfigFile,
        CancellationToken cancellationToken)
    {
        var packages = await SearchAsync(
            workingDirectory,
            "Aspire.ProjectTemplates",
            prerelease,
            nugetConfigFile,
            cancellationToken).ConfigureAwait(false);
 
        return packages.Where(package => package.Id.Equals("Aspire.ProjectTemplates", StringComparison.OrdinalIgnoreCase));
    }
 
    public async Task<IEnumerable<NuGetPackage>> GetIntegrationPackagesAsync(
        DirectoryInfo workingDirectory,
        bool prerelease,
        FileInfo? nugetConfigFile,
        CancellationToken cancellationToken)
    {
        var packages = await SearchAsync(
            workingDirectory,
            "Aspire.Hosting",
            prerelease,
            nugetConfigFile,
            cancellationToken).ConfigureAwait(false);
 
        return FilterPackages(packages, filter: null);
    }
 
    public async Task<IEnumerable<NuGetPackage>> GetCliPackagesAsync(
        DirectoryInfo workingDirectory,
        bool prerelease,
        FileInfo? nugetConfigFile,
        CancellationToken cancellationToken)
    {
        var packages = await SearchAsync(
            workingDirectory,
            "Aspire.Cli",
            prerelease,
            nugetConfigFile,
            cancellationToken).ConfigureAwait(false);
 
        return packages.Where(package => package.Id.Equals("Aspire.Cli", StringComparison.OrdinalIgnoreCase));
    }
 
    public async Task<IEnumerable<NuGetPackage>> GetPackagesAsync(
        DirectoryInfo workingDirectory,
        string packageId,
        Func<string, bool>? filter,
        bool prerelease,
        FileInfo? nugetConfigFile,
        bool useCache,
        CancellationToken cancellationToken)
    {
        var packages = await SearchAsync(
            workingDirectory,
            packageId,
            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 results = await SearchClientAsync(
            workingDirectory,
            exactPackageId,
            prerelease,
            nugetConfigFile,
            cancellationToken).ConfigureAwait(false);
 
        // The helper had no exact-match mode. The CLI ran an ordinary search for the package ID and expanded the
        // versions of the one result whose ID matched it exactly, including its casing.
        var exactMatch = results.FirstOrDefault(package => package.Id.Equals(exactPackageId, StringComparison.Ordinal));
        if (exactMatch is null)
        {
            return [];
        }
 
        return exactMatch.AllVersions.Select(version => new NuGetPackage
        {
            Id = exactMatch.Id,
            Version = version,
            Source = exactMatch.Source
        }).ToList();
    }
 
    private async Task<List<NuGetPackage>> SearchAsync(
        DirectoryInfo workingDirectory,
        string query,
        bool prerelease,
        FileInfo? nugetConfigFile,
        CancellationToken cancellationToken)
    {
        var results = await SearchClientAsync(
            workingDirectory,
            query,
            prerelease,
            nugetConfigFile,
            cancellationToken).ConfigureAwait(false);
 
        return results.Select(package => new NuGetPackage
        {
            Id = package.Id,
            Version = package.Version,
            Source = package.Source
        }).ToList();
    }
 
    private async Task<IReadOnlyList<NuGetSearchResult>> SearchClientAsync(
        DirectoryInfo workingDirectory,
        string query,
        bool prerelease,
        FileInfo? nugetConfigFile,
        CancellationToken cancellationToken)
    {
        try
        {
            return await nuGetClient.SearchAsync(
                query,
                prerelease,
                SearchTake,
                explicitSources: [],
                nugetConfigFile?.FullName,
                workingDirectory.FullName,
                cancellationToken).ConfigureAwait(false);
        }
        catch (NuGetOperationException ex)
        {
            logger.LogError("NuGet search failed");
            logger.LogError("NuGet search stderr: {Error}", ex.Output);
 
            // The helper exited with code 1 for every search failure, and this is the message the CLI reported for it.
            throw new NuGetPackageCacheException(
                string.Format(CultureInfo.CurrentCulture, ErrorStrings.FailedToSearchForPackages, 1),
                ex);
        }
    }
 
    private IEnumerable<NuGetPackage> FilterPackages(
        IEnumerable<NuGetPackage> packages,
        Func<string, bool>? filter)
    {
        return filter is not null
            ? packages.Where(package => filter(package.Id))
            : FilterDeprecatedPackages(packages.Where(package => PackageIdFilters.IsOfficialOrCommunityToolkitPackage(package.Id)));
    }
 
    private IEnumerable<NuGetPackage> FilterDeprecatedPackages(IEnumerable<NuGetPackage> packages)
    {
        return features.IsFeatureEnabled(KnownFeatures.ShowDeprecatedPackages, defaultValue: false)
            ? packages
            : packages.Where(package => !DeprecatedPackages.IsDeprecated(package.Id));
    }
}