diff --git a/src/Aspire.Cli/Aspire.Cli.csproj b/src/Aspire.Cli/Aspire.Cli.csproj
index aec002100aa..b29804475cb 100644
--- a/src/Aspire.Cli/Aspire.Cli.csproj
+++ b/src/Aspire.Cli/Aspire.Cli.csproj
@@ -93,6 +93,7 @@
+
diff --git a/src/Aspire.Cli/Commands/InitCommand.cs b/src/Aspire.Cli/Commands/InitCommand.cs
index e643237737f..e816f491db4 100644
--- a/src/Aspire.Cli/Commands/InitCommand.cs
+++ b/src/Aspire.Cli/Commands/InitCommand.cs
@@ -15,6 +15,7 @@
using Aspire.Cli.Scaffolding;
using Aspire.Cli.Telemetry;
using Aspire.Cli.Utils;
+using Aspire.Shared;
namespace Aspire.Cli.Commands;
@@ -398,32 +399,26 @@ private int DropAspireConfig(DirectoryInfo directory, string appHostPath, string
// Normally scaffolding + codegen creates these, but our thin init skips scaffolding.
if (settings["profiles"] is null)
{
- // Port ranges match CliTemplateFactory.GenerateRandomPorts()
- var httpPort = Random.Shared.Next(15000, 15300);
- var httpsPort = Random.Shared.Next(17000, 17300);
- var otlpHttpPort = Random.Shared.Next(19000, 19300);
- var otlpHttpsPort = Random.Shared.Next(21000, 21300);
- var resourceHttpPort = Random.Shared.Next(20000, 20300);
- var resourceHttpsPort = Random.Shared.Next(22000, 22300);
+ var ports = AppHostProfilePortGenerator.Generate(Random.Shared);
settings["profiles"] = new JsonObject
{
["https"] = new JsonObject
{
- ["applicationUrl"] = $"https://localhost:{httpsPort};http://localhost:{httpPort}",
+ ["applicationUrl"] = $"https://localhost:{ports.DashboardHttpsPort};http://localhost:{ports.DashboardHttpPort}",
["environmentVariables"] = new JsonObject
{
- ["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = $"https://localhost:{otlpHttpsPort}",
- ["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"] = $"https://localhost:{resourceHttpsPort}"
+ ["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = $"https://localhost:{ports.OtlpHttpsPort}",
+ ["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"] = $"https://localhost:{ports.ResourceServiceHttpsPort}"
}
},
["http"] = new JsonObject
{
- ["applicationUrl"] = $"http://localhost:{httpPort}",
+ ["applicationUrl"] = $"http://localhost:{ports.DashboardHttpPort}",
["environmentVariables"] = new JsonObject
{
- ["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = $"http://localhost:{otlpHttpPort}",
- ["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"] = $"http://localhost:{resourceHttpPort}",
+ ["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = $"http://localhost:{ports.OtlpHttpPort}",
+ ["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"] = $"http://localhost:{ports.ResourceServiceHttpPort}",
["ASPIRE_ALLOW_UNSECURED_TRANSPORT"] = "true"
}
}
diff --git a/src/Aspire.Cli/Templating/CliTemplateFactory.cs b/src/Aspire.Cli/Templating/CliTemplateFactory.cs
index 82f84dfed17..098592a8848 100644
--- a/src/Aspire.Cli/Templating/CliTemplateFactory.cs
+++ b/src/Aspire.Cli/Templating/CliTemplateFactory.cs
@@ -10,6 +10,7 @@
using Aspire.Cli.Resources;
using Aspire.Cli.Scaffolding;
using Aspire.Cli.Utils;
+using Aspire.Shared;
using Microsoft.Extensions.Logging;
namespace Aspire.Cli.Templating;
@@ -164,37 +165,26 @@ private bool IsTemplateAvailable(ITemplate template)
return _languageDiscovery.GetLanguageById(new LanguageId(template.LanguageId)) is not null;
}
- private static string ApplyTokens(string content, string projectName, string projectNameLower, string aspireVersion, TemplatePorts ports, string hostName = "localhost")
+ private static string ApplyTokens(string content, string projectName, string projectNameLower, string aspireVersion, AppHostProfilePorts ports, string hostName = "localhost")
{
return content
.Replace("{{projectName}}", projectName)
.Replace("{{projectNameLower}}", projectNameLower)
.Replace("{{aspireVersion}}", aspireVersion)
.Replace("{{hostName}}", hostName)
- .Replace("{{httpPort}}", ports.HttpPort.ToString(CultureInfo.InvariantCulture))
- .Replace("{{httpsPort}}", ports.HttpsPort.ToString(CultureInfo.InvariantCulture))
+ .Replace("{{httpPort}}", ports.DashboardHttpPort.ToString(CultureInfo.InvariantCulture))
+ .Replace("{{httpsPort}}", ports.DashboardHttpsPort.ToString(CultureInfo.InvariantCulture))
.Replace("{{otlpHttpPort}}", ports.OtlpHttpPort.ToString(CultureInfo.InvariantCulture))
.Replace("{{otlpHttpsPort}}", ports.OtlpHttpsPort.ToString(CultureInfo.InvariantCulture))
- .Replace("{{resourceHttpPort}}", ports.ResourceHttpPort.ToString(CultureInfo.InvariantCulture))
- .Replace("{{resourceHttpsPort}}", ports.ResourceHttpsPort.ToString(CultureInfo.InvariantCulture));
+ .Replace("{{resourceHttpPort}}", ports.ResourceServiceHttpPort.ToString(CultureInfo.InvariantCulture))
+ .Replace("{{resourceHttpsPort}}", ports.ResourceServiceHttpsPort.ToString(CultureInfo.InvariantCulture));
}
- private static TemplatePorts GenerateRandomPorts()
+ private static AppHostProfilePorts GenerateRandomPorts()
{
- return new TemplatePorts(
- HttpPort: Random.Shared.Next(15000, 15300),
- HttpsPort: Random.Shared.Next(17000, 17300),
- OtlpHttpPort: Random.Shared.Next(19000, 19300),
- OtlpHttpsPort: Random.Shared.Next(21000, 21300),
- ResourceHttpPort: Random.Shared.Next(20000, 20300),
- ResourceHttpsPort: Random.Shared.Next(22000, 22300));
+ return AppHostProfilePortGenerator.Generate(Random.Shared);
}
- private sealed record TemplatePorts(
- int HttpPort, int HttpsPort,
- int OtlpHttpPort, int OtlpHttpsPort,
- int ResourceHttpPort, int ResourceHttpsPort);
-
private static void AddOptionIfMissing(System.CommandLine.Command command, System.CommandLine.Option option)
{
if (!command.Options.Contains(option))
diff --git a/src/Aspire.Hosting.CodeGeneration.TypeScript/Aspire.Hosting.CodeGeneration.TypeScript.csproj b/src/Aspire.Hosting.CodeGeneration.TypeScript/Aspire.Hosting.CodeGeneration.TypeScript.csproj
index 44bd6c969fb..68bd97d52c9 100644
--- a/src/Aspire.Hosting.CodeGeneration.TypeScript/Aspire.Hosting.CodeGeneration.TypeScript.csproj
+++ b/src/Aspire.Hosting.CodeGeneration.TypeScript/Aspire.Hosting.CodeGeneration.TypeScript.csproj
@@ -24,6 +24,7 @@
+
diff --git a/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs b/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs
index 2e22ceb629b..003382c0750 100644
--- a/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs
+++ b/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs
@@ -120,19 +120,16 @@ public Dictionary Scaffold(ScaffoldRequest request)
? new Random(request.PortSeed.Value)
: Random.Shared;
- var httpsPort = random.Next(10000, 65000);
- var httpPort = random.Next(10000, 65000);
- var otlpPort = random.Next(10000, 65000);
- var resourceServicePort = random.Next(10000, 65000);
+ var ports = AppHostProfilePortGenerator.Generate(random);
files["apphost.run.json"] = $$"""
{
"profiles": {
"https": {
- "applicationUrl": "https://localhost:{{httpsPort}};http://localhost:{{httpPort}}",
+ "applicationUrl": "https://localhost:{{ports.DashboardHttpsPort}};http://localhost:{{ports.DashboardHttpPort}}",
"environmentVariables": {
- "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:{{otlpPort}}",
- "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:{{resourceServicePort}}"
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:{{ports.OtlpHttpsPort}}",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:{{ports.ResourceServiceHttpsPort}}"
}
}
}
diff --git a/src/Shared/AppHostProfilePortGenerator.cs b/src/Shared/AppHostProfilePortGenerator.cs
new file mode 100644
index 00000000000..f9516b5d734
--- /dev/null
+++ b/src/Shared/AppHostProfilePortGenerator.cs
@@ -0,0 +1,41 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Shared;
+
+internal static class AppHostProfilePortGenerator
+{
+ internal const int DashboardHttpPortMin = 15000;
+ internal const int DashboardHttpPortMaxExclusive = 15300;
+ internal const int DashboardHttpsPortMin = 17000;
+ internal const int DashboardHttpsPortMaxExclusive = 17300;
+ internal const int OtlpHttpPortMin = 19000;
+ internal const int OtlpHttpPortMaxExclusive = 19300;
+ internal const int OtlpHttpsPortMin = 21000;
+ internal const int OtlpHttpsPortMaxExclusive = 21300;
+ internal const int ResourceServiceHttpPortMin = 20000;
+ internal const int ResourceServiceHttpPortMaxExclusive = 20300;
+ internal const int ResourceServiceHttpsPortMin = 22000;
+ internal const int ResourceServiceHttpsPortMaxExclusive = 22300;
+
+ internal static AppHostProfilePorts Generate(Random random)
+ {
+ ArgumentNullException.ThrowIfNull(random);
+
+ return new AppHostProfilePorts(
+ DashboardHttpPort: random.Next(DashboardHttpPortMin, DashboardHttpPortMaxExclusive),
+ DashboardHttpsPort: random.Next(DashboardHttpsPortMin, DashboardHttpsPortMaxExclusive),
+ OtlpHttpPort: random.Next(OtlpHttpPortMin, OtlpHttpPortMaxExclusive),
+ OtlpHttpsPort: random.Next(OtlpHttpsPortMin, OtlpHttpsPortMaxExclusive),
+ ResourceServiceHttpPort: random.Next(ResourceServiceHttpPortMin, ResourceServiceHttpPortMaxExclusive),
+ ResourceServiceHttpsPort: random.Next(ResourceServiceHttpsPortMin, ResourceServiceHttpsPortMaxExclusive));
+ }
+}
+
+internal readonly record struct AppHostProfilePorts(
+ int DashboardHttpPort,
+ int DashboardHttpsPort,
+ int OtlpHttpPort,
+ int OtlpHttpsPort,
+ int ResourceServiceHttpPort,
+ int ResourceServiceHttpsPort);
diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/TypeScriptLanguageSupportTests.cs b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/TypeScriptLanguageSupportTests.cs
index adaab97bdf7..1b016e144bc 100644
--- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/TypeScriptLanguageSupportTests.cs
+++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/TypeScriptLanguageSupportTests.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text.Json.Nodes;
+using Aspire.Shared;
using Aspire.TypeSystem;
namespace Aspire.Hosting.CodeGeneration.TypeScript.Tests;
@@ -187,6 +188,41 @@ public void Scaffold_DoesNotEmitRootTsConfig_WhenOneAlreadyExists()
Assert.Equal(existingTsConfig, File.ReadAllText(existingTsConfigPath));
}
+ [Theory]
+ [InlineData(null)]
+ [InlineData(0)]
+ [InlineData(1)]
+ [InlineData(16626)]
+ [InlineData(55571)]
+ public void Scaffold_GeneratesProfilePortsOutsideWindowsEphemeralRange(int? portSeed)
+ {
+ using var testDir = new TestTempDirectory();
+
+ var files = _languageSupport.Scaffold(new ScaffoldRequest
+ {
+ TargetPath = testDir.Path,
+ ProjectName = "PortsApp",
+ PortSeed = portSeed
+ });
+
+ var appHostRunJson = ParseJson(files["apphost.run.json"]);
+ var httpsProfile = appHostRunJson["profiles"]!["https"]!.AsObject();
+ var applicationUrls = httpsProfile["applicationUrl"]!.GetValue().Split(';', StringSplitOptions.RemoveEmptyEntries);
+ var environmentVariables = httpsProfile["environmentVariables"]!.AsObject();
+
+ Assert.Equal(2, applicationUrls.Length);
+
+ var httpsPort = GetPort(applicationUrls.Single(url => url.StartsWith("https://", StringComparison.OrdinalIgnoreCase)));
+ var httpPort = GetPort(applicationUrls.Single(url => url.StartsWith("http://", StringComparison.OrdinalIgnoreCase)));
+ var otlpHttpsPort = GetPort(environmentVariables["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"]!.GetValue());
+ var resourceServiceHttpsPort = GetPort(environmentVariables["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"]!.GetValue());
+
+ AssertPortInRange(httpPort, AppHostProfilePortGenerator.DashboardHttpPortMin, AppHostProfilePortGenerator.DashboardHttpPortMaxExclusive);
+ AssertPortInRange(httpsPort, AppHostProfilePortGenerator.DashboardHttpsPortMin, AppHostProfilePortGenerator.DashboardHttpsPortMaxExclusive);
+ AssertPortInRange(otlpHttpsPort, AppHostProfilePortGenerator.OtlpHttpsPortMin, AppHostProfilePortGenerator.OtlpHttpsPortMaxExclusive);
+ AssertPortInRange(resourceServiceHttpsPort, AppHostProfilePortGenerator.ResourceServiceHttpsPortMin, AppHostProfilePortGenerator.ResourceServiceHttpsPortMaxExclusive);
+ }
+
[Fact]
public void GetRuntimeSpec_UsesAppHostSpecificTsConfig()
{
@@ -198,4 +234,14 @@ public void GetRuntimeSpec_UsesAppHostSpecificTsConfig()
}
private static JsonObject ParseJson(string content) => JsonNode.Parse(content)!.AsObject();
+
+ private static int GetPort(string url) => new Uri(url).Port;
+
+ private const int WindowsEphemeralPortMin = 49152;
+
+ private static void AssertPortInRange(int port, int minInclusive, int maxExclusive)
+ {
+ Assert.InRange(port, minInclusive, maxExclusive - 1);
+ Assert.True(port < WindowsEphemeralPortMin, $"Expected port {port} to be below the Windows ephemeral range.");
+ }
}