File: NuGet\NuGetPackagePrefetcher.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 Aspire.Cli.Commands;
using Aspire.Cli.Configuration;
using Aspire.Cli.Packaging;
using Aspire.Cli.Utils;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Cli.NuGet;
 
internal sealed class NuGetPackagePrefetcher(ILogger<NuGetPackagePrefetcher> logger, CliExecutionContext executionContext, IFeatures features, IPackagingService packagingService, ICliUpdateNotifier cliUpdateNotifier) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Wait for command to be selected
        var command = await WaitForCommandSelectionAsync(stoppingToken);
        
        var shouldPrefetchTemplates = ShouldPrefetchTemplatePackages(command);
        var shouldPrefetchCli = ShouldPrefetchCliPackages(command);
 
        // Prefetch template packages if needed
        if (shouldPrefetchTemplates)
        {
            _ = Task.Run(async () =>
             {
                 try
                 {
                     var channels = await packagingService.GetChannelsAsync();
 
                     foreach (var channel in channels)
                     {
                         // Discard the results here, we just want them in the cache.
                         _ = await channel.GetTemplatePackagesAsync(executionContext.WorkingDirectory, stoppingToken);
                     }
                 }
                 catch (System.Exception ex)
                 {
                     logger.LogDebug(ex, "Non-fatal error while prefetching template packages. This is not critical to the operation of the CLI.");
                     // This prefetching is best effort. If it fails we log (above) and then the
                     // background service will exit gracefully. Code paths that depend on this
                     // data will handle the absence of pre-fetched packages gracefully.
                 }
             }, stoppingToken);
        }
 
        // Prefetch CLI packages if needed
        if (shouldPrefetchCli)
        {
            _ = Task.Run(async () =>
            {
                if (features.IsFeatureEnabled(KnownFeatures.UpdateNotificationsEnabled, true))
                {
                    try
                    {
                        await cliUpdateNotifier.CheckForCliUpdatesAsync(
                            workingDirectory: executionContext.WorkingDirectory,
                            cancellationToken: stoppingToken
                            );
                    }
                    catch (System.Exception ex)
                    {
                        logger.LogDebug(ex, "Non-fatal error while prefetching CLI packages. This is not critical to the operation of the CLI.");
                    }
                }
            }, stoppingToken);
        }
    }
 
    private async Task<BaseCommand?> WaitForCommandSelectionAsync(CancellationToken cancellationToken)
    {
        try
        {
            // Wait for command to be selected, with a timeout
            // If timeout occurs, proceed with default behavior (no command)
            using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(1));
            using var combined = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeout.Token);
            
            var command = await executionContext.CommandSelected.Task.WaitAsync(combined.Token);
            return command;
        }
        catch (OperationCanceledException)
        {
            // Timeout or cancellation occurred - proceed with no command (default behavior)
            return null;
        }
    }
 
    private static bool ShouldPrefetchTemplatePackages(BaseCommand? command)
    {
        // If the command implements IPackageMetaPrefetchingCommand, use its setting
        if (command is IPackageMetaPrefetchingCommand prefetchingCommand)
        {
            return prefetchingCommand.PrefetchesTemplatePackageMetadata;
        }
 
        // Default behavior: prefetch templates for all commands except run, publish, deploy
        // Because of this: https://github.com/dotnet/aspire/issues/6956
        return command is null || !IsRuntimeOnlyCommand(command);
    }
 
    private static bool ShouldPrefetchCliPackages(BaseCommand? command)
    {
        // If the command implements IPackageMetaPrefetchingCommand, use its setting
        if (command is IPackageMetaPrefetchingCommand prefetchingCommand)
        {
            return prefetchingCommand.PrefetchesCliPackageMetadata;
        }
 
        // Default behavior: always prefetch CLI packages for update notifications
        return true;
    }
 
    private static bool IsRuntimeOnlyCommand(BaseCommand command)
    {
        var commandName = command.Name;
        return commandName is "run" or "publish" or "deploy";
    }
}