diff --git a/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs b/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs index 5f601062be4..35408653002 100644 --- a/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs @@ -402,11 +402,27 @@ private static IResourceBuilder WithProjectDefaults(this IResou if (!kestrelEndpointsByScheme.Any()) { var urlsFromApplicationUrl = launchProfile.ApplicationUrl?.Split(';', StringSplitOptions.RemoveEmptyEntries) ?? []; + Dictionary endpointCountByScheme = []; foreach (var url in urlsFromApplicationUrl) { var uri = new Uri(url); - builder.WithEndpoint(uri.Scheme, e => + // Keep track of how many endpoints we have for each scheme + endpointCountByScheme.TryGetValue(uri.Scheme, out var count); + endpointCountByScheme[uri.Scheme] = count + 1; + + // If we have multiple for the same scheme, we differentiate them by appending a number. + // We only do this starting with the second endpoint, so that the first stays just http/https. + // This allows us to keep the same behavior as "dotnet run". + // Also, note that we only do this in Run mode, as in Publish mode those extra endpoints + // with generic names would not be easily usable. + var endpointName = uri.Scheme; + if (endpointCountByScheme[uri.Scheme] > 1) + { + endpointName += endpointCountByScheme[uri.Scheme]; + } + + builder.WithEndpoint(endpointName, e => { e.Port = uri.Port; e.UriScheme = uri.Scheme; diff --git a/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs b/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs index f7d3532c604..a0395440134 100644 --- a/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs @@ -410,6 +410,35 @@ public async Task ProjectWithLaunchProfileAddsHttpOrHttpsEndpointAddsToEnv() Assert.False(config.ContainsKey("ASPNETCORE_HTTPS_PORT")); } + [Fact] + public async Task ProjectWithMultipleLaunchProfileAppUrlsGetsAllUrls() + { + var appBuilder = CreateBuilder(operation: DistributedApplicationOperation.Run); + + var builder = appBuilder.AddProject("projectName"); + + // Need to allocated all the endpoints we get from the launch profile applicationUrl + var index = 0; + foreach (var q in new[] { "http", "http2", "https", "https2", "https3" }) + { + builder.WithEndpoint(q, e => + { + e.AllocatedEndpoint = new(e, "localhost", e.Port!.Value, targetPortExpression: $"p{index++}"); + }); + } + + using var app = appBuilder.Build(); + var appModel = app.Services.GetRequiredService(); + var projectResources = appModel.GetProjectResources(); + var resource = Assert.Single(projectResources); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance); + + Assert.Equal("https://localhost:p2;http://localhost:p0;http://localhost:p1;https://localhost:p3;https://localhost:p4", config["ASPNETCORE_URLS"]); + + // The first https port is the one that should be used for ASPNETCORE_HTTPS_PORT + Assert.Equal("7144", config["ASPNETCORE_HTTPS_PORT"]); + } + [Fact] public void DisabledForwardedHeadersAddsAnnotationToProject() { @@ -618,4 +647,25 @@ public TestProjectWithLaunchSettings() }; } } + + private sealed class TestProjectWithManyAppUrlsInLaunchSettings : BaseProjectWithProfileAndConfig + { + public TestProjectWithManyAppUrlsInLaunchSettings() + { + Profiles = new() + { + ["https"] = new() + { + CommandName = "Project", + CommandLineArgs = "arg1 arg2", + LaunchBrowser = true, + ApplicationUrl = "https://localhost:7144;http://localhost:5193;http://localhost:5194;https://localhost:7145;https://localhost:7146", + EnvironmentVariables = new() + { + ["ASPNETCORE_ENVIRONMENT"] = "Development" + } + } + }; + } + } }