From 6b3de30a9a4deffaed294be32e4f55cafffe3815 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 2 Jul 2025 21:24:49 -0700 Subject: [PATCH 1/6] Add non-localhost URLs --- playground/TestShop/TestShop.AppHost/Program.cs | 3 ++- .../Orchestrator/ApplicationOrchestrator.cs | 11 +++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/playground/TestShop/TestShop.AppHost/Program.cs b/playground/TestShop/TestShop.AppHost/Program.cs index 0a86d3c5bd1..a3a9bc4d495 100644 --- a/playground/TestShop/TestShop.AppHost/Program.cs +++ b/playground/TestShop/TestShop.AppHost/Program.cs @@ -55,8 +55,9 @@ var messaging = builder.AddRabbitMQ("messaging") .WithDataVolume() - .WithLifetime(ContainerLifetime.Persistent) + //.WithLifetime(ContainerLifetime.Persistent) .WithManagementPlugin() + .WithEndpoint("management", e => e.TargetHost = "0.0.0.0", createIfNotExists: false) .PublishAsContainer(); var basketService = builder.AddProject("basketservice", @"..\BasketService\BasketService.csproj") diff --git a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs index 153431f6b96..44a2e839d45 100644 --- a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs +++ b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs @@ -212,9 +212,16 @@ 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")) + { + // 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); + } } } From 22c3391d8a88de8042ce61f693da9be92ddd2ece Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 3 Jul 2025 10:25:19 -0700 Subject: [PATCH 2/6] Fix build --- .../Orchestrator/ApplicationOrchestrator.cs | 71 ++++++++++--------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs index 44a2e839d45..30b19b0fab0 100644 --- a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs +++ b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs @@ -222,55 +222,56 @@ private async Task ProcessResourceUrlCallbacks(IResource resource, CancellationT url = new ResourceUrlAnnotation { Url = $"{allocatedEndpoint.UriScheme}://{address}:{allocatedEndpoint.Port}", Endpoint = endpointReference }; urls.Add(url); } + } } - } - - if (resource.TryGetUrls(out var existingUrls)) - { - // Static URLs added to the resource via WithUrl(string name, string url), i.e. not callback-based - urls.AddRange(existingUrls); - } - // Run the URL callbacks - if (resource.TryGetAnnotationsOfType(out var callbacks)) - { - var urlsCallbackContext = new ResourceUrlsCallbackContext(_executionContext, resource, urls, cancellationToken) + if (resource.TryGetUrls(out var existingUrls)) { - Logger = _loggerService.GetLogger(resource.Name) - }; - foreach (var callback in callbacks) + // Static URLs added to the resource via WithUrl(string name, string url), i.e. not callback-based + urls.AddRange(existingUrls); + } + + // Run the URL callbacks + if (resource.TryGetAnnotationsOfType(out var callbacks)) { - await callback.Callback(urlsCallbackContext).ConfigureAwait(false); + var urlsCallbackContext = new ResourceUrlsCallbackContext(_executionContext, resource, urls, cancellationToken) + { + Logger = _loggerService.GetLogger(resource.Name) + }; + foreach (var callback in callbacks) + { + await callback.Callback(urlsCallbackContext).ConfigureAwait(false); + } } - } - // Clear existing URLs - if (existingUrls is not null) - { - var existing = existingUrls.ToArray(); - for (var i = existing.Length - 1; i >= 0; i--) + // Clear existing URLs + if (existingUrls is not null) { - var url = existing[i]; - resource.Annotations.Remove(url); + var existing = existingUrls.ToArray(); + for (var i = existing.Length - 1; i >= 0; i--) + { + var url = existing[i]; + resource.Annotations.Remove(url); + } } - } - // Convert relative endpoint URLs to absolute URLs - foreach (var url in urls) - { - if (url.Endpoint is { } endpoint) + // Convert relative endpoint URLs to absolute URLs + foreach (var url in urls) { - if (url.Url.StartsWith('/') && endpoint.AllocatedEndpoint is { } allocatedEndpoint) + if (url.Endpoint is { } endpoint) { - url.Url = allocatedEndpoint.UriString.TrimEnd('/') + url.Url; + if (url.Url.StartsWith('/') && endpoint.AllocatedEndpoint is { } allocatedEndpoint) + { + url.Url = allocatedEndpoint.UriString.TrimEnd('/') + url.Url; + } } } - } - // Add URLs - foreach (var url in urls) - { - resource.Annotations.Add(url); + // Add URLs + foreach (var url in urls) + { + resource.Annotations.Add(url); + } } } From 68ec4688ae57726faba51fc9360412256567be33 Mon Sep 17 00:00:00 2001 From: Brennan Date: Tue, 8 Jul 2025 15:37:47 -0700 Subject: [PATCH 3/6] *.localhost --- src/Aspire.Hosting/Dcp/DcpExecutor.cs | 1 + src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/Aspire.Hosting/Dcp/DcpExecutor.cs b/src/Aspire.Hosting/Dcp/DcpExecutor.cs index 2b3bd106e20..da9ed8739c0 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutor.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutor.cs @@ -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 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) diff --git a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs index 30b19b0fab0..02a4d0341dc 100644 --- a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs +++ b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs @@ -222,6 +222,11 @@ private async Task ProcessResourceUrlCallbacks(IResource resource, CancellationT 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); + } } } From 039f225e14dd878b65a8d07dd33a18b6f533fa0d Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 11 Jul 2025 12:50:16 -0700 Subject: [PATCH 4/6] test --- .../Orchestrator/ApplicationOrchestrator.cs | 70 +++++++++---------- .../Aspire.Hosting.Tests/WithEndpointTests.cs | 66 +++++++++++++++++ 2 files changed, 101 insertions(+), 35 deletions(-) diff --git a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs index 02a4d0341dc..5a9091ac7c9 100644 --- a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs +++ b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs @@ -229,54 +229,54 @@ private async Task ProcessResourceUrlCallbacks(IResource resource, CancellationT } } } + } - if (resource.TryGetUrls(out var existingUrls)) - { - // Static URLs added to the resource via WithUrl(string name, string url), i.e. not callback-based - urls.AddRange(existingUrls); - } + if (resource.TryGetUrls(out var existingUrls)) + { + // Static URLs added to the resource via WithUrl(string name, string url), i.e. not callback-based + urls.AddRange(existingUrls); + } - // Run the URL callbacks - if (resource.TryGetAnnotationsOfType(out var callbacks)) + // Run the URL callbacks + if (resource.TryGetAnnotationsOfType(out var callbacks)) + { + var urlsCallbackContext = new ResourceUrlsCallbackContext(_executionContext, resource, urls, cancellationToken) { - var urlsCallbackContext = new ResourceUrlsCallbackContext(_executionContext, resource, urls, cancellationToken) - { - Logger = _loggerService.GetLogger(resource.Name) - }; - foreach (var callback in callbacks) - { - await callback.Callback(urlsCallbackContext).ConfigureAwait(false); - } + Logger = _loggerService.GetLogger(resource.Name) + }; + foreach (var callback in callbacks) + { + await callback.Callback(urlsCallbackContext).ConfigureAwait(false); } + } - // Clear existing URLs - if (existingUrls is not null) + // Clear existing URLs + if (existingUrls is not null) + { + var existing = existingUrls.ToArray(); + for (var i = existing.Length - 1; i >= 0; i--) { - var existing = existingUrls.ToArray(); - for (var i = existing.Length - 1; i >= 0; i--) - { - var url = existing[i]; - resource.Annotations.Remove(url); - } + var url = existing[i]; + resource.Annotations.Remove(url); } + } - // Convert relative endpoint URLs to absolute URLs - foreach (var url in urls) + // Convert relative endpoint URLs to absolute URLs + foreach (var url in urls) + { + if (url.Endpoint is { } endpoint) { - if (url.Endpoint is { } endpoint) + if (url.Url.StartsWith('/') && endpoint.AllocatedEndpoint is { } allocatedEndpoint) { - if (url.Url.StartsWith('/') && endpoint.AllocatedEndpoint is { } allocatedEndpoint) - { - url.Url = allocatedEndpoint.UriString.TrimEnd('/') + url.Url; - } + url.Url = allocatedEndpoint.UriString.TrimEnd('/') + url.Url; } } + } - // Add URLs - foreach (var url in urls) - { - resource.Annotations.Add(url); - } + // Add URLs + foreach (var url in urls) + { + resource.Annotations.Add(url); } } diff --git a/tests/Aspire.Hosting.Tests/WithEndpointTests.cs b/tests/Aspire.Hosting.Tests/WithEndpointTests.cs index 32c25dc9fd1..1a2fbcbec4b 100644 --- a/tests/Aspire.Hosting.Tests/WithEndpointTests.cs +++ b/tests/Aspire.Hosting.Tests/WithEndpointTests.cs @@ -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") + .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(); + 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()); + 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") + .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(); + 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()); + 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"; From 25681525dff407d327d03eb7b04d04d4258c2f16 Mon Sep 17 00:00:00 2001 From: Brennan Date: Fri, 11 Jul 2025 13:24:03 -0700 Subject: [PATCH 5/6] cleanup --- playground/TestShop/TestShop.AppHost/Program.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/playground/TestShop/TestShop.AppHost/Program.cs b/playground/TestShop/TestShop.AppHost/Program.cs index a3a9bc4d495..0a86d3c5bd1 100644 --- a/playground/TestShop/TestShop.AppHost/Program.cs +++ b/playground/TestShop/TestShop.AppHost/Program.cs @@ -55,9 +55,8 @@ var messaging = builder.AddRabbitMQ("messaging") .WithDataVolume() - //.WithLifetime(ContainerLifetime.Persistent) + .WithLifetime(ContainerLifetime.Persistent) .WithManagementPlugin() - .WithEndpoint("management", e => e.TargetHost = "0.0.0.0", createIfNotExists: false) .PublishAsContainer(); var basketService = builder.AddProject("basketservice", @"..\BasketService\BasketService.csproj") From 1ae3c641ec4f1a6dce28aba141acb7bf7f575357 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Mon, 14 Jul 2025 10:46:16 -0700 Subject: [PATCH 6/6] Apply suggestions from code review --- src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs index 5a9091ac7c9..709cc47000b 100644 --- a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs +++ b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs @@ -217,13 +217,15 @@ private async Task ProcessResourceUrlCallbacks(IResource resource, CancellationT urls.Add(url); if (allocatedEndpoint.BindingMode != EndpointBindingMode.SingleAddress && (endpoint.TargetHost is not "localhost" or "127.0.0.1")) { - // Endpoint is listening on multiple addresses so add more URLs using - var address = endpoint.TargetHost == "0.0.0.0" ? Environment.MachineName : endpoint.TargetHost; + // 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); }