Skip to content

Commit ae5295f

Browse files
[release/10.0] Backport fixes to templates derived *.dev.localhost hostnames in launch profiles (#65048)
* Fix DNS-invalid hostnames with underscores in dotnet new templates (#64988) * Initial plan * Add DNS-safe hostname forms and symbols to templates Co-authored-by: DamianEdwards <249088+DamianEdwards@users.noreply.github.com> * Add trimming of leading/trailing hyphens for DNS-safe hostnames Co-authored-by: DamianEdwards <249088+DamianEdwards@users.noreply.github.com> * Use distinct placeholder LocalhostTldHostNamePrefix for hostname replacement Co-authored-by: DamianEdwards <249088+DamianEdwards@users.noreply.github.com> * Add tests for DNS-compliant hostname generation with --localhost-tld Co-authored-by: DamianEdwards <249088+DamianEdwards@users.noreply.github.com> * Add tests for DNS-compliant hostname generation in Blazor templates Co-authored-by: DamianEdwards <249088+DamianEdwards@users.noreply.github.com> * Move VerifyDnsCompliantHostname to shared Project class Co-authored-by: DamianEdwards <249088+DamianEdwards@users.noreply.github.com> * Fix -n parameter conflict and add tests with numbers in project names Co-authored-by: DamianEdwards <249088+DamianEdwards@users.noreply.github.com> * Refactor DNS hostname tests to use Theory with InlineData Co-authored-by: DamianEdwards <249088+DamianEdwards@users.noreply.github.com> * Fix razorcomponent item test --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: DamianEdwards <249088+DamianEdwards@users.noreply.github.com> Co-authored-by: Damian Edwards <damian@damianedwards.com> * Add replaceRepeatedHyphens step to template processing and update tests for DNS-compliant hostnames (#65027) Related to #64978 --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: DamianEdwards <249088+DamianEdwards@users.noreply.github.com>
1 parent a190abe commit ae5295f

File tree

10 files changed

+198
-8
lines changed

10 files changed

+198
-8
lines changed

src/ProjectTemplates/Shared/ArgConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ internal static class ArgConstants
2424
public const string UseLocalDb = "-uld";
2525
public const string NoHttps = "--no-https";
2626
public const string PublishNativeAot = "--aot";
27+
public const string LocalhostTld = "--localhost-tld";
2728
public const string NoInteractivity = "--interactivity none";
2829
public const string WebAssemblyInteractivity = "--interactivity WebAssembly";
2930
public const string AutoInteractivity = "--interactivity Auto";

src/ProjectTemplates/Shared/Project.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ internal async Task RunDotNetNewAsync(
7171
// Used to set special options in MSBuild
7272
IDictionary<string, string> environmentVariables = null)
7373
{
74+
if (templateName.Contains(' '))
75+
{
76+
throw new ArgumentException("Template name cannot contain spaces.");
77+
}
78+
7479
var hiveArg = $"--debug:disable-sdk-templates --debug:custom-hive \"{TemplatePackageInstaller.CustomHivePath}\"";
7580
var argString = $"new {templateName} {hiveArg}";
7681
environmentVariables ??= new Dictionary<string, string>();
@@ -111,6 +116,13 @@ internal async Task RunDotNetNewAsync(
111116
// We omit the hive argument and the template output dir as they are not relevant and add noise.
112117
ProjectArguments = argString.Replace(hiveArg, "");
113118

119+
// Only add -n parameter if ProjectName is set and args doesn't already contain -n or --name
120+
if (!string.IsNullOrEmpty(ProjectName) &&
121+
args?.Any(a => a.Contains("-n ") || a.Contains("--name ") || a == "-n" || a == "--name") != true)
122+
{
123+
argString += $" -n \"{ProjectName}\"";
124+
}
125+
114126
argString += $" -o {TemplateOutputDir}";
115127

116128
if (Directory.Exists(TemplateOutputDir))
@@ -350,6 +362,42 @@ public async Task VerifyLaunchSettings(string[] expectedLaunchProfileNames)
350362
}
351363
}
352364

365+
public async Task VerifyDnsCompliantHostname(string expectedHostname)
366+
{
367+
var launchSettingsPath = Path.Combine(TemplateOutputDir, "Properties", "launchSettings.json");
368+
Assert.True(File.Exists(launchSettingsPath), $"launchSettings.json not found at {launchSettingsPath}");
369+
370+
var launchSettingsContent = await File.ReadAllTextAsync(launchSettingsPath);
371+
using var launchSettings = JsonDocument.Parse(launchSettingsContent);
372+
373+
var profiles = launchSettings.RootElement.GetProperty("profiles");
374+
375+
foreach (var profile in profiles.EnumerateObject())
376+
{
377+
if (profile.Value.TryGetProperty("applicationUrl", out var applicationUrl))
378+
{
379+
var urls = applicationUrl.GetString();
380+
if (!string.IsNullOrEmpty(urls))
381+
{
382+
// Verify the hostname in the URL matches expected DNS-compliant format
383+
Assert.Contains($"{expectedHostname}.dev.localhost:", urls);
384+
385+
// Verify no underscores in hostname (RFC 952/1123 compliance)
386+
var hostnamePattern = @"://([^:]+)\.dev\.localhost:";
387+
var matches = System.Text.RegularExpressions.Regex.Matches(urls, hostnamePattern);
388+
foreach (System.Text.RegularExpressions.Match match in matches)
389+
{
390+
var hostname = match.Groups[1].Value;
391+
Assert.DoesNotContain("_", hostname);
392+
Assert.DoesNotContain(".", hostname);
393+
Assert.False(hostname.StartsWith("-", StringComparison.Ordinal), $"Hostname '{hostname}' should not start with hyphen (RFC 952/1123 violation)");
394+
Assert.False(hostname.EndsWith("-", StringComparison.Ordinal), $"Hostname '{hostname}' should not end with hyphen (RFC 952/1123 violation)");
395+
}
396+
}
397+
}
398+
}
399+
}
400+
353401
public async Task VerifyHasProperty(string propertyName, string expectedValue)
354402
{
355403
var projectFile = Directory.EnumerateFiles(TemplateOutputDir, "*proj").FirstOrDefault();

src/ProjectTemplates/Shared/ProjectFactoryFixture.cs

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,22 @@ public async Task<Project> CreateProject(ITestOutputHelper output)
3939
return project;
4040
}
4141

42-
private Project CreateProjectImpl(ITestOutputHelper output)
42+
public async Task<Project> CreateProject(ITestOutputHelper output, string projectName)
43+
{
44+
await TemplatePackageInstaller.EnsureTemplatingEngineInitializedAsync(output);
45+
46+
var project = CreateProjectImpl(output, projectName);
47+
48+
var projectKey = Guid.NewGuid().ToString().Substring(0, 10).ToLowerInvariant();
49+
if (!_projects.TryAdd(projectKey, project))
50+
{
51+
throw new InvalidOperationException($"Project key collision in {nameof(ProjectFactoryFixture)}.{nameof(CreateProject)}!");
52+
}
53+
54+
return project;
55+
}
56+
57+
private Project CreateProjectImpl(ITestOutputHelper output, string projectName = null)
4358
{
4459
var project = new Project
4560
{
@@ -49,11 +64,20 @@ private Project CreateProjectImpl(ITestOutputHelper output)
4964
// declarations (i.e. make it more stable for testing)
5065
ProjectGuid = GetRandomLetter() + Path.GetRandomFileName().Replace(".", string.Empty)
5166
};
52-
project.ProjectName = $"AspNet.{project.ProjectGuid}";
67+
68+
if (string.IsNullOrEmpty(projectName))
69+
{
70+
project.ProjectName = $"AspNet.{project.ProjectGuid}";
71+
}
72+
else
73+
{
74+
project.ProjectName = projectName;
75+
}
5376

5477
var assemblyPath = GetType().Assembly;
5578
var basePath = GetTemplateFolderBasePath(assemblyPath);
56-
project.TemplateOutputDir = Path.Combine(basePath, project.ProjectName);
79+
// Use ProjectGuid for directory to avoid filesystem issues with invalid characters in projectName
80+
project.TemplateOutputDir = Path.Combine(basePath, $"AspNet.{project.ProjectGuid}");
5781

5882
return project;
5983
}

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/.template.config/template.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,45 @@
202202
]
203203
}
204204
],
205+
"forms": {
206+
"lowerCaseInvariantWithHyphens": {
207+
"identifier": "chain",
208+
"steps": [
209+
"lowerCaseInvariant",
210+
"replaceDnsInvalidChars",
211+
"replaceRepeatedHyphens",
212+
"trimLeadingHyphens",
213+
"trimTrailingHyphens"
214+
]
215+
},
216+
"replaceDnsInvalidChars": {
217+
"identifier": "replace",
218+
"pattern": "[^a-z0-9-]",
219+
"replacement": "-"
220+
},
221+
"replaceRepeatedHyphens": {
222+
"identifier": "replace",
223+
"pattern": "-{2,}",
224+
"replacement": "-"
225+
},
226+
"trimLeadingHyphens": {
227+
"identifier": "replace",
228+
"pattern": "^-+",
229+
"replacement": ""
230+
},
231+
"trimTrailingHyphens": {
232+
"identifier": "replace",
233+
"pattern": "-+$",
234+
"replacement": ""
235+
}
236+
},
205237
"symbols": {
238+
"hostName": {
239+
"type": "derived",
240+
"valueSource": "name",
241+
"valueTransform": "lowerCaseInvariantWithHyphens",
242+
"replaces": "LocalhostTldHostNamePrefix"
243+
},
206244
"Framework": {
207245
"type": "parameter",
208246
"description": "The target framework for the project.",

src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWebCSharp.1/Properties/launchSettings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
1111
//#endif
1212
//#if (LocalhostTld)
13-
"applicationUrl": "http://blazorwebcsharp__1.dev.localhost:5500",
13+
"applicationUrl": "http://LocalhostTldHostNamePrefix.dev.localhost:5500",
1414
//#else
1515
"applicationUrl": "http://localhost:5500",
1616
//#endif
@@ -32,7 +32,7 @@
3232
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
3333
//#endif
3434
//#if (LocalhostTld)
35-
"applicationUrl": "https://blazorwebcsharp__1.dev.localhost:5501;http://blazorwebcsharp__1.dev.localhost:5500",
35+
"applicationUrl": "https://LocalhostTldHostNamePrefix.dev.localhost:5501;http://LocalhostTldHostNamePrefix.dev.localhost:5500",
3636
//#else
3737
"applicationUrl": "https://localhost:5501;http://localhost:5500",
3838
//#endif

src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/.template.config/template.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,45 @@
4848
]
4949
}
5050
],
51+
"forms": {
52+
"lowerCaseInvariantWithHyphens": {
53+
"identifier": "chain",
54+
"steps": [
55+
"lowerCaseInvariant",
56+
"replaceDnsInvalidChars",
57+
"replaceRepeatedHyphens",
58+
"trimLeadingHyphens",
59+
"trimTrailingHyphens"
60+
]
61+
},
62+
"replaceDnsInvalidChars": {
63+
"identifier": "replace",
64+
"pattern": "[^a-z0-9-]",
65+
"replacement": "-"
66+
},
67+
"replaceRepeatedHyphens": {
68+
"identifier": "replace",
69+
"pattern": "-{2,}",
70+
"replacement": "-"
71+
},
72+
"trimLeadingHyphens": {
73+
"identifier": "replace",
74+
"pattern": "^-+",
75+
"replacement": ""
76+
},
77+
"trimTrailingHyphens": {
78+
"identifier": "replace",
79+
"pattern": "-+$",
80+
"replacement": ""
81+
}
82+
},
5183
"symbols": {
84+
"hostName": {
85+
"type": "derived",
86+
"valueSource": "name",
87+
"valueTransform": "lowerCaseInvariantWithHyphens",
88+
"replaces": "LocalhostTldHostNamePrefix"
89+
},
5290
"ExcludeLaunchSettings": {
5391
"type": "parameter",
5492
"datatype": "bool",

src/ProjectTemplates/Web.ProjectTemplates/content/EmptyWeb-CSharp/Properties/launchSettings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"dotnetRunMessages": true,
77
"launchBrowser": true,
88
//#if (LocalhostTld)
9-
"applicationUrl": "http://company_webapplication1.dev.localhost:5000",
9+
"applicationUrl": "http://LocalhostTldHostNamePrefix.dev.localhost:5000",
1010
//#else
1111
"applicationUrl": "http://localhost:5000",
1212
//#endif
@@ -24,7 +24,7 @@
2424
"dotnetRunMessages": true,
2525
"launchBrowser": true,
2626
//#if (LocalhostTld)
27-
"applicationUrl": "https://company_webapplication1.dev.localhost:5001;http://company_webapplication1.dev.localhost:5000",
27+
"applicationUrl": "https://LocalhostTldHostNamePrefix.dev.localhost:5001;http://LocalhostTldHostNamePrefix.dev.localhost:5000",
2828
//#else
2929
"applicationUrl": "https://localhost:5001;http://localhost:5000",
3030
//#endif

src/ProjectTemplates/test/Templates.Blazor.Tests/BlazorWebTemplateTest.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Net;
5+
using System.Text.Json;
56
using Microsoft.AspNetCore.BrowserTesting;
7+
using Microsoft.AspNetCore.InternalTesting;
68
using Templates.Test.Helpers;
79

810
namespace BlazorTemplates.Tests;
@@ -88,4 +90,23 @@ private async Task TestProjectCoreAsync(Project project, BrowserKind browserKind
8890
await TestBasicInteractionInNewPageAsync(browserKind, aspNetProcess.ListeningUri.AbsoluteUri, appName, pagesToExclude, authenticationFeatures);
8991
}
9092
}
93+
94+
[ConditionalTheory]
95+
[InlineData("my.namespace.blazor", "my-namespace-blazor")]
96+
[InlineData(".StartWithDot", "startwithdot")]
97+
[InlineData("EndWithDot.", "endwithdot")]
98+
[InlineData("My..Test__Project", "my-test-project")]
99+
[InlineData("Project123.Test456", "project123-test456")]
100+
[InlineData("xn--My.Test.Project", "xn-my-test-project")]
101+
[SkipOnHelix("Cert failure, https://github.com/dotnet/aspnetcore/issues/28090", Queues = "All.OSX;" + HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)]
102+
public async Task BlazorWebTemplateLocalhostTld_GeneratesDnsCompliantHostnames(string projectName, string expectedHostname)
103+
{
104+
var project = await ProjectFactory.CreateProject(Output, projectName);
105+
106+
await project.RunDotNetNewAsync("blazor", args: new[] { ArgConstants.LocalhostTld, ArgConstants.NoInteractivity });
107+
108+
var expectedLaunchProfileNames = new[] { "http", "https" };
109+
await project.VerifyLaunchSettings(expectedLaunchProfileNames);
110+
await project.VerifyDnsCompliantHostname(expectedHostname);
111+
}
91112
}

