Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,11 @@
<SystemFormatsAsn1Version>9.0.10</SystemFormatsAsn1Version>
<SystemTextJsonVersion>9.0.10</SystemTextJsonVersion>
<!-- OpenTelemetry (OTel) -->
<OpenTelemetryInstrumentationAspNetCoreVersion>1.12.0</OpenTelemetryInstrumentationAspNetCoreVersion>
<OpenTelemetryInstrumentationHttpVersion>1.12.0</OpenTelemetryInstrumentationHttpVersion>
<OpenTelemetryInstrumentationExtensionsHostingVersion>1.12.0</OpenTelemetryInstrumentationExtensionsHostingVersion>
<OpenTelemetryInstrumentationRuntimeVersion>1.12.0</OpenTelemetryInstrumentationRuntimeVersion>
<OpenTelemetryExporterOpenTelemetryProtocolVersion>1.12.0</OpenTelemetryExporterOpenTelemetryProtocolVersion>
<OpenTelemetryInstrumentationAspNetCoreVersion>1.13.0</OpenTelemetryInstrumentationAspNetCoreVersion>
<OpenTelemetryInstrumentationHttpVersion>1.13.0</OpenTelemetryInstrumentationHttpVersion>
<OpenTelemetryInstrumentationExtensionsHostingVersion>1.13.1</OpenTelemetryInstrumentationExtensionsHostingVersion>
<OpenTelemetryInstrumentationRuntimeVersion>1.13.0</OpenTelemetryInstrumentationRuntimeVersion>
<OpenTelemetryExporterOpenTelemetryProtocolVersion>1.13.1</OpenTelemetryExporterOpenTelemetryProtocolVersion>
</PropertyGroup>
<!-- .NET 8.0 Package Versions -->
<PropertyGroup Label="LTS">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "/counter",
"applicationUrl": "https://localhost:7009;http://localhost:5117",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
Expand Down
4 changes: 2 additions & 2 deletions playground/FileBasedApps/apphost.run.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:17123;http://localhost:15234",
"applicationUrl": "https://filebasedapps.dev.localhost:17123;http://filebasedapps.dev.localhost:15234",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
Expand All @@ -17,7 +17,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:15234",
"applicationUrl": "http://filebasedapps.dev.localhost:15234",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
Expand Down
12 changes: 7 additions & 5 deletions src/Aspire.Hosting/Dcp/DcpExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1048,11 +1048,13 @@ private void PrepareProjectExecutables()
// `dotnet watch` does not work with file-based apps yet, so we have to use `dotnet run` in that case
if (_configuration.GetBool("DOTNET_WATCH") is not true || projectMetadata.IsFileBasedApp)
{
projectArgs.AddRange([
"run",
projectMetadata.IsFileBasedApp ? "--file" : "--project",
projectMetadata.ProjectPath,
]);
projectArgs.Add("run");
projectArgs.Add(projectMetadata.IsFileBasedApp ? "--file" : "--project");
projectArgs.Add(projectMetadata.ProjectPath);
if (projectMetadata.IsFileBasedApp)
{
projectArgs.Add("--no-cache");
}
if (projectMetadata.SuppressBuild)
{
projectArgs.Add("--no-build");
Expand Down
89 changes: 86 additions & 3 deletions src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
using System.Diagnostics;
using Aspire.Dashboard.Model;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Dashboard;
using Aspire.Hosting.Dcp;
using Aspire.Hosting.Eventing;
using Aspire.Hosting.Lifecycle;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.Options;

namespace Aspire.Hosting.Orchestrator;

Expand All @@ -26,6 +29,7 @@ internal sealed class ApplicationOrchestrator
private readonly ResourceLoggerService _loggerService;
private readonly IDistributedApplicationEventing _eventing;
private readonly IServiceProvider _serviceProvider;
private readonly Uri? _dashboardUri;
private readonly DistributedApplicationExecutionContext _executionContext;
private readonly ParameterProcessor _parameterProcessor;
private readonly CancellationTokenSource _shutdownCancellation = new();
Expand All @@ -41,7 +45,8 @@ public ApplicationOrchestrator(DistributedApplicationModel model,
IDistributedApplicationEventing eventing,
IServiceProvider serviceProvider,
DistributedApplicationExecutionContext executionContext,
ParameterProcessor parameterProcessor)
ParameterProcessor parameterProcessor,
IOptions<DashboardOptions> dashboardOptions)
{
_dcpExecutor = dcpExecutor;
_model = model;
Expand All @@ -53,6 +58,8 @@ public ApplicationOrchestrator(DistributedApplicationModel model,
_serviceProvider = serviceProvider;
_executionContext = executionContext;
_parameterProcessor = parameterProcessor;
var dashboardUrl = dashboardOptions.Value.DashboardUrl?.Split(';', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault();
Uri.TryCreate(dashboardUrl, UriKind.Absolute, out _dashboardUri);

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

// Project endpoints to URLs
if (resource.TryGetEndpoints(out var endpoints) && resource is IResourceWithEndpoints resourceWithEndpoints)
Expand All @@ -216,6 +224,11 @@ 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)
{
if (endpoint.FromLaunchProfile && primaryLaunchProfileEndpoint is null)
{
primaryLaunchProfileEndpoint = endpoint;
}

// The allocated endpoint is used for service discovery and is the primary URL displayed to
// the user. In general, if valid for a particular service binding, the allocated endpoint
// will be "localhost" as that's a valid address for the .NET developer certificate. However,
Expand All @@ -224,8 +237,6 @@ private async Task ProcessResourceUrlCallbacks(IResource resource, CancellationT
var endpointReference = new EndpointReference(resourceWithEndpoints, endpoint);
var url = new ResourceUrlAnnotation { Url = allocatedEndpoint.UriString, Endpoint = endpointReference };

urls.Add(url);

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

if (additionalUrl is not null && EndpointHostHelpers.IsLocalhostTld(additionalUrl.Endpoint?.EndpointAnnotation.TargetHost))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes are surprising in this PR. Are these intentional?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. This is to optimize what's shown in the dashboard for resources using *.localhost URLs, which includes the Blazor app in the Aspire starter template. Without this change, you get four URLs shown on the resources summary page, and the https://localhost:1234 URL is the first one shown, which is not optimal. This changes it so that the summary page only shows the *.localhost URLs if they're used, and moves the localhost URLs to the details pane.

{
// If the additional URL is a *.localhost address we want to highlight that URL in the dashboard
additionalUrl.DisplayLocation = UrlDisplayLocation.SummaryAndDetails;
url.DisplayLocation = UrlDisplayLocation.DetailsOnly;
}
else if ((string.Equals(endpoint.UriScheme, "http", StringComparison.OrdinalIgnoreCase) || string.Equals(endpoint.UriScheme, "https", StringComparison.OrdinalIgnoreCase))
&& additionalUrl is null && EndpointHostHelpers.IsDevLocalhostTld(_dashboardUri))
{
// 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,
// we want to assign a *.dev.localhost address to every HTTP resource endpoint based on the dashboard URL.
// This allows users to access their services from the dashboard using a consistent pattern.
var subdomainSuffix = _dashboardUri.Host[.._dashboardUri.Host.IndexOf(".dev.localhost", StringComparison.OrdinalIgnoreCase)];
// Strip any "apphost" suffix that might be present on the dashboard name.
subdomainSuffix = TrimSuffix(subdomainSuffix, "apphost");

additionalUrl = new ResourceUrlAnnotation
{
// <scheme>://<resource-name>-<subdomain-suffix>.dev.localhost:<port>
Url = $"{allocatedEndpoint.UriScheme}://{resource.Name.ToLowerInvariant()}-{subdomainSuffix}.dev.localhost:{allocatedEndpoint.Port}",
Endpoint = endpointReference,
DisplayLocation = UrlDisplayLocation.SummaryAndDetails
};
url.DisplayLocation = UrlDisplayLocation.DetailsOnly;

static string TrimSuffix(string value, string suffix)
{
char[] separators = ['-', '_', '.'];
Span<char> suffixSpan = stackalloc char[suffix.Length + 1];
foreach (var separator in separators)
{
suffixSpan[0] = separator;
suffix.CopyTo(suffixSpan[1..]);
if (value.EndsWith(suffixSpan, StringComparison.OrdinalIgnoreCase))
{
return value[..^suffixSpan.Length];
}
}

return value;
}
}

urls.Add(url);
if (additionalUrl is not null)
{
urls.Add(additionalUrl);
Expand Down Expand Up @@ -284,6 +339,34 @@ private async Task ProcessResourceUrlCallbacks(IResource resource, CancellationT
}
}

// Apply path from primary launch profile endpoint URL to additional launch profile endpoint URLs.
// This needs to happen after running URL callbacks as the application of the launch profile launchUrl happens in a callback.
if (primaryLaunchProfileEndpoint is not null)
{
// Matches URL lookup logic in ProjectResourceBuilderExtensions.WithProjectDefaults
var primaryUrl = urls.FirstOrDefault(u => string.Equals(u.Endpoint?.EndpointName, primaryLaunchProfileEndpoint.Name, StringComparisons.EndpointAnnotationName));
if (primaryUrl is not null)
{
var primaryUri = new Uri(primaryUrl.Url);
var primaryPath = primaryUri.AbsolutePath;

if (primaryPath != "/")
{
foreach (var url in urls)
{
if (url.Endpoint?.EndpointAnnotation == primaryLaunchProfileEndpoint && !string.Equals(url.Url, primaryUrl.Url, StringComparisons.Url))
{
var uriBuilder = new UriBuilder(url.Url)
{
Path = primaryPath
};
url.Url = uriBuilder.Uri.ToString();
}
}
}
}
}

// Convert relative endpoint URLs to absolute URLs
foreach (var url in urls)
{
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1029,7 +1029,7 @@ private static void SetKestrelUrlOverrideEnvVariables(this IResourceBuilder<Proj

private static string ParseKestrelHost(string host)
{
if (string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase))
if (EndpointHostHelpers.IsLocalhost(host))
{
// Localhost is used as-is rather than being resolved to a specific loopback IP address.
return "localhost";
Expand Down
69 changes: 66 additions & 3 deletions src/Aspire.Hosting/Utils/EndpointHostHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;

namespace Aspire.Hosting.Utils;

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

/// <summary>
/// Determines whether the specified URI uses a host that is "localhost".
/// </summary>
/// <param name="uri">The URI to check.</param>
/// <returns>
/// <c>true</c> if the host is "localhost" (case-insensitive); otherwise, <c>false</c>.
/// </returns>
public static bool IsLocalhost([NotNullWhen(true)] Uri? uri)
{
return uri?.Host is not null && IsLocalhost(uri.Host);
}

/// <summary>
/// Determines whether the specified host ends with ".localhost".
/// </summary>
/// <param name="host">The host to check.</param>
/// <returns>
/// <c>true</c> if the host ends with ".localhost" (case-insensitive); otherwise, <c>false</c>.
/// </returns>
public static bool IsLocalhostTld(string? host)
public static bool IsLocalhostTld([NotNullWhen(true)] string? host)
{
return host is not null && host.EndsWith(".localhost", StringComparison.OrdinalIgnoreCase);
}

/// <summary>
/// Determines whether the specified host ends with ".dev.localhost".
/// </summary>
/// <param name="host">The host to check.</param>
/// <returns>
/// <c>true</c> if the host ends with ".dev.localhost" (case-insensitive); otherwise, <c>false</c>.
/// </returns>
public static bool IsDevLocalhostTld([NotNullWhen(true)] string? host)
{
return host is not null && host.EndsWith(".dev.localhost", StringComparison.OrdinalIgnoreCase);
}

/// <summary>
/// Determines whether the specified URI uses a host that is "localhost".
/// </summary>
/// <param name="uri">The URI to check.</param>
/// <returns>
/// <c>true</c> if the host ends with ".localhost" (case-insensitive); otherwise, <c>false</c>.
/// </returns>
public static bool IsLocalhostTld([NotNullWhen(true)] Uri? uri)
{
return uri?.Host is not null && IsLocalhostTld(uri.Host);
}

/// <summary>
/// Determines whether the specified URI uses a host that ends with ".dev.localhost".
/// </summary>
/// <param name="uri">The URI to check.</param>
/// <returns>
/// <c>true</c> if the host ends with ".dev.localhost" (case-insensitive); otherwise, <c>false</c>.
/// </returns>
public static bool IsDevLocalhostTld([NotNullWhen(true)] Uri? uri)
{
return uri?.Host is not null && IsDevLocalhostTld(uri.Host);
}

/// <summary>
/// Determines whether the specified host is "localhost" or uses the ".localhost" top-level domain.
/// </summary>
Expand All @@ -40,8 +90,21 @@ public static bool IsLocalhostTld(string? host)
/// <c>true</c> if the host is "localhost" (case-insensitive) or ends with ".localhost" (case-insensitive);
/// otherwise, <c>false</c>.
/// </returns>
public static bool IsLocalhostOrLocalhostTld(string? host)
public static bool IsLocalhostOrLocalhostTld([NotNullWhen(true)] string? host)
{
return IsLocalhost(host) || IsLocalhostTld(host);
}

/// <summary>
/// Determines whether the specified URI uses a host that is "localhost" or ends with ".localhost".
/// </summary>
/// <param name="uri"></param>
/// <returns>
/// <c>true</c> if the host is "localhost" (case-insensitive) or ends with ".localhost" (case-insensitive);
/// otherwise, <c>false</c>.
/// </returns>
public static bool IsLocalhostOrLocalhostTld([NotNullWhen(true)] Uri? uri)
{
return uri?.Host is not null && IsLocalhostOrLocalhostTld(uri.Host);
}
}
4 changes: 2 additions & 2 deletions src/Aspire.ProjectTemplates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ For each template:
2. **Copy** content folder named for old current version to a new folder named for new current version, e.g. *./9.4* -> *./9.5*
3. Edit *./.template.config/template.json* and replace instances of old latest version with new latest version, e.g. `9.4` -> `9.5`
4. Edit *./.template.config/template.json* and replace instances of old previous version with new previous version, e.g. `9.3` -> `9.4`
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.
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.
6. In all *.csproj* files in the content folder named for the new previous version, e.g. *./9.4/**/*.csproj*:
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`
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.

**Note:** There's a few ways to determine the static version value:
- 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
- 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:
Expand Down
Loading
Loading