File: Utils\EnvironmentChecker\DotNetSdkCheck.cs
Web Access
Project: src\src\Aspire.Cli\Aspire.Cli.Tool.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.DotNet;
using Aspire.Cli.Projects;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Cli.Utils.EnvironmentChecker;
 
/// <summary>
/// Checks if the .NET SDK is installed and meets the minimum version requirement.
/// </summary>
/// <remarks>
/// This check is skipped when the detected AppHost is a non-.NET project (e.g., TypeScript, Python, Go),
/// since .NET SDK is not required for polyglot scenarios.
/// </remarks>
internal sealed class DotNetSdkCheck(
    IDotNetSdkInstaller sdkInstaller,
    IProjectLocator projectLocator,
    ILanguageDiscovery languageDiscovery,
    CliExecutionContext executionContext,
    ILogger<DotNetSdkCheck> logger) : IEnvironmentCheck
{
    public int Order => 30; // File system check - slightly more expensive
 
    public async Task<IReadOnlyList<EnvironmentCheckResult>> CheckAsync(CancellationToken cancellationToken = default)
    {
        try
        {
            if (!await IsDotNetAppHostAsync(cancellationToken))
            {
                logger.LogDebug("Skipping .NET SDK check because no .NET AppHost was detected");
                return [];
            }
 
            var (success, highestVersion, minimumRequiredVersion) = await sdkInstaller.CheckAsync(cancellationToken);
 
            if (!success)
            {
                // Parse major version from string like "10.0.100" -> 10
                var majorVersion = 10;
                if (Version.TryParse(minimumRequiredVersion, out var parsedVersion))
                {
                    majorVersion = parsedVersion.Major;
                }
 
                return [new EnvironmentCheckResult
                {
                    Category = "sdk",
                    Name = "dotnet-sdk",
                    Status = EnvironmentCheckStatus.Fail,
                    Message = highestVersion is null
                        ? ".NET SDK not found"
                        : $".NET {highestVersion} found but {minimumRequiredVersion} or higher required",
                    Fix = $"Download .NET SDK from: https://dotnet.microsoft.com/download/dotnet/{majorVersion}.0",
                    Link = $"https://dotnet.microsoft.com/download/dotnet/{majorVersion}.0"
                }];
            }
 
            var architecture = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture.ToString().ToLowerInvariant();
 
            return [new EnvironmentCheckResult
            {
                Category = "sdk",
                Name = "dotnet-sdk",
                Status = EnvironmentCheckStatus.Pass,
                Message = $".NET {highestVersion} installed ({architecture})"
            }];
        }
        catch (Exception ex)
        {
            logger.LogDebug(ex, "Error checking .NET SDK");
            return [new EnvironmentCheckResult
            {
                Category = "sdk",
                Name = "dotnet-sdk",
                Status = EnvironmentCheckStatus.Fail,
                Message = "Error checking .NET SDK",
                Details = ex.Message
            }];
        }
    }
 
    /// <summary>
    /// Determines whether a .NET AppHost is positively detected, meaning the .NET SDK check should run.
    /// Only returns <c>true</c> when a settings file is found and the apphost is a .NET project.
    /// When no settings file exists or the apphost is non-.NET, the check is skipped.
    /// </summary>
    private async Task<bool> IsDotNetAppHostAsync(CancellationToken cancellationToken)
    {
        try
        {
            // Use the silent settings-only lookup to find the apphost without
            // emitting interaction output or performing recursive filesystem scans.
            var appHostFile = await projectLocator.GetAppHostFromSettingsAsync(cancellationToken);
 
            if (appHostFile is not null && languageDiscovery.GetLanguageByFile(appHostFile) is { } language)
            {
                return language.LanguageId.Value.Equals(KnownLanguageId.CSharp, StringComparison.OrdinalIgnoreCase);
            }
 
            // No apphost configured in settings. Look for possible .NET app hosts on file system (projects or apphost.cs)
            // This doesn't guarantee the apphost is .NET, but it's a signal that it might be and worth checking for .NET SDK.
            var csharp = languageDiscovery.GetLanguageById(KnownLanguageId.CSharp);
            if (csharp is null)
            {
                return false;
            }
 
            // Scan file system directly instead of using ProjectLocator for performance.
            // Limit recursive scan to avoid expensive file system operations in large workspaces.
            // We don't want a complete list of all possible project files, just a quick signal that a .NET apphost is probably present.
            var match = FileSystemHelper.FindFirstFile(executionContext.WorkingDirectory.FullName, recurseLimit: 5, csharp.DetectionPatterns);
            return match is not null;
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch (Exception ex)
        {
            logger.LogDebug(ex, "Error detecting AppHost language, skipping .NET SDK check");
            return false;
        }
    }
}