File: VersionChecking\VersionFetcher.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text;
using System.Text.Json;
using Aspire.Hosting.Dcp.Process;
using Microsoft.Extensions.Logging;
using Semver;
 
namespace Aspire.Hosting.VersionChecking;
 
internal sealed class VersionFetcher : IVersionFetcher
{
    private readonly ILogger<VersionFetcher> _logger;
 
    public VersionFetcher(ILogger<VersionFetcher> logger)
    {
        _logger = logger;
    }
 
    public async Task<SemVersion?> TryFetchLatestVersionAsync(string appHostDirectory, CancellationToken cancellationToken)
    {
        var outputJson = new StringBuilder();
        var spec = new ProcessSpec("dotnet")
        {
            Arguments = "package search Aspire.Hosting.AppHost --format json",
            ThrowOnNonZeroReturnCode = false,
            InheritEnv = true,
            OnOutputData = output =>
            {
                outputJson.Append(output);
                _logger.LogDebug("dotnet (stdout): {Output}", output);
            },
            OnErrorData = error =>
            {
                _logger.LogDebug("dotnet (stderr): {Error}", error);
            },
            WorkingDirectory = appHostDirectory
        };
 
        _logger.LogDebug("Running dotnet CLI to check for latest version with arguments: {ArgumentList}", spec.Arguments);
        var (pendingProcessResult, processDisposable) = ProcessUtil.Run(spec);
 
        await using (processDisposable)
        {
            var processResult = await pendingProcessResult
                .WaitAsync(cancellationToken)
                .ConfigureAwait(false);
 
            if (processResult.ExitCode != 0)
            {
                _logger.LogDebug("The dotnet CLI call to check for latest version failed with exit code {ExitCode}.", processResult.ExitCode);
                return null;
            }
        }
 
        return GetLatestVersion(outputJson.ToString());
    }
 
    internal static SemVersion? GetLatestVersion(string outputJson)
    {
        var packages = ParseOutput(outputJson);
        var versions = new List<SemVersion>();
        foreach (var package in packages)
        {
            if (SemVersion.TryParse(package.LatestVersion, out var version) && !version.IsPrerelease)
            {
                versions.Add(version);
            }
        }
 
        return versions.OrderDescending(SemVersion.PrecedenceComparer).FirstOrDefault();
    }
 
    private static List<Package> ParseOutput(string outputJson)
    {
        var packages = new List<Package>();
 
        using var document = JsonDocument.Parse(outputJson);
        var root = document.RootElement;
 
        if (root.TryGetProperty("searchResult", out var searchResults))
        {
            foreach (var result in searchResults.EnumerateArray())
            {
                if (result.TryGetProperty("packages", out var packagesArray))
                {
                    foreach (var pkg in packagesArray.EnumerateArray())
                    {
                        var id = pkg.GetProperty("id").GetString();
                        var latestVersion = pkg.GetProperty("latestVersion").GetString();
                        if (id != null && latestVersion != null)
                        {
                            packages.Add(new Package(id, latestVersion));
                        }
                    }
                }
            }
        }
 
        return packages;
    }
}