File: Utils\EnvironmentChecker\PendingMigrationsCheck.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.Migrations;
using Aspire.Cli.Resources;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Cli.Utils.EnvironmentChecker;
 
/// <summary>
/// Surfaces a non-blocking <c>aspire doctor</c> warning for each <see cref="IMigration"/> that
/// applies to the current project, nudging the user toward running <c>aspire update --migrate</c>.
/// </summary>
/// <remarks>
/// This is the read-only half of the migration system: it shares the exact same
/// <see cref="IMigration.DetectAsync(MigrationContext, CancellationToken)"/> detection that <c>aspire update --migrate</c> uses to apply changes,
/// so the two can never drift. Any new migration registered in DI automatically shows up here with
/// no changes to this check.
/// See: https://github.com/microsoft/aspire/issues/17842
/// </remarks>
internal sealed class PendingMigrationsCheck : IEnvironmentCheck
{
    private readonly IEnumerable<IMigration> _migrations;
    private readonly ILogger<PendingMigrationsCheck> _logger;
 
    public PendingMigrationsCheck(
        IEnumerable<IMigration> migrations,
        ILogger<PendingMigrationsCheck> logger)
    {
        _migrations = migrations;
        _logger = logger;
    }
 
    /// <inheritdoc />
    // Run last, after the deprecated agent config (100) and legacy settings (101) checks, so the
    // migration nudges appear together at the end of the deprecated/legacy section of the report.
    public int Order => 102;
 
    /// <inheritdoc />
    public async Task<IReadOnlyList<EnvironmentCheckResult>> CheckAsync(CancellationToken cancellationToken = default)
    {
        var results = new List<EnvironmentCheckResult>();
 
        foreach (var migration in _migrations.OrderBy(m => m.Order))
        {
            MigrationDescriptor? descriptor;
            try
            {
                descriptor = await migration.DetectAsync(MigrationContext.CurrentDirectory, cancellationToken);
            }
            catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
            {
                throw;
            }
            catch (Exception ex)
            {
                // A broken migration provider must not fail the whole doctor run.
                _logger.LogDebug(ex, "Migration '{MigrationId}' detection failed", migration.Id);
                continue;
            }
 
            if (descriptor is null)
            {
                continue;
            }
 
            results.Add(new EnvironmentCheckResult
            {
                Category = EnvironmentCheckCategories.AppHost,
                Name = migration.Id,
                Status = EnvironmentCheckStatus.Warning,
                Message = descriptor.Detail,
                Fix = DoctorCommandStrings.PendingMigrationFix,
                Metadata = descriptor.Metadata
            });
        }
 
        return results;
    }
}