Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
79 changes: 79 additions & 0 deletions docs/fundamentals/custom-resource-urls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
title: Custom resource URLs
description: Learn how to create custom URLs for .NET Aspire resources.
ms.date: 04/08/2025
ms.topic: how-to
---

# Custom resource URLs

.NET Aspire resources that expose endpoints only configure host and port values. However, there might be situations where you want to access a specific route of an exposed endpoint. The host and port are unknown until run time. In these cases, you can use custom resource URLs to define specific routes on a configured endpoint, which is convenient for accessing resources from the [dashboard](dashboard/overview.md).

## Default endpoint behavior

By default, as described in the [Networking inner loop](networking-overview.md#networking-in-the-inner-loop) article, .NET Aspire relies on existing configurations such as Kestrel or launch profiles to determine the host and port of a resource for a configured endpoint.

Likewise, you can explicitly expose endpoints using the <xref:Aspire.Hosting.ResourceBuilderExtensions.WithEndpoint*> API. This API allows you to specify the host and port for a resource, which is then used to create the default URL for that resource. The default URL is typically in the format `http://<host>:<port>` or `https://<host>:<port>`, depending on the protocol used. To omit the host port, use one of the following methods:

- <xref:Aspire.Hosting.ResourceBuilderExtensions.WithHttpEndpoint*>
- <xref:Aspire.Hosting.ResourceBuilderExtensions.WithHttpsEndpoint*>

For more information, see [Endpoint extension methods](networking-overview.md#endpoint-extension-methods).

## Supported resource types

Currently, custom resource URLs are supported for the following resource types:

- <xref:Aspire.Hosting.ApplicationModel.ContainerResource>
- <xref:Aspire.Hosting.ApplicationModel.ExecutableResource>
- <xref:Aspire.Hosting.ApplicationModel.ProjectResource>

## Customize resource URLs

Use the appropriate `WithUrl` overload, `WithUrls`, or `WithUrlForEndpoint` APIs on any supported resource builder to define custom URLs for a resource. The following example demonstrates how to set a custom URL for a project resource:

:::code source="snippets/custom-urls/AspireApp.AppHost/Program.WithUrl.cs" id="withurl":::

> [!TIP]
> There's an overload that accepts a `string` allowing you to pass any URL. This is useful for defining custom URLs that aren't directly related to the resource's endpoint.

The preceding code assigns a project reference to the `api` variable, which is then used to create a custom URL for the `Admin Portal` route. The `WithUrl` method takes a <xref:Aspire.Hosting.ApplicationModel.ReferenceExpression> and a display name as parameters. The resulting URL is available in the dashboard as shown in the following screenshot:

:::image type="content" source="dashboard/media/custom-urls/custom-url-admin-portal.png" alt-text=".NET Aspire dashboard custom Admin Portal URL." lightbox="dashboard/media/custom-urls/custom-url-admin-portal.png":::

### Customize endpoint URL

<!-- TODO: Add xref to WithUrlForEndpoint when available -->

To expose specific endpoints like Scalar or Swagger in the dashboard, use the `WithUrlForEndpoint` method. The following example demonstrates how to customize the URL for a resource endpoint:

:::code source="snippets/custom-urls/AspireApp.AppHost/Program.WithUrlForEndpoint.cs" id="withurlforendpoint":::

<!-- TODO: Add xref to ResourceUrlAnnotation when available -->

The preceding example assumes that the `api` project resource has an `https` endpoint configured. The `WithUrlForEndpoint` method updates the `ResourceUrlAnnotation` for the endpoint, in this case assigning the display text to `Scalar (HTTPS)` and appends the `/scalar` path to the URL.

When the resource is started, the URL is available in the dashboard as shown in the following screenshot:

:::image type="content" source="dashboard/media/custom-urls/custom-url-scalar-https.png" alt-text=".NET Aspire dashboard with custom Scalar URL." lightbox="dashboard/media/custom-urls/custom-url-scalar-https.png":::

### Customize multiple resource URLs

<!-- TODO: Add xref to WithUrls when available -->

To customize multiple URLs for a resource, use the `WithUrls` method. This method allows you to specify multiple URLs for a resource, each with its own display text. The following example demonstrates how to set multiple URLs for a project resource:

:::code source="snippets/custom-urls/AspireApp.AppHost/Program.WithUrls.cs" id="withurls":::

The preceding code iterates through the URLs defined for the `api` project resource and assigns a display text and order to each URL. The resulting URLs are available in the dashboard as shown in the following screenshot:

:::image type="content" source="dashboard/media/custom-urls/custom-url-ordered.png" alt-text=".NET Aspire dashboard custom ordered and named URLs.":::

## URL customization lifecycle

URL customization callbacks run during the application model lifecycle, specifically during the <xref:Aspire.Hosting.ApplicationModel.BeforeResourceStartedEvent> event processing. URLs associated with endpoints become active and appear on the dashboard once the endpoint itself becomes active. URLs not associated with endpoints become active only when the resource enters the "Running" state. This ensures that all custom URLs are accurately represented and available when the application resources are fully operational.

## See also

- [.NET Aspire dashboard overview](./overview.md)
- [.NET Aspire app host](../app-host.md)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.3" />
<PackageReference Include="Scalar.AspNetCore" Version="2.1.7" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@AspireApp.Api_HostAddress = http://localhost:5020

GET {{AspireApp.Api_HostAddress}}/weatherforecast/
Accept: application/json

###
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.35906.104
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspireApp.Api", "AspireApp.Api.csproj", "{BB5FD48A-DA3C-4E2C-B01B-FF8FB922F4F5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AspireApp.AppHost", "..\AspireApp.AppHost\AspireApp.AppHost.csproj", "{DC5D0ECC-8CBC-4436-B1DC-3D2681DD4B50}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BB5FD48A-DA3C-4E2C-B01B-FF8FB922F4F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BB5FD48A-DA3C-4E2C-B01B-FF8FB922F4F5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BB5FD48A-DA3C-4E2C-B01B-FF8FB922F4F5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BB5FD48A-DA3C-4E2C-B01B-FF8FB922F4F5}.Release|Any CPU.Build.0 = Release|Any CPU
{DC5D0ECC-8CBC-4436-B1DC-3D2681DD4B50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DC5D0ECC-8CBC-4436-B1DC-3D2681DD4B50}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC5D0ECC-8CBC-4436-B1DC-3D2681DD4B50}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DC5D0ECC-8CBC-4436-B1DC-3D2681DD4B50}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6D149486-7C78-4537-B183-314816378E9C}
EndGlobalSection
EndGlobal
120 changes: 120 additions & 0 deletions docs/fundamentals/snippets/custom-urls/AspireApp.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using Scalar.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference();
}

app.UseHttpsRedirection();

var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast");

app.MapGet("/admin", () =>
{
return Results.Content("""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Admin Portal Login</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body style="
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', sans-serif;
background: linear-gradient(to right, #667eea, #764ba2);
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
">

<div style="
background-color: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
">
<h2 style="
margin-bottom: 1.5rem;
text-align: center;
color: #333;
">Admin Portal</h2>

<form>
<label for="email" style="display: block; margin-bottom: 0.5rem; color: #555;">Email</label>
<input type="email" id="email" name="email" placeholder="[email protected]" style="
width: 100%;
padding: 0.75rem;
margin-bottom: 1rem;
border: 1px solid #ccc;
border-radius: 8px;
font-size: 1rem;
" required>

<label for="password" style="display: block; margin-bottom: 0.5rem; color: #555;">Password</label>
<input type="password" id="password" name="password" placeholder="••••••••" style="
width: 100%;
padding: 0.75rem;
margin-bottom: 1.5rem;
border: 1px solid #ccc;
border-radius: 8px;
font-size: 1rem;
" required>

<button type="submit" style="
width: 100%;
padding: 0.75rem;
background-color: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s ease;
">Login</button>
</form>
</div>

</body>
</html>
""", "text/html");
});

app.Run();

internal record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5020",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7233;http://localhost:5020",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<Sdk Name="Aspire.AppHost.Sdk" Version="9.2.0-preview.1.25208.5" />

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>31d1f6cf-dfb5-4923-b92a-6c0e9e032f15</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" Version="9.2.0-preview.1.25208.5" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AspireApp.Api\AspireApp.Api.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
internal static partial class Program
{
internal static void WithUrlExample(string[] args)
{
// <withurl>
var builder = DistributedApplication.CreateBuilder(args);

var api = builder.AddProject<Projects.AspireApp_Api>("api");

api.WithUrl($"{api.GetEndpoint("https")}/admin"), "Admin Portal");

builder.Build().Run();
// </withurl>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
internal static partial class Program
{
internal static void WithUrlForEndpointExample(string[] args)
{
// <withurlforendpoint>
var builder = DistributedApplication.CreateBuilder(args);

builder.AddProject<Projects.AspireApp_Api>("api")
.WithUrlForEndpoint("https", url =>
{
url.DisplayText = "Scalar (HTTPS)";
url.Url += "/scalar";
});

builder.Build().Run();
// </withurlforendpoint>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
internal static partial class Program
{
internal static void WithUrlsExample(string[] args)
{
// <withurls>
var builder = DistributedApplication.CreateBuilder(args);

builder.AddProject<Projects.AspireApp_Api>("api")
.WithUrls(context =>
{
foreach (var tuple in context.Urls
.Select(url => (Url: url, Uri: new Uri(url.Url)))
.OrderByDescending(_ => _.Uri.Scheme is "https")
.Select((pair, index) => (pair.Url, pair.Uri.Scheme, Index: index)))
{
var (url, scheme, index) = tuple;

// Order HTTPS first.
var order = context.Urls.Count - 1 - index;

url.DisplayText = $"{index + 1}. {scheme.ToUpper()}";
url.DisplayOrder = order;
}
});

builder.Build().Run();
// </withurls>
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Uncomment a single line to run the example

// Admin Portal
WithUrlExample(args);

// Ordered / Schemes
//WithUrlsExample(args);

// Scalar (HTTPS)
//WithUrlForEndpointExample(args);
Loading
Loading