File: Backchannel\DashboardUrlsHelper.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 Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Dashboard;
using Aspire.Hosting.Devcontainers.Codespaces;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
 
namespace Aspire.Hosting.Backchannel;
 
/// <summary>
/// Helper class for retrieving dashboard URLs with login tokens.
/// </summary>
internal static class DashboardUrlsHelper
{
    /// <summary>
    /// Gets the dashboard URLs including the login token.
    /// Waits for the dashboard to become healthy before returning.
    /// </summary>
    /// <param name="serviceProvider">The service provider.</param>
    /// <param name="logger">The logger for diagnostic output.</param>
    /// <param name="cancellationToken">A cancellation token.</param>
    /// <returns>The Dashboard URLs state including health and login URLs.</returns>
    public static async Task<DashboardUrlsState> GetDashboardUrlsAsync(
        IServiceProvider serviceProvider,
        ILogger logger,
        CancellationToken cancellationToken = default)
    {
        var resourceNotificationService = serviceProvider.GetRequiredService<ResourceNotificationService>();
 
        // Wait for the dashboard to be healthy before returning the URL. This is to ensure that the
        // endpoint for the resource is available and the dashboard is ready to be used. This helps
        // avoid some issues with port forwarding in devcontainer/codespaces scenarios.
        try
        {
            await resourceNotificationService.WaitForResourceHealthyAsync(
                KnownResourceNames.AspireDashboard,
                WaitBehavior.StopOnResourceUnavailable,
                cancellationToken).ConfigureAwait(false);
        }
        catch (DistributedApplicationException ex)
        {
            logger.LogWarning(ex, "An error occurred while waiting for the Aspire Dashboard to become healthy.");
 
            return new DashboardUrlsState
            {
                DashboardHealthy = false,
                BaseUrlWithLoginToken = null,
                CodespacesUrlWithLoginToken = null
            };
        }
 
        var dashboardOptions = serviceProvider.GetService<IOptions<DashboardOptions>>();
 
        if (dashboardOptions is null)
        {
            logger.LogWarning("Dashboard options not found.");
            throw new InvalidOperationException("Dashboard options not found.");
        }
 
        // Get the actual allocated URL from the dashboard resource endpoint
        var appModel = serviceProvider.GetService<DistributedApplicationModel>();
        string? dashboardUrl = null;
 
        if (appModel?.Resources.SingleOrDefault(r => StringComparers.ResourceName.Equals(r.Name, KnownResourceNames.AspireDashboard)) is IResourceWithEndpoints dashboardResource)
        {
            // Try HTTPS first, then HTTP
            var httpsEndpoint = dashboardResource.GetEndpoint("https");
            var httpEndpoint = dashboardResource.GetEndpoint("http");
 
            var endpoint = httpsEndpoint.Exists ? httpsEndpoint : httpEndpoint;
            if (endpoint.Exists)
            {
                dashboardUrl = await endpoint.GetValueAsync(cancellationToken).ConfigureAwait(false);
            }
        }
 
        // Fall back to configured URL if we couldn't get it from the resource
        if (string.IsNullOrEmpty(dashboardUrl))
        {
            if (!StringUtils.TryGetUriFromDelimitedString(dashboardOptions.Value.DashboardUrl, ";", out var dashboardUri))
            {
                logger.LogWarning("Dashboard URL could not be parsed from dashboard options.");
                throw new InvalidOperationException("Dashboard URL could not be parsed from dashboard options.");
            }
            dashboardUrl = dashboardUri.GetLeftPart(UriPartial.Authority);
        }
 
        var codespacesUrlRewriter = serviceProvider.GetService<CodespacesUrlRewriter>();
 
        var baseUrlWithLoginToken = $"{dashboardUrl.TrimEnd('/')}/login?t={dashboardOptions.Value.DashboardToken}";
        var codespacesUrlWithLoginToken = codespacesUrlRewriter?.RewriteUrl(baseUrlWithLoginToken);
 
        if (baseUrlWithLoginToken == codespacesUrlWithLoginToken)
        {
            return new DashboardUrlsState
            {
                DashboardHealthy = true,
                BaseUrlWithLoginToken = baseUrlWithLoginToken,
                CodespacesUrlWithLoginToken = null
            };
        }
        else
        {
            return new DashboardUrlsState
            {
                DashboardHealthy = true,
                BaseUrlWithLoginToken = baseUrlWithLoginToken,
                CodespacesUrlWithLoginToken = codespacesUrlWithLoginToken
            };
        }
    }
}