Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
15 changes: 14 additions & 1 deletion src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,21 @@ 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 more URLs using
var address = endpoint.TargetHost == "0.0.0.0" ? 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))
{
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
Loading