File: src\Shared\DashboardImage.cs
Web Access
Project: src\src\Aspire.Hosting.Kubernetes\Aspire.Hosting.Kubernetes.csproj (Aspire.Hosting.Kubernetes)
// 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 System.Reflection;
 
namespace Aspire.Shared;
 
/// <summary>
/// Resolves the Aspire Dashboard container image reference used by the Docker Compose and
/// Kubernetes publishers when they inject a dashboard into generated deployment artifacts.
/// </summary>
internal static class DashboardImage
{
    /// <summary>
    /// The Aspire Dashboard container image (without a tag), published to the .NET nightly registry.
    /// See <see href="https://mcr.microsoft.com/artifact/mar/dotnet/nightly/aspire-dashboard/about"/>.
    /// </summary>
    public const string Name = "mcr.microsoft.com/dotnet/nightly/aspire-dashboard";
 
    /// <summary>
    /// Resolves the tag to pin the dashboard image to, derived from the running Aspire product
    /// version's <c>major.minor</c> (for example <c>13.5</c>).
    /// </summary>
    /// <remarks>
    /// The publishers previously emitted the image without a tag, which Docker and Kubernetes both
    /// resolve to the mutable <c>:latest</c> tag. That made generated manifests non-reproducible and
    /// let the dashboard drift away from the app's Aspire version. Pinning to <c>major.minor</c> keeps
    /// the dashboard on the same Aspire line that generated the manifest and always resolves to a tag
    /// that exists on the registry — including for prerelease/CI builds, where a full
    /// <c>major.minor.patch-prerelease</c> tag is not published.
    /// </remarks>
    public static string ResolveTag()
        => ResolveTag(typeof(DashboardImage).Assembly);
 
    internal static string ResolveTag(Assembly assembly)
    {
        // The product version is stamped into AssemblyInformationalVersion at build time, e.g.:
        //   "13.5.0-preview.1.25111.1+ad18db0213e9db8209bca0feb83fc801f34634f5"
        // The assembly version (e.g. "13.5.0.0") is used as a fallback when it is unavailable.
        var informationalVersion = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
 
        return ResolveTag(informationalVersion, assembly.GetName().Version?.ToString());
    }
 
    internal static string ResolveTag(string? informationalVersion, string? assemblyVersion)
    {
        if (TryGetMajorMinor(informationalVersion, out var majorMinor) ||
            TryGetMajorMinor(assemblyVersion, out majorMinor))
        {
            return majorMinor;
        }
 
        // Defensive only: shipped assemblies always carry a version, so this guards against a stripped
        // version attribute. Preserve the historical ":latest" behavior rather than emitting an
        // invalid tag.
        return "latest";
    }
 
    private static bool TryGetMajorMinor(string? version, out string majorMinor)
    {
        majorMinor = string.Empty;
 
        if (string.IsNullOrEmpty(version))
        {
            return false;
        }
 
        // Strip any prerelease label ("-preview...") and build metadata ("+sha") before splitting so
        // that both SemVer informational versions and 4-part assembly versions ("13.5.0.0") parse the
        // same way.
        var core = version.Split('-', '+')[0];
        var segments = core.Split('.');
 
        if (segments.Length < 2 ||
            !int.TryParse(segments[0], NumberStyles.None, CultureInfo.InvariantCulture, out var major) ||
            !int.TryParse(segments[1], NumberStyles.None, CultureInfo.InvariantCulture, out var minor))
        {
            return false;
        }
 
        majorMinor = string.Create(CultureInfo.InvariantCulture, $"{major}.{minor}");
        return true;
    }
}