Skip to content
Merged
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
1 change: 1 addition & 0 deletions src/Aspire.Hosting/Dcp/DcpExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1581,6 +1581,7 @@ private static (string, EndpointBindingMode) NormalizeTargetHost(string targetHo
{
null or "" => ("localhost", EndpointBindingMode.SingleAddress), // Default is localhost
var s when string.Equals(s, "localhost", StringComparison.OrdinalIgnoreCase) => ("localhost", EndpointBindingMode.SingleAddress), // Explicitly set to localhost
var s when s.Length > 10 && s.EndsWith(".localhost", StringComparison.OrdinalIgnoreCase) => ("localhost", EndpointBindingMode.SingleAddress), // Explicitly set to localhost when using .localhost subdomain
Copy link

Copilot AI Jul 11, 2025

Choose a reason for hiding this comment

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

The length check (s.Length > 10) is redundant because EndsWith(".localhost") already implies a minimum length. Consider removing the length condition or defining a clear constant for the subdomain suffix to improve readability.

Suggested change
var s when s.Length > 10 && s.EndsWith(".localhost", StringComparison.OrdinalIgnoreCase) => ("localhost", EndpointBindingMode.SingleAddress), // Explicitly set to localhost when using .localhost subdomain
var s when s.EndsWith(".localhost", StringComparison.OrdinalIgnoreCase) => ("localhost", EndpointBindingMode.SingleAddress), // Explicitly set to localhost when using .localhost subdomain

Copilot uses AI. Check for mistakes.
var s when IPAddress.TryParse(s, out var ipAddress) => ipAddress switch // The host is an IP address
{
var ip when IPAddress.Any.Equals(ip) => ("localhost", EndpointBindingMode.IPv4AnyAddresses), // 0.0.0.0 (IPv4 all addresses)
Expand Down
17 changes: 16 additions & 1 deletion src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,23 @@ private async Task ProcessResourceUrlCallbacks(IResource resource, CancellationT
Debug.Assert(endpoint.AllocatedEndpoint is not null, "Endpoint should be allocated at this point as we're calling this from ResourceEndpointsAllocatedEvent handler.");
if (endpoint.AllocatedEndpoint is { } allocatedEndpoint)
{
var url = new ResourceUrlAnnotation { Url = allocatedEndpoint.UriString, Endpoint = new EndpointReference(resourceWithEndpoints, endpoint) };
var endpointReference = new EndpointReference(resourceWithEndpoints, endpoint);
var url = new ResourceUrlAnnotation { Url = allocatedEndpoint.UriString, Endpoint = endpointReference };
urls.Add(url);
if (allocatedEndpoint.BindingMode != EndpointBindingMode.SingleAddress && (endpoint.TargetHost is not "localhost" or "127.0.0.1"))
Copy link

Copilot AI Jul 11, 2025

Choose a reason for hiding this comment

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

The check on endpoint.TargetHost.Length elsewhere uses a magic number (10) to detect .localhost domains. Rather than relying on string length, consider using EndsWith(".localhost", StringComparison.OrdinalIgnoreCase) by itself or extracting the suffix into a named constant for clarity.

Copilot uses AI. Check for mistakes.
{
// Endpoint is listening on multiple addresses so add another URL based on the declared target hostname
// For endpoints targeting all external addresses (IPv4 0.0.0.0 or IPv6 ::) use the machine name
var address = endpoint.TargetHost is "0.0.0.0" or "::" ? Environment.MachineName : endpoint.TargetHost;
url = new ResourceUrlAnnotation { Url = $"{allocatedEndpoint.UriScheme}://{address}:{allocatedEndpoint.Port}", Endpoint = endpointReference };
urls.Add(url);
}
else if (endpoint.TargetHost.Length > 10 && endpoint.TargetHost.EndsWith(".localhost", StringComparison.OrdinalIgnoreCase))
{
// Add the originally declared *.localhost URL
url = new ResourceUrlAnnotation { Url = $"{allocatedEndpoint.UriScheme}://{endpoint.TargetHost}:{allocatedEndpoint.Port}", Endpoint = endpointReference };
urls.Add(url);
}
}
}
}
Expand Down
66 changes: 66 additions & 0 deletions tests/Aspire.Hosting.Tests/WithEndpointTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,72 @@ public void WithEndpoint_WithAllArguments_ForwardsAllArguments()
Assert.Equal(System.Net.Sockets.ProtocolType.Tcp, endpoint.Protocol);
}

[Fact]
public async Task LocalhostTopLevelDomainSetsAnnotationValues()
{
using var builder = TestDistributedApplicationBuilder.Create();

var tcs = new TaskCompletionSource();
var projectA = builder.AddProject<ProjectA>("projecta")
.WithHttpsEndpoint()
.WithEndpoint("https", e => e.TargetHost = "example.localhost", createIfNotExists: false)
.OnBeforeResourceStarted((_, _, _) =>
{
tcs.SetResult();
return Task.CompletedTask;
});

var app = await builder.BuildAsync();
await app.StartAsync();
await tcs.Task;

var urls = projectA.Resource.Annotations.OfType<ResourceUrlAnnotation>();
Assert.Collection(urls,
url => Assert.StartsWith("https://localhost:", url.Url),
url => Assert.StartsWith("https://example.localhost:", url.Url));

EndpointAnnotation endpoint = Assert.Single(projectA.Resource.Annotations.OfType<EndpointAnnotation>());
Assert.NotNull(endpoint.AllocatedEndpoint);
Assert.Equal(EndpointBindingMode.SingleAddress, endpoint.AllocatedEndpoint.BindingMode);
Assert.Equal("localhost", endpoint.AllocatedEndpoint.Address);

await app.StopAsync();
}

[Theory]
[InlineData("0.0.0.0", EndpointBindingMode.IPv4AnyAddresses)]
//[InlineData("::", EndpointBindingMode.IPv6AnyAddresses)] // Need to figure out a good way to check that Ipv6 binding is supported
public async Task TopLevelDomainSetsAnnotationValues(string host, EndpointBindingMode endpointBindingMode)
{
using var builder = TestDistributedApplicationBuilder.Create();

var tcs = new TaskCompletionSource();
var projectA = builder.AddProject<ProjectA>("projecta")
.WithHttpsEndpoint()
.WithEndpoint("https", e => e.TargetHost = host, createIfNotExists: false)
.OnBeforeResourceStarted((_, _, _) =>
{
tcs.SetResult();
return Task.CompletedTask;
});

var app = await builder.BuildAsync();
await app.StartAsync();
await tcs.Task;

var urls = projectA.Resource.Annotations.OfType<ResourceUrlAnnotation>();
Assert.Collection(urls,
url => Assert.StartsWith("https://localhost:", url.Url),
url => Assert.StartsWith($"https://{Environment.MachineName}:", url.Url));

EndpointAnnotation endpoint = Assert.Single(projectA.Resource.Annotations.OfType<EndpointAnnotation>());
Assert.NotNull(endpoint.AllocatedEndpoint);
Assert.Equal(endpointBindingMode, endpoint.AllocatedEndpoint.BindingMode);
Assert.Equal("localhost", endpoint.AllocatedEndpoint.Address);

await app.StopAsync();
}

private sealed class TestProject : IProjectMetadata
{
public string ProjectPath => "projectpath";
Expand Down