src/ProjectTemplates/test/Templates.Tests/EmptyWebTemplateTest.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Text.Json;
45
using System.Threading.Tasks;
56
using Microsoft.AspNetCore.InternalTesting;
67
using Templates.Test.Helpers;
@@ -73,6 +74,25 @@ public async Task EmptyWebTemplateNoHttpsFSharp()
7374
await EmtpyTemplateCore("F#", args: new[] { ArgConstants.NoHttps });
7475
}
7576

77+
[ConditionalTheory]
78+
[InlineData("my.namespace.web", "my-namespace-web")]
79+
[InlineData(".StartWithDot", "startwithdot")]
80+
[InlineData("EndWithDot.", "endwithdot")]
81+
[InlineData("My..Test__Project", "my-test-project")]
82+
[InlineData("Project123.Test456", "project123-test456")]
83+
[InlineData("xn--My.Test.Project", "xn-my-test-project")]
84+
[SkipOnHelix("Cert failure, https://github.com/dotnet/aspnetcore/issues/28090", Queues = "All.OSX;" + HelixConstants.Windows10Arm64 + HelixConstants.DebianArm64)]
85+
public async Task EmptyWebTemplateLocalhostTld_GeneratesDnsCompliantHostnames(string projectName, string expectedHostname)
86+
{
87+
var project = await ProjectFactory.CreateProject(Output, projectName);
88+
89+
await project.RunDotNetNewAsync("web", args: new[] { ArgConstants.LocalhostTld });
90+
91+
var expectedLaunchProfileNames = new[] { "http", "https" };
92+
await project.VerifyLaunchSettings(expectedLaunchProfileNames);
93+
await project.VerifyDnsCompliantHostname(expectedHostname);
94+
}
95+
7696
private async Task EmtpyTemplateCore(string languageOverride, string[] args = null)
7797
{
7898
var project = await ProjectFactory.CreateProject(Output);

src/ProjectTemplates/test/Templates.Tests/ItemTemplateTests/BlazorServerTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public async Task BlazorServerItemTemplate()
2929
{
3030
Project = await ProjectFactory.CreateProject(Output);
3131

32-
await Project.RunDotNetNewAsync("razorcomponent --name Different", isItemTemplate: true);
32+
await Project.RunDotNetNewAsync("razorcomponent", isItemTemplate: true, args: ["--name", "Different"]);
3333

3434
Project.AssertFileExists("Different.razor", shouldExist: true);
3535
Assert.Contains("<h3>Different</h3>", Project.ReadFile("Different.razor"));

0 commit comments

Comments
 (0)