File: ApplicationModel\EndpointReference.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 System.Globalization;
 
namespace Aspire.Hosting.ApplicationModel;
 
/// <summary>
/// Represents an endpoint reference for a resource with endpoints.
/// </summary>
public sealed class EndpointReference : IManifestExpressionProvider, IValueProvider, IValueWithReferences
{
    // A reference to the endpoint annotation if it exists.
    private EndpointAnnotation? _endpointAnnotation;
    private bool? _isAllocated;
 
    // TODO: Expose this
    internal EndpointAnnotation EndpointAnnotation => GetEndpointAnnotation() ?? throw new InvalidOperationException($"The endpoint `{EndpointName}` is not defined for the resource `{Resource.Name}`.");
 
    /// <summary>
    /// Gets the resource owner of the endpoint reference.
    /// </summary>
    public IResourceWithEndpoints Resource { get; }
 
    IEnumerable<object> IValueWithReferences.References => [Resource];
 
    /// <summary>
    /// Gets the name of the endpoint associated with the endpoint reference.
    /// </summary>
    public string EndpointName { get; }
 
    /// <summary>
    /// Gets a value indicating whether the endpoint is allocated.
    /// </summary>
    public bool IsAllocated => _isAllocated ??= GetAllocatedEndpoint() is not null;
 
    /// <summary>
    /// Gets a value indicating whether the endpoint exists.
    /// </summary>
    public bool Exists => GetEndpointAnnotation() is not null;
 
    string IManifestExpressionProvider.ValueExpression => GetExpression();
 
    ValueTask<string?> IValueProvider.GetValueAsync(CancellationToken cancellationToken) => new(Url);
 
    /// <summary>
    /// Gets the specified property expression of the endpoint. Defaults to the URL if no property is specified.
    /// </summary>
    internal string GetExpression(EndpointProperty property = EndpointProperty.Url)
    {
        var prop = property switch
        {
            EndpointProperty.Url => "url",
            EndpointProperty.Host or EndpointProperty.IPV4Host => "host",
            EndpointProperty.Port => "port",
            EndpointProperty.Scheme => "scheme",
            EndpointProperty.TargetPort => "targetPort",
            _ => throw new InvalidOperationException($"The property '{property}' is not supported for the endpoint '{EndpointName}'.")
        };
 
        return $"{{{Resource.Name}.bindings.{EndpointName}.{prop}}}";
    }
 
    /// <summary>
    /// Gets the specified property expression of the endpoint. Defaults to the URL if no property is specified.
    /// </summary>
    /// <param name="property">The <see cref="EndpointProperty"/> enum value to use in the reference.</param>
    /// <returns>An <see cref="EndpointReferenceExpression"/> representing the specified <see cref="EndpointProperty"/>.</returns>
    public EndpointReferenceExpression Property(EndpointProperty property)
    {
        return new(this, property);
    }
 
    /// <summary>
    /// Gets the port for this endpoint.
    /// </summary>
    public int Port => AllocatedEndpoint.Port;
 
    /// <summary>
    /// Gets the target port for this endpoint. If the port is dynamically allocated, this will return <see langword="null"/>.
    /// </summary>
    public int? TargetPort => EndpointAnnotation.TargetPort;
 
    /// <summary>
    /// Gets the host for this endpoint.
    /// </summary>
    public string Host => AllocatedEndpoint.Address ?? "localhost";
 
    /// <summary>
    /// Gets the container host for this endpoint.
    /// </summary>
    public string ContainerHost => AllocatedEndpoint.ContainerHostAddress ?? throw new InvalidOperationException($"The endpoint \"{EndpointName}\" has no associated container host name.");
 
    /// <summary>
    /// Gets the scheme for this endpoint.
    /// </summary>
    public string Scheme => EndpointAnnotation.UriScheme;
 
    /// <summary>
    /// Gets the URL for this endpoint.
    /// </summary>
    public string Url => AllocatedEndpoint.UriString;
 
    internal AllocatedEndpoint AllocatedEndpoint =>
        GetAllocatedEndpoint()
        ?? throw new InvalidOperationException($"The endpoint `{EndpointName}` is not allocated for the resource `{Resource.Name}`.");
 
    private EndpointAnnotation? GetEndpointAnnotation() =>
        _endpointAnnotation ??= Resource.Annotations.OfType<EndpointAnnotation>().SingleOrDefault(a => StringComparers.EndpointAnnotationName.Equals(a.Name, EndpointName));
 
    private AllocatedEndpoint? GetAllocatedEndpoint() => GetEndpointAnnotation()?.AllocatedEndpoint;
 
