File: src\Shared\ComputeEnvironmentEndpointResolver.cs
Web Access
Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics.CodeAnalysis;
using Aspire.Hosting.ApplicationModel;
 
namespace Aspire.Hosting;
 
/// <summary>
/// Helper for resolving endpoint references that point at a resource deployed to a different
/// compute environment than the one currently generating deployment artifacts.
/// </summary>
/// <remarks>
/// Compute environment publishers (App Service, Azure Container Apps, Kubernetes) resolve
/// endpoint references against their own local endpoint map. When a resource references an
/// endpoint owned by a resource deployed to a different compute environment (for example a
/// Foundry hosted agent deployed to an <c>AzureCognitiveServicesProjectResource</c>), the
/// endpoint is not present in the local map and the lookup fails. In that case the owning
/// compute environment knows how to express the endpoint property, so we delegate to it.
/// This mirrors the inverse-direction logic used by Foundry hosted agents when they reference
/// endpoints owned by App Service/ACA/Kubernetes resources.
/// </remarks>
internal static class ComputeEnvironmentEndpointResolver
{
    /// <summary>
    /// Attempts to produce a <see cref="ReferenceExpression"/> for an endpoint's URL by
    /// delegating to the compute environment that owns the endpoint's resource, when that
    /// environment is different from the publisher's current compute environment(s).
    /// </summary>
    /// <param name="endpointReference">The endpoint reference to resolve.</param>
    /// <param name="currentComputeEnvironments">
    /// The compute environment(s) the current publisher is generating artifacts for. When the
    /// endpoint's owning resource deploys to one of these, resolution is left to the local
    /// endpoint map and this method returns <see langword="false"/>.
    /// </param>
    /// <param name="expression">
    /// When this method returns <see langword="true"/>, contains the delegated reference expression.
    /// </param>
    /// <returns>
    /// <see langword="true"/> if the endpoint is owned by a different compute environment and a
    /// delegated expression was produced; otherwise <see langword="false"/>.
    /// </returns>
    public static bool TryGetCrossEnvironmentEndpointExpression(
        EndpointReference endpointReference,
        IReadOnlyList<IComputeEnvironmentResource?> currentComputeEnvironments,
        [NotNullWhen(true)] out ReferenceExpression? expression)
    {
        ArgumentNullException.ThrowIfNull(endpointReference);
 
        return TryGetCrossEnvironmentEndpointExpression(
            endpointReference.Property(EndpointProperty.Url),
            currentComputeEnvironments,
            out expression);
    }
 
    /// <summary>
    /// Attempts to produce a <see cref="ReferenceExpression"/> for an endpoint property by
    /// delegating to the compute environment that owns the endpoint's resource, when that
    /// environment is different from the publisher's current compute environment(s).
    /// </summary>
    /// <param name="endpointReferenceExpression">The endpoint reference expression to resolve.</param>
    /// <param name="currentComputeEnvironments">
    /// The compute environment(s) the current publisher is generating artifacts for. When the
    /// endpoint's owning resource deploys to one of these, resolution is left to the local
    /// endpoint map and this method returns <see langword="false"/>.
    /// </param>
    /// <param name="expression">
    /// When this method returns <see langword="true"/>, contains the delegated reference expression.
    /// </param>
    /// <returns>
    /// <see langword="true"/> if the endpoint is owned by a different compute environment and a
    /// delegated expression was produced; otherwise <see langword="false"/>.
    /// </returns>
    public static bool TryGetCrossEnvironmentEndpointExpression(
        EndpointReferenceExpression endpointReferenceExpression,
        IReadOnlyList<IComputeEnvironmentResource?> currentComputeEnvironments,
        [NotNullWhen(true)] out ReferenceExpression? expression)
    {
        ArgumentNullException.ThrowIfNull(endpointReferenceExpression);
        ArgumentNullException.ThrowIfNull(currentComputeEnvironments);
 
        expression = null;
 
        var owningResource = endpointReferenceExpression.Endpoint.Resource;
 
        // Resolve the compute environment the owning resource deploys to. A plain resource that is
        // not deployed anywhere has none, so there is nothing to delegate to and the local lookup
        // handles it.
        if (!TryGetEffectiveComputeEnvironment(owningResource, out var owningComputeEnvironment))
        {
            return false;
        }
 
        // If the owning resource deploys to one of the current publisher's compute environments, the
        // endpoint lives in the local endpoint map. Leave resolution to the existing local lookup so
        // generated artifacts (bicep parameters, helm values, etc.) are unchanged.
        foreach (var current in currentComputeEnvironments)
        {
            if (ReferenceEquals(current, owningComputeEnvironment))
            {
                return false;
            }
        }
 
#pragma warning disable ASPIRECOMPUTE002 // Experimental: compute environment endpoint expression
        expression = owningComputeEnvironment.GetEndpointPropertyExpression(endpointReferenceExpression);
#pragma warning restore ASPIRECOMPUTE002
 
        return true;
    }
 
    /// <summary>
    /// Resolves the compute environment that a resource is deployed to. A resource may be bound to a
    /// compute environment explicitly (via <see cref="ResourceExtensions.GetComputeEnvironment"/>) or
    /// implicitly through its deployment target.
    /// </summary>
    /// <param name="resource">The resource whose compute environment should be resolved.</param>
    /// <param name="computeEnvironment">
    /// When this method returns <see langword="true"/>, contains the owning compute environment.
    /// </param>
    /// <returns>
    /// <see langword="true"/> if a compute environment was resolved; otherwise <see langword="false"/>.
    /// </returns>
    public static bool TryGetEffectiveComputeEnvironment(
        IResource resource,
        [NotNullWhen(true)] out IComputeEnvironmentResource? computeEnvironment)
    {
        ArgumentNullException.ThrowIfNull(resource);
 
        // Prefer an explicit compute environment binding, then fall back to the deployment target's
        // compute environment. This matches how endpoint references are resolved elsewhere
        // (Azure Front Door origins, Foundry hosted agents) so all call sites agree on "where is
        // this resource deployed".
        computeEnvironment = resource.GetComputeEnvironment() ?? resource.GetDeploymentTargetAnnotation()?.ComputeEnvironment;
        return computeEnvironment is not null;
    }
}