Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/EndpointUpdateContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net.Sockets;

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Provides a mutable callback context for updating an endpoint in polyglot app hosts.
/// </summary>
[AspireExport(ExposeProperties = true)]
internal sealed class EndpointUpdateContext(EndpointAnnotation endpointAnnotation)
{
private readonly EndpointAnnotation _endpointAnnotation = endpointAnnotation ?? throw new ArgumentNullException(nameof(endpointAnnotation));

/// <summary>
/// Gets the endpoint name.
/// </summary>
public string Name => _endpointAnnotation.Name;

/// <summary>
/// Gets or sets the network protocol.
/// </summary>
public ProtocolType Protocol
{
get => _endpointAnnotation.Protocol;
set => _endpointAnnotation.Protocol = value;
}

/// <summary>
/// Gets or sets the desired host port.
/// </summary>
public int? Port
{
get => _endpointAnnotation.Port;
set => _endpointAnnotation.Port = value;
}

/// <summary>
/// Gets or sets the target port.
/// </summary>
public int? TargetPort
{
get => _endpointAnnotation.TargetPort;
set => _endpointAnnotation.TargetPort = value;
}

/// <summary>
/// Gets or sets the URI scheme.
/// </summary>
public string UriScheme
{
get => _endpointAnnotation.UriScheme;
set => _endpointAnnotation.UriScheme = value;
}

/// <summary>
/// Gets or sets the target host.
/// </summary>
public string TargetHost
{
get => _endpointAnnotation.TargetHost;
set => _endpointAnnotation.TargetHost = value;
}

/// <summary>
/// Gets or sets the transport.
/// </summary>
public string Transport
{
get => _endpointAnnotation.Transport;
set => _endpointAnnotation.Transport = value;
}

/// <summary>
/// Gets or sets a value indicating whether the endpoint is external.
/// </summary>
public bool IsExternal
{
get => _endpointAnnotation.IsExternal;
set => _endpointAnnotation.IsExternal = value;
}

/// <summary>
/// Gets or sets a value indicating whether the endpoint is proxied.
/// </summary>
public bool IsProxied
{
get => _endpointAnnotation.IsProxied;
set => _endpointAnnotation.IsProxied = value;
}

/// <summary>
/// Gets or sets a value indicating whether the endpoint is excluded from the default reference set.
/// </summary>
public bool ExcludeReferenceEndpoint
{
get => _endpointAnnotation.ExcludeReferenceEndpoint;
set => _endpointAnnotation.ExcludeReferenceEndpoint = value;
}

/// <summary>
/// Gets or sets a value indicating whether TLS is enabled.
/// </summary>
public bool TlsEnabled
{
get => _endpointAnnotation.TlsEnabled;
set => _endpointAnnotation.TlsEnabled = value;
}
}
32 changes: 29 additions & 3 deletions src/Aspire.Hosting/ResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1210,9 +1210,9 @@ private static void ApplyEndpoints<T>(this IResourceBuilder<T> builder, IResourc
/// });
/// </code>
/// </example>
/// <para>This method is not available in polyglot app hosts. Use the parameter-based overload instead.</para>
/// <para>This method is not available in polyglot app hosts. Use the callback-based endpoint mutation export instead.</para>
/// </remarks>
[AspireExportIgnore(Reason = "EndpointAnnotation has read-only properties AllocatedEndpointSnapshot and AllAllocatedEndpoints that are not ATS-compatible. Callback-free variant is exported.")]
[AspireExportIgnore(Reason = "Polyglot app hosts use the internal withEndpointCallback export, which exposes EndpointUpdateContext instead of EndpointAnnotation.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "<Pending>")]
public static IResourceBuilder<T> WithEndpoint<T>(this IResourceBuilder<T> builder, [EndpointName] string endpointName, Action<EndpointAnnotation> callback, bool createIfNotExists = true) where T : IResourceWithEndpoints
{
Expand Down Expand Up @@ -1247,6 +1247,33 @@ public static IResourceBuilder<T> WithEndpoint<T>(this IResourceBuilder<T> build
return builder;
}

[AspireExport(Description = "Updates a named endpoint via callback")]
internal static IResourceBuilder<T> WithEndpointCallback<T>(this IResourceBuilder<T> builder, [EndpointName] string endpointName, Action<EndpointUpdateContext> callback, bool createIfNotExists = true) where T : IResourceWithEndpoints
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(endpointName);
ArgumentNullException.ThrowIfNull(callback);

return builder.WithEndpoint(endpointName, endpoint => callback(new EndpointUpdateContext(endpoint)), createIfNotExists);
}

[AspireExport(Description = "Updates an HTTP endpoint via callback")]
internal static IResourceBuilder<T> WithHttpEndpointCallback<T>(this IResourceBuilder<T> builder, Action<EndpointUpdateContext> callback, [EndpointName] string? name = null, bool createIfNotExists = true) where T : IResourceWithEndpoints
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also WithHttpsEndpointCallback?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's WithEndpointCallback and WithHttpEndpointCallback here but no WithHttpsEndpointCallback. The public API has the symmetric trio (WithEndpoint / WithHttpEndpoint / WithHttpsEndpoint), so the callback surface should match.

The polyglot samples work around this by calling withEndpointCallback("callback-https", ...), but when createIfNotExists is true that creates a bare TCP endpoint (via WithEndpoint(endpointName, ...)) rather than one with scheme: "https" — so the created endpoint would have the wrong URI scheme and transport.

{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(callback);

var endpointName = name ?? "http";

if (createIfNotExists &&
!builder.Resource.Annotations.OfType<EndpointAnnotation>().Any(endpoint => string.Equals(endpoint.Name, endpointName, StringComparisons.EndpointAnnotationName)))
{
builder.WithHttpEndpoint(name: endpointName);
}

return builder.WithEndpoint(endpointName, endpoint => callback(new EndpointUpdateContext(endpoint)), createIfNotExists: false);
}

/// <summary>
/// Exposes an endpoint on a resource. A reference to this endpoint can be retrieved using <see cref="ResourceBuilderExtensions.GetEndpoint{T}(IResourceBuilder{T}, string, NetworkIdentifier)"/>.
/// The endpoint name will be the scheme name if not specified.
Expand Down Expand Up @@ -1394,7 +1421,6 @@ public static IResourceBuilder<T> WithExternalHttpEndpoints<T>(this IResourceBui

return builder;
}

/// <summary>
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The blank line between WithExternalHttpEndpoints and the GetEndpoint XML doc comment was removed — appears to be an unrelated formatting change.

/// Gets an <see cref="EndpointReference"/> by name from the resource. These endpoints are declared either using <see cref="WithEndpoint{T}(IResourceBuilder{T}, int?, int?, string?, string?, string?, bool, bool?, ProtocolType?)"/> or by launch settings (for project resources).
/// The <see cref="EndpointReference"/> can be used to resolve the address of the endpoint in <see cref="WithEnvironment{T}(IResourceBuilder{T}, Action{EnvironmentCallbackContext})"/>.
Expand Down
Loading
Loading