Skip to content

Commit 1c1c7b4

Browse files
DamianEdwardsradical
authored andcommitted
Update the templates for .NET 10 (#12267)
* Update templates for .NET 10 * Ensure *.localhost resource URLs are given priority in dashboard * More template config tweaks * Fix localhosttld option in VS * More fixes & enable *.localhost for python starter * Fix launch profile URL in aspire empty template * Simplify template aspire version config * Update to OTel 1.13.x * Use EndpointHostHelpers
1 parent 67144f8 commit 1c1c7b4

File tree

169 files changed

+771
-2241
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

169 files changed

+771
-2241
lines changed

eng/Versions.props

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,11 @@
8989
<SystemFormatsAsn1Version>9.0.10</SystemFormatsAsn1Version>
9090
<SystemTextJsonVersion>9.0.10</SystemTextJsonVersion>
9191
<!-- OpenTelemetry (OTel) -->
92-
<OpenTelemetryInstrumentationAspNetCoreVersion>1.12.0</OpenTelemetryInstrumentationAspNetCoreVersion>
93-
<OpenTelemetryInstrumentationHttpVersion>1.12.0</OpenTelemetryInstrumentationHttpVersion>
94-
<OpenTelemetryInstrumentationExtensionsHostingVersion>1.12.0</OpenTelemetryInstrumentationExtensionsHostingVersion>
95-
<OpenTelemetryInstrumentationRuntimeVersion>1.12.0</OpenTelemetryInstrumentationRuntimeVersion>
96-
<OpenTelemetryExporterOpenTelemetryProtocolVersion>1.12.0</OpenTelemetryExporterOpenTelemetryProtocolVersion>
92+
<OpenTelemetryInstrumentationAspNetCoreVersion>1.13.0</OpenTelemetryInstrumentationAspNetCoreVersion>
93+
<OpenTelemetryInstrumentationHttpVersion>1.13.0</OpenTelemetryInstrumentationHttpVersion>
94+
<OpenTelemetryInstrumentationExtensionsHostingVersion>1.13.1</OpenTelemetryInstrumentationExtensionsHostingVersion>
95+
<OpenTelemetryInstrumentationRuntimeVersion>1.13.0</OpenTelemetryInstrumentationRuntimeVersion>
96+
<OpenTelemetryExporterOpenTelemetryProtocolVersion>1.13.1</OpenTelemetryExporterOpenTelemetryProtocolVersion>
9797
</PropertyGroup>
9898
<!-- .NET 8.0 Package Versions -->
9999
<PropertyGroup Label="LTS">

playground/FileBasedApps/FileBasedApps.WebFrontEnd/Properties/launchSettings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"commandName": "Project",
1515
"dotnetRunMessages": true,
1616
"launchBrowser": true,
17+
"launchUrl": "/counter",
1718
"applicationUrl": "https://localhost:7009;http://localhost:5117",
1819
"environmentVariables": {
1920
"ASPNETCORE_ENVIRONMENT": "Development"

playground/FileBasedApps/apphost.run.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"commandName": "Project",
66
"dotnetRunMessages": true,
77
"launchBrowser": true,
8-
"applicationUrl": "https://localhost:17123;http://localhost:15234",
8+
"applicationUrl": "https://filebasedapps.dev.localhost:17123;http://filebasedapps.dev.localhost:15234",
99
"environmentVariables": {
1010
"ASPNETCORE_ENVIRONMENT": "Development",
1111
"DOTNET_ENVIRONMENT": "Development",
@@ -17,7 +17,7 @@
1717
"commandName": "Project",
1818
"dotnetRunMessages": true,
1919
"launchBrowser": true,
20-
"applicationUrl": "http://localhost:15234",
20+
"applicationUrl": "http://filebasedapps.dev.localhost:15234",
2121
"environmentVariables": {
2222
"ASPNETCORE_ENVIRONMENT": "Development",
2323
"DOTNET_ENVIRONMENT": "Development",

src/Aspire.Hosting/Dcp/DcpExecutor.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,11 +1048,13 @@ private void PrepareProjectExecutables()
10481048
// `dotnet watch` does not work with file-based apps yet, so we have to use `dotnet run` in that case
10491049
if (_configuration.GetBool("DOTNET_WATCH") is not true || projectMetadata.IsFileBasedApp)
10501050
{
1051-
projectArgs.AddRange([
1052-
"run",
1053-
projectMetadata.IsFileBasedApp ? "--file" : "--project",
1054-
projectMetadata.ProjectPath,
1055-
]);
1051+
projectArgs.Add("run");
1052+
projectArgs.Add(projectMetadata.IsFileBasedApp ? "--file" : "--project");
1053+
projectArgs.Add(projectMetadata.ProjectPath);
1054+
if (projectMetadata.IsFileBasedApp)
1055+
{
1056+
projectArgs.Add("--no-cache");
1057+
}
10561058
if (projectMetadata.SuppressBuild)
10571059
{
10581060
projectArgs.Add("--no-build");

src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
using System.Diagnostics;
99
using Aspire.Dashboard.Model;
1010
using Aspire.Hosting.ApplicationModel;
11+
using Aspire.Hosting.Dashboard;
1112
using Aspire.Hosting.Dcp;
1213
using Aspire.Hosting.Eventing;
1314
using Aspire.Hosting.Lifecycle;
15+
using Aspire.Hosting.Utils;
16+
using Microsoft.Extensions.Options;
1417

1518
namespace Aspire.Hosting.Orchestrator;
1619

@@ -26,6 +29,7 @@ internal sealed class ApplicationOrchestrator
2629
private readonly ResourceLoggerService _loggerService;
2730
private readonly IDistributedApplicationEventing _eventing;
2831
private readonly IServiceProvider _serviceProvider;
32+
private readonly Uri? _dashboardUri;
2933
private readonly DistributedApplicationExecutionContext _executionContext;
3034
private readonly ParameterProcessor _parameterProcessor;
3135
private readonly CancellationTokenSource _shutdownCancellation = new();
@@ -41,7 +45,8 @@ public ApplicationOrchestrator(DistributedApplicationModel model,
4145
IDistributedApplicationEventing eventing,
4246
IServiceProvider serviceProvider,
4347
DistributedApplicationExecutionContext executionContext,
44-
ParameterProcessor parameterProcessor)
48+
ParameterProcessor parameterProcessor,
49+
IOptions<DashboardOptions> dashboardOptions)
4550
{
4651
_dcpExecutor = dcpExecutor;
4752
_model = model;
@@ -53,6 +58,8 @@ public ApplicationOrchestrator(DistributedApplicationModel model,
5358
_serviceProvider = serviceProvider;
5459
_executionContext = executionContext;
5560
_parameterProcessor = parameterProcessor;
61+
var dashboardUrl = dashboardOptions.Value.DashboardUrl?.Split(';', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
62+
Uri.TryCreate(dashboardUrl, UriKind.Absolute, out _dashboardUri);
5663

5764
dcpExecutorEvents.Subscribe<OnResourcesPreparedContext>(OnResourcesPrepared);
5865
dcpExecutorEvents.Subscribe<OnResourceChangedContext>(OnResourceChanged);
@@ -206,6 +213,7 @@ private async Task OnResourcesPrepared(OnResourcesPreparedContext context)
206213
private async Task ProcessResourceUrlCallbacks(IResource resource, CancellationToken cancellationToken)
207214
{
208215
var urls = new List<ResourceUrlAnnotation>();
216+
EndpointAnnotation? primaryLaunchProfileEndpoint = null;
209217

210218
// Project endpoints to URLs
211219
if (resource.TryGetEndpoints(out var endpoints) && resource is IResourceWithEndpoints resourceWithEndpoints)
@@ -216,6 +224,11 @@ private async Task ProcessResourceUrlCallbacks(IResource resource, CancellationT
216224
Debug.Assert(endpoint.AllocatedEndpoint is not null, "Endpoint should be allocated at this point as we're calling this from ResourceEndpointsAllocatedEvent handler.");
217225
if (endpoint.AllocatedEndpoint is { } allocatedEndpoint)
218226
{
227+
if (endpoint.FromLaunchProfile && primaryLaunchProfileEndpoint is null)
228+
{
229+
primaryLaunchProfileEndpoint = endpoint;
230+
}
231+
219232
// The allocated endpoint is used for service discovery and is the primary URL displayed to
220233
// the user. In general, if valid for a particular service binding, the allocated endpoint
221234
// will be "localhost" as that's a valid address for the .NET developer certificate. However,
@@ -224,8 +237,6 @@ private async Task ProcessResourceUrlCallbacks(IResource resource, CancellationT
224237
var endpointReference = new EndpointReference(resourceWithEndpoints, endpoint);
225238
var url = new ResourceUrlAnnotation { Url = allocatedEndpoint.UriString, Endpoint = endpointReference };
226239

227-
urls.Add(url);
228-
229240
// In the case that a service is bound to multiple addresses or a *.localhost address, we generate
230241
// additional URLs to indicate to the user other ways their service can be reached. If the service
231242
// is bound to all interfaces (0.0.0.0, ::, etc.) we use the machine name as the additional
@@ -251,6 +262,50 @@ private async Task ProcessResourceUrlCallbacks(IResource resource, CancellationT
251262
},
252263
};
253264

265+
if (additionalUrl is not null && EndpointHostHelpers.IsLocalhostTld(additionalUrl.Endpoint?.EndpointAnnotation.TargetHost))
266+
{
267+
// If the additional URL is a *.localhost address we want to highlight that URL in the dashboard
268+
additionalUrl.DisplayLocation = UrlDisplayLocation.SummaryAndDetails;
269+
url.DisplayLocation = UrlDisplayLocation.DetailsOnly;
270+
}
271+
else if ((string.Equals(endpoint.UriScheme, "http", StringComparison.OrdinalIgnoreCase) || string.Equals(endpoint.UriScheme, "https", StringComparison.OrdinalIgnoreCase))
272+
&& additionalUrl is null && EndpointHostHelpers.IsDevLocalhostTld(_dashboardUri))
273+
{
274+
// For HTTP endpoints, if the endpoint target host has not already resulted in an additional URL and the dashboard URL is using a *.dev.localhost address,
275+
// we want to assign a *.dev.localhost address to every HTTP resource endpoint based on the dashboard URL.
276+
// This allows users to access their services from the dashboard using a consistent pattern.
277+
var subdomainSuffix = _dashboardUri.Host[.._dashboardUri.Host.IndexOf(".dev.localhost", StringComparison.OrdinalIgnoreCase)];
278+
// Strip any "apphost" suffix that might be present on the dashboard name.
279+
subdomainSuffix = TrimSuffix(subdomainSuffix, "apphost");
280+
281+
additionalUrl = new ResourceUrlAnnotation
282+
{
283+
// <scheme>://<resource-name>-<subdomain-suffix>.dev.localhost:<port>
284+
Url = $"{allocatedEndpoint.UriScheme}://{resource.Name.ToLowerInvariant()}-{subdomainSuffix}.dev.localhost:{allocatedEndpoint.Port}",
285+
Endpoint = endpointReference,
286+
DisplayLocation = UrlDisplayLocation.SummaryAndDetails
287+
};
288+
url.DisplayLocation = UrlDisplayLocation.DetailsOnly;
289+
290+
static string TrimSuffix(string value, string suffix)
291+
{
292+
char[] separators = ['-', '_', '.'];
293+
Span<char> suffixSpan = stackalloc char[suffix.Length + 1];
294+
foreach (var separator in separators)
295+
{
296+
suffixSpan[0] = separator;
297+
suffix.CopyTo(suffixSpan[1..]);
298+
if (value.EndsWith(suffixSpan, StringComparison.OrdinalIgnoreCase))
299+
{
300+
return value[..^suffixSpan.Length];
301+
}
302+
}
303+
304+
return value;
305+
}
306+
}
307+
308+
urls.Add(url);
254309
if (additionalUrl is not null)
255310
{
256311
urls.Add(additionalUrl);
@@ -284,6 +339,34 @@ private async Task ProcessResourceUrlCallbacks(IResource resource, CancellationT
284339
}
285340
}
286341

342+
// Apply path from primary launch profile endpoint URL to additional launch profile endpoint URLs.
343+
// This needs to happen after running URL callbacks as the application of the launch profile launchUrl happens in a callback.
344+
if (primaryLaunchProfileEndpoint is not null)
345+
{
346+
// Matches URL lookup logic in ProjectResourceBuilderExtensions.WithProjectDefaults
347+
var primaryUrl = urls.FirstOrDefault(u => string.Equals(u.Endpoint?.EndpointName, primaryLaunchProfileEndpoint.Name, StringComparisons.EndpointAnnotationName));
348+
if (primaryUrl is not null)
349+
{
350+
var primaryUri = new Uri(primaryUrl.Url);
351+
var primaryPath = primaryUri.AbsolutePath;
352+
353+
if (primaryPath != "/")
354+
{
355+
foreach (var url in urls)
356+
{
357+
if (url.Endpoint?.EndpointAnnotation == primaryLaunchProfileEndpoint && !string.Equals(url.Url, primaryUrl.Url, StringComparisons.Url))
358+
{
359+
var uriBuilder = new UriBuilder(url.Url)
360+
{
361+
Path = primaryPath
362+
};
363+
url.Url = uriBuilder.Uri.ToString();
364+
}
365+
}
366+
}
367+
}
368+
}
369+
287370
// Convert relative endpoint URLs to absolute URLs
288371
foreach (var url in urls)
289372
{

src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1029,7 +1029,7 @@ private static void SetKestrelUrlOverrideEnvVariables(this IResourceBuilder<Proj
10291029

10301030
private static string ParseKestrelHost(string host)
10311031
{
1032-
if (string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase))
1032+
if (EndpointHostHelpers.IsLocalhost(host))
10331033
{
10341034
// Localhost is used as-is rather than being resolved to a specific loopback IP address.
10351035
return "localhost";

src/Aspire.Hosting/Utils/EndpointHostHelpers.cs

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
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.Diagnostics.CodeAnalysis;
5+
46
namespace Aspire.Hosting.Utils;
57

68
/// <summary>
@@ -15,23 +17,71 @@ public static class EndpointHostHelpers
1517
/// <returns>
1618
/// <c>true</c> if the host is "localhost" (case-insensitive); otherwise, <c>false</c>.
1719
/// </returns>
18-
public static bool IsLocalhost(string? host)
20+
public static bool IsLocalhost([NotNullWhen(true)] string? host)
1921
{
2022
return host is not null && string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase);
2123
}
2224

25+
/// <summary>
26+
/// Determines whether the specified URI uses a host that is "localhost".
27+
/// </summary>
28+
/// <param name="uri">The URI to check.</param>
29+
/// <returns>
30+
/// <c>true</c> if the host is "localhost" (case-insensitive); otherwise, <c>false</c>.
31+
/// </returns>
32+
public static bool IsLocalhost([NotNullWhen(true)] Uri? uri)
33+
{
34+
return uri?.Host is not null && IsLocalhost(uri.Host);
35+
}
36+
2337
/// <summary>
2438
/// Determines whether the specified host ends with ".localhost".
2539
/// </summary>
2640
/// <param name="host">The host to check.</param>
2741
/// <returns>
2842
/// <c>true</c> if the host ends with ".localhost" (case-insensitive); otherwise, <c>false</c>.
2943
/// </returns>
30-
public static bool IsLocalhostTld(string? host)
44+
public static bool IsLocalhostTld([NotNullWhen(true)] string? host)
3145
{
3246
return host is not null && host.EndsWith(".localhost", StringComparison.OrdinalIgnoreCase);
3347
}
3448

49+
/// <summary>
50+
/// Determines whether the specified host ends with ".dev.localhost".
51+
/// </summary>
52+
/// <param name="host">The host to check.</param>
53+
/// <returns>
54+
/// <c>true</c> if the host ends with ".dev.localhost" (case-insensitive); otherwise, <c>false</c>.
55+
/// </returns>
56+
public static bool IsDevLocalhostTld([NotNullWhen(true)] string? host)
57+
{
58+
return host is not null && host.EndsWith(".dev.localhost", StringComparison.OrdinalIgnoreCase);
59+
}
60+
61+
/// <summary>
62+
/// Determines whether the specified URI uses a host that is "localhost".
63+
/// </summary>
64+
/// <param name="uri">The URI to check.</param>
65+
/// <returns>
66+
/// <c>true</c> if the host ends with ".localhost" (case-insensitive); otherwise, <c>false</c>.
67+
/// </returns>
68+
public static bool IsLocalhostTld([NotNullWhen(true)] Uri? uri)
69+
{
70+
return uri?.Host is not null && IsLocalhostTld(uri.Host);
71+
}
72+
73+
/// <summary>
74+
/// Determines whether the specified URI uses a host that ends with ".dev.localhost".
75+
/// </summary>
76+
/// <param name="uri">The URI to check.</param>
77+
/// <returns>
78+
/// <c>true</c> if the host ends with ".dev.localhost" (case-insensitive); otherwise, <c>false</c>.
79+
/// </returns>
80+
public static bool IsDevLocalhostTld([NotNullWhen(true)] Uri? uri)
81+
{
82+
return uri?.Host is not null && IsDevLocalhostTld(uri.Host);
83+
}
84+
3585
/// <summary>
3686
/// Determines whether the specified host is "localhost" or uses the ".localhost" top-level domain.
3787
/// </summary>
@@ -40,8 +90,21 @@ public static bool IsLocalhostTld(string? host)
4090
/// <c>true</c> if the host is "localhost" (case-insensitive) or ends with ".localhost" (case-insensitive);
4191
/// otherwise, <c>false</c>.
4292
/// </returns>
43-
public static bool IsLocalhostOrLocalhostTld(string? host)
93+
public static bool IsLocalhostOrLocalhostTld([NotNullWhen(true)] string? host)
4494
{
4595
return IsLocalhost(host) || IsLocalhostTld(host);
4696
}
97+
98+
/// <summary>
99+
/// Determines whether the specified URI uses a host that is "localhost" or ends with ".localhost".
100+
/// </summary>
101+
/// <param name="uri"></param>
102+
/// <returns>
103+
/// <c>true</c> if the host is "localhost" (case-insensitive) or ends with ".localhost" (case-insensitive);
104+
/// otherwise, <c>false</c>.
105+
/// </returns>
106+
public static bool IsLocalhostOrLocalhostTld([NotNullWhen(true)] Uri? uri)
107+
{
108+
return uri?.Host is not null && IsLocalhostOrLocalhostTld(uri.Host);
109+
}
47110
}

src/Aspire.ProjectTemplates/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ For each template:
2323
2. **Copy** content folder named for old current version to a new folder named for new current version, e.g. *./9.4* -> *./9.5*
2424
3. Edit *./.template.config/template.json* and replace instances of old latest version with new latest version, e.g. `9.4` -> `9.5`
2525
4. Edit *./.template.config/template.json* and replace instances of old previous version with new previous version, e.g. `9.3` -> `9.4`
26-
5. If supported TFMs changed between old previous version and new previous version, or old current version and new current version, update `AspireVersionNetX` options appropriately. Note that the `AspireVersion` option maps to the `net8.0` TFM.
26+
5. If supported TFMs changed between old previous version and new previous version, or old current version and new current version, add or update `AspireNetXVersion` options appropriately. Note that the `AspireVersion` option maps to the `net8.0` TFM.
2727
6. In all *.csproj* files in the content folder named for the new previous version, e.g. *./9.4/**/*.csproj*:
2828
1. Update all versions for Aspire-produced packages (and SDKs) referenced to the new previous package version (`major.minor.patch` for latest patch), replacing the replacement token value with a static version value, e.g. `!!REPLACE_WITH_LATEST_VERSION!!` -> `9.4.2`
2929
2. Update all versions for non-Aspire packages to the version referenced by current released version of the template, replacing the replacement token value with the relevant static version value, e.g. `!!REPLACE_WITH_ASPNETCORE_10_VERSION!!` -> `10.0.0-preview.7.25380.108`. Some non-Aspire packages referenced don't use a replacement token and instead just use a static value. In these cases simply leave the value as is.
30-
30+
3131
**Note:** There's a few ways to determine the static version value:
3232
- Look at the contents of the latest released version of the templates package at https://nuget.info/packages/Aspire.ProjectTemplates and find the version from the relvant *.csproj* file in the template package content
3333
- Checkout the relevant `release/X.X` branch for the latest public release, e.g. `release/9.4`, and in the *./src/Aspire.ProjectTemplates/* directory, run the `dotnet` CLI command to extract the appropriate version from the build system, e.g. `dotnet msbuild -getProperty:MicrosoftAspNetCorePackageVersionForNet9`. The property name to pass for a given replacement token can be determined by looking in the *./src/Aspire.ProjectTemplates/Aspire.ProjectTemplates.csproj* file, at the `<WriteLinesToFile ...>` task, which should look something like the following:

0 commit comments

Comments
 (0)