    /// <summary>
    /// Creates a new instance of <see cref="EndpointReference"/> with the specified endpoint name.
    /// </summary>
    /// <param name="owner">The resource with endpoints that owns the endpoint reference.</param>
    /// <param name="endpoint">The endpoint annotation.</param>
    public EndpointReference(IResourceWithEndpoints owner, EndpointAnnotation endpoint)
    {
        ArgumentNullException.ThrowIfNull(owner);
        ArgumentNullException.ThrowIfNull(endpoint);
 
        Resource = owner;
        EndpointName = endpoint.Name;
        _endpointAnnotation = endpoint;
    }
 
    /// <summary>
    /// Creates a new instance of <see cref="EndpointReference"/> with the specified endpoint name.
    /// </summary>
    /// <param name="owner">The resource with endpoints that owns the endpoint reference.</param>
    /// <param name="endpointName">The name of the endpoint.</param>
    public EndpointReference(IResourceWithEndpoints owner, string endpointName)
    {
        ArgumentNullException.ThrowIfNull(owner);
        ArgumentNullException.ThrowIfNull(endpointName);
 
        Resource = owner;
        EndpointName = endpointName;
    }
}
 
/// <summary>
/// Represents a property expression for an endpoint reference.
/// </summary>
/// <param name="endpointReference">The endpoint reference.</param>
/// <param name="property">The property of the endpoint.</param>
public class EndpointReferenceExpression(EndpointReference endpointReference, EndpointProperty property) : IManifestExpressionProvider, IValueProvider, IValueWithReferences
{
    /// <summary>
    /// Gets the <see cref="EndpointReference"/>.
    /// </summary>
    public EndpointReference Endpoint { get; } = endpointReference ?? throw new ArgumentNullException(nameof(endpointReference));
 
    /// <summary>
    /// Gets the <see cref="EndpointProperty"/> for the property expression.
    /// </summary>
    public EndpointProperty Property { get; } = property;
 
    /// <summary>
    /// Gets the expression of the property of the endpoint.
    /// </summary>
    public string ValueExpression =>
        Endpoint.GetExpression(Property);
 
    /// <summary>
    /// Gets the value of the property of the endpoint.
    /// </summary>
    /// <param name="cancellationToken">A <see cref="CancellationToken"/>.</param>
    /// <returns>A <see cref="string"/> containing the selected <see cref="EndpointProperty"/> value.</returns>
    /// <exception cref="InvalidOperationException">Throws when the selected <see cref="EndpointProperty"/> enumeration is not known.</exception>
    public ValueTask<string?> GetValueAsync(CancellationToken cancellationToken) => Property switch
    {
        EndpointProperty.Url => new(Endpoint.Url),
        EndpointProperty.Host => new(Endpoint.Host),
        EndpointProperty.IPV4Host => new("127.0.0.1"),
        EndpointProperty.Port => new(Endpoint.Port.ToString(CultureInfo.InvariantCulture)),
        EndpointProperty.Scheme => new(Endpoint.Scheme),
        EndpointProperty.TargetPort => new(ComputeTargetPort()),
        _ => throw new InvalidOperationException($"The property '{Property}' is not supported for the endpoint '{Endpoint.EndpointName}'.")
    };
 
    private string? ComputeTargetPort()
    {
        // We have a target port, so we can return it directly.
        if (Endpoint.TargetPort is int port)
        {
            return port.ToString(CultureInfo.InvariantCulture);
        }
 
        // There is no way to resolve the value of the target port until runtime. Even then, replicas make this very complex because
        // the target port is not known until the replica is allocated.
        // Instead, we return an expression that will be resolved at runtime by the orchestrator.
        return Endpoint.AllocatedEndpoint.TargetPortExpression
            ?? throw new InvalidOperationException("The endpoint does not have an associated TargetPortExpression from the orchestrator.");
    }
 
    IEnumerable<object> IValueWithReferences.References => [Endpoint];
}
 
/// <summary>
/// Represents the properties of an endpoint that can be referenced.
/// </summary>
public enum EndpointProperty
{
    /// <summary>
    /// The entire URL of the endpoint.
    /// </summary>
    Url,
    /// <summary>
    /// The host of the endpoint.
    /// </summary>
    Host,
    /// <summary>
    /// The IPv4 address of the endpoint.
    /// </summary>
    IPV4Host,
    /// <summary>
    /// The port of the endpoint.
    /// </summary>
    Port,
    /// <summary>
    /// The scheme of the endpoint.
    /// </summary>
    Scheme,
    /// <summary>
    /// The target port of the endpoint.
    /// </summary>
    TargetPort,
}