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 connection information.
/// </summary>
internal static class DashboardUrlsHelper
{
    private const string McpEndpointName = "mcp";
 
    /// <summary>
    /// Gets all dashboard connection information in a single call.
    /// 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>Complete dashboard connection information.</returns>
    public static async Task<DashboardConnectionInfo> GetDashboardConnectionInfoAsync(
        IServiceProvider serviceProvider,
        ILogger logger,
        CancellationToken cancellationToken = default)
    {
        var resourceNotificationService = serviceProvider.GetRequiredService<ResourceNotificationService>();
 
        // Wait for the dashboard to be healthy
        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 DashboardConnectionInfo.Unhealthy;
        }
 
        var dashboardOptions = serviceProvider.GetService<IOptions<DashboardOptions>>()?.Value;
        if (dashboardOptions is null)
        {
            logger.LogWarning("Dashboard options not found.");
            return DashboardConnectionInfo.Unhealthy;
        }
 
        // Find the dashboard resource and get all endpoints
        var appModel = serviceProvider.GetService<DistributedApplicationModel>();
        var dashboardResource = appModel?.Resources.SingleOrDefault(
            r => StringComparers.ResourceName.Equals(r.Name, KnownResourceNames.AspireDashboard)) as IResourceWithEndpoints;
 
        string? apiBaseUrl = null;
        string? mcpBaseUrl = null;
 
        if (dashboardResource is not null)
        {
            // API endpoint (https or http) - used for Dashboard UI and Telemetry API
            var httpsEndpoint = dashboardResource.GetEndpoint("https");
            var httpEndpoint = dashboardResource.GetEndpoint("http");
            var apiEndpoint = httpsEndpoint.Exists ? httpsEndpoint : httpEndpoint;
            if (apiEndpoint.Exists)
            {
                apiBaseUrl = await EndpointHostHelpers.GetUrlWithTargetHostAsync(apiEndpoint, cancellationToken).ConfigureAwait(false);
            }
 
            // MCP endpoint
            var mcpEndpoint = dashboardResource.GetEndpoint(McpEndpointName);
            if (mcpEndpoint.Exists)
            {
                var mcpUrl = await mcpEndpoint.GetValueAsync(cancellationToken).ConfigureAwait(false);
                if (!string.IsNullOrEmpty(mcpUrl))
                {
                    mcpBaseUrl = $"{mcpUrl}/mcp";
                }
            }
        }
 
        // Fall back to configured URL if we couldn't get it from the resource
        if (string.IsNullOrEmpty(apiBaseUrl))
        {
            if (StringUtils.TryGetUriFromDelimitedString(dashboardOptions.DashboardUrl, ";", out var dashboardUri))
            {
                apiBaseUrl = dashboardUri.GetLeftPart(UriPartial.Authority);
            }
        }
 
        // Build login URLs
        var codespacesUrlRewriter = serviceProvider.GetService<CodespacesUrlRewriter>();
        string? baseUrlWithLoginToken = null;
        string? codespacesUrlWithLoginToken = null;
 
        if (!string.IsNullOrEmpty(apiBaseUrl) && !string.IsNullOrEmpty(dashboardOptions.DashboardToken))
        {
            baseUrlWithLoginToken = $"{apiBaseUrl.TrimEnd('/')}/login?t={dashboardOptions.DashboardToken}";
            var rewrittenUrl = codespacesUrlRewriter?.RewriteUrl(baseUrlWithLoginToken);
            if (rewrittenUrl != baseUrlWithLoginToken)
            {
                codespacesUrlWithLoginToken = rewrittenUrl;
            }
        }
 
        return new DashboardConnectionInfo
        {
            IsHealthy = true,
            ApiBaseUrl = apiBaseUrl,
            ApiToken = dashboardOptions.ApiKey,
            McpBaseUrl = mcpBaseUrl,
            McpApiToken = dashboardOptions.McpApiKey,
            BaseUrlWithLoginToken = baseUrlWithLoginToken,
            CodespacesUrlWithLoginToken = codespacesUrlWithLoginToken
        };
    }
 
    /// <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 info = await GetDashboardConnectionInfoAsync(serviceProvider, logger, cancellationToken).ConfigureAwait(false);
        return new DashboardUrlsState
        {
            DashboardHealthy = info.IsHealthy,
            BaseUrlWithLoginToken = info.BaseUrlWithLoginToken,
            CodespacesUrlWithLoginToken = info.CodespacesUrlWithLoginToken
        };
    }
}
 
/// <summary>
/// Contains all dashboard connection information.
/// </summary>
internal sealed class DashboardConnectionInfo
{
    public static readonly DashboardConnectionInfo Unhealthy = new() { IsHealthy = false };
 
    public bool IsHealthy { get; init; }
    public string? ApiBaseUrl { get; init; }
    public string? ApiToken { get; init; }
    public string? McpBaseUrl { get; init; }
    public string? McpApiToken { get; init; }
    public string? BaseUrlWithLoginToken { get; init; }
    public string? CodespacesUrlWithLoginToken { get; init; }
}