Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
26eb2dd
Add persistent executable support
danegsta May 12, 2026
0df4932
Support persistent project endpoint allocation
danegsta May 15, 2026
54180cc
Unify resource lifetime API
danegsta May 15, 2026
8c49d78
Add DCP monitor process fields
danegsta May 15, 2026
034ef79
Use explicit parent process lifetime API
danegsta May 15, 2026
4e9dea8
Add named resource lifetime APIs
danegsta May 15, 2026
5ea0446
Update API compatibility suppressions
danegsta May 15, 2026
b433333
Fix persistent resource CI regressions
danegsta May 16, 2026
74912d6
Fix DCP endpoint allocation ordering
danegsta May 16, 2026
d6369a6
Harden DCP executable test assertions
danegsta May 17, 2026
ffd554e
Add parent-scoped lifetime E2E coverage
danegsta May 17, 2026
76cdb1d
Fix container tunnel service allocation in tests
danegsta May 18, 2026
2090193
Add shared resource lifetime annotations
danegsta May 18, 2026
de0264a
Stabilize OTLP instance IDs for persistent resources
danegsta May 18, 2026
3f77fa6
Regenerate CodeGeneration snapshots
danegsta May 18, 2026
1f265e0
Stabilize persistent executable certificate test
danegsta May 18, 2026
360f16f
Reject persistent executable replicas
danegsta May 18, 2026
fbfdfa3
Use allocated container port for dev tunnels
danegsta May 18, 2026
2fda9ee
Avoid storing Process handles in lifetime annotations
danegsta May 18, 2026
ffd9279
Merge main into executable persistence
danegsta May 18, 2026
6777519
Avoid unsafe lifetime annotation enumeration
danegsta May 19, 2026
16fac48
Add persistent executable playground coverage
danegsta May 19, 2026
86e52c4
Mark shared lifetime APIs experimental
danegsta May 19, 2026
0247c03
Consolidate persistence lifetime annotations
danegsta May 19, 2026
f24db52
Preserve endpoint proxy API binary signatures
danegsta May 20, 2026
e3b6350
Preserve endpoint annotation constructors
danegsta May 20, 2026
4cfc2a3
Cover proxy support override in DCP
danegsta May 20, 2026
51442c7
Preserve EndpointAnnotation IsProxied signature
danegsta May 20, 2026
2ecc32a
Use public endpoint events for URL processing
danegsta May 20, 2026
f17260b
Restore polyglot container compatibility
danegsta May 20, 2026
f27309b
Revert ATS scanner collision handling change
danegsta May 20, 2026
1f79c9e
Use generalized endpoint proxy export for polyglot
danegsta May 20, 2026
a260954
Merge remote-tracking branch 'origin/main' into danegsta-microsoft/ex…
danegsta May 20, 2026
573c4ca
Merge branch 'main' of github.com:microsoft/aspire into danegsta-micr…
danegsta May 20, 2026
90b8bb4
Make endpoint allocation events sequential
danegsta May 20, 2026
8254cf0
Update ATS and polyglot codegen baselines
danegsta May 20, 2026
f1de1ef
Suppress endpoint proxy ATS return-type break
danegsta May 20, 2026
114ae80
Fix endpoint proxy overload resolution
danegsta May 20, 2026
fd33849
Clean up endpoint proxy export
danegsta May 20, 2026
01ebf38
Update non-TypeScript codegen baselines
danegsta May 20, 2026
aee7987
Preserve endpoint proxy binary compatibility
danegsta May 20, 2026
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
4 changes: 2 additions & 2 deletions playground/AzureAppService/AzureAppService.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

// Testing kv secret refs
var cosmosDb = builder.AddAzureCosmosDB("account")
.RunAsEmulator(c => c.WithLifetime(ContainerLifetime.Persistent));
.RunAsEmulator(c => c.WithPersistentLifetime());

cosmosDb.AddCosmosDatabase("db");

Expand All @@ -24,7 +24,7 @@
var storage = infra.GetProvisionableResources().OfType<StorageAccount>().Single();
storage.AllowBlobPublicAccess = false;
})
.RunAsEmulator(c => c.WithLifetime(ContainerLifetime.Persistent));
.RunAsEmulator(c => c.WithPersistentLifetime());
var blobs = storage.AddBlobs("blobs");

// Testing projects
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@

// Testing volumes
var redis = builder.AddRedis("cache")
.WithLifetime(ContainerLifetime.Persistent)
.WithPersistentLifetime()
.WithDataVolume();

// Testing secret outputs
var cosmosDb = builder.AddAzureCosmosDB("account")
.WithAccessKeyAuthentication()
.RunAsEmulator(c => c.WithLifetime(ContainerLifetime.Persistent));
.RunAsEmulator(c => c.WithPersistentLifetime());

cosmosDb.AddCosmosDatabase("db");

// Testing a connection string
var storage = builder.AddAzureStorage("storage")
.RunAsEmulator(c => c.WithLifetime(ContainerLifetime.Persistent));
.RunAsEmulator(c => c.WithPersistentLifetime());
var blobs = storage.AddBlobs("blobs");

// Testing docker files
Expand Down
2 changes: 1 addition & 1 deletion playground/AzureServiceBus/ServiceBus.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
serviceBus.RunAsEmulator(configure => configure.WithConfiguration(document =>
{
document["UserConfig"]!["Logging"] = new JsonObject { ["Type"] = "Console" };
}).WithLifetime(ContainerLifetime.Persistent));
}).WithPersistentLifetime());

builder.AddProject<Projects.ServiceBusWorker>("worker")
.WithReference(queue).WaitFor(queue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
privateEndpointsSubnet.AddPrivateEndpoint(queues);

var sqlServer = builder.AddAzureSqlServer("sql")
.RunAsContainer(c => c.WithLifetime(ContainerLifetime.Persistent));
.RunAsContainer(c => c.WithPersistentLifetime());
privateEndpointsSubnet.AddPrivateEndpoint(sqlServer);

var db = sqlServer.AddDatabase("sqldb");
Expand Down
2 changes: 1 addition & 1 deletion playground/TestShop/TestShop.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@

var messaging = builder.AddRabbitMQ("messaging")
.WithDataVolume()
.WithLifetime(ContainerLifetime.Persistent)
.WithPersistentLifetime()
.WithManagementPlugin()
.PublishAsContainer();

Expand Down
3 changes: 1 addition & 2 deletions playground/TypeScriptAppHost/apphost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
createBuilder,
refExpr,
EnvironmentCallbackContext,
ContainerLifetime,
ExecuteCommandContext,
InputsDialogValidationContext,
InputType
Expand Down Expand Up @@ -50,7 +49,7 @@ console.log("Added Express API with reference to PostgreSQL database");

// Redis
const cache = await builder.addRedis("cache");
await cache.withLifetime(ContainerLifetime.Persistent);
await cache.withPersistentLifetime();
await cache.withCommand(
"set-prefix",
"Set prefix",
Expand Down
4 changes: 2 additions & 2 deletions playground/TypeScriptApps/RpsArena/apphost.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Rock Paper Scissors Arena — Aspire TypeScript AppHost
// A polyglot game: C# Game Master, Python & Node.js players, React frontend, PostgreSQL

import { createBuilder, ContainerLifetime, type ExecuteCommandContext, type ExecuteCommandResult } from './.modules/aspire.js';
import { createBuilder, type ExecuteCommandContext, type ExecuteCommandResult } from './.modules/aspire.js';

const builder = await createBuilder();

Expand All @@ -10,7 +10,7 @@ const builder = await createBuilder();
// Persistent lifetime so data survives restarts during development.
const postgres = await builder
.addPostgres("postgres")
.withLifetime(ContainerLifetime.Persistent)
.withPersistentLifetime()
.withPgAdmin()
.withDataVolume();

Expand Down
15 changes: 5 additions & 10 deletions src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -273,22 +273,17 @@ public static IResourceBuilder<AzureEventHubsResource> RunAsEmulator(this IResou
.AddAzureStorage($"{builder.Resource.Name}-storage")
.WithParentRelationship(builder);

var lifetime = ContainerLifetime.Session;

// Copy the lifetime from the main resource to the storage resource
var surrogate = new AzureEventHubsEmulatorResource(builder.Resource);
var surrogateBuilder = builder.ApplicationBuilder.CreateResourceBuilder(surrogate);
if (configureContainer != null)
{
configureContainer(surrogateBuilder);

if (surrogate.TryGetLastAnnotation<ContainerLifetimeAnnotation>(out var lifetimeAnnotation))
{
lifetime = lifetimeAnnotation.Lifetime;
}
storageResource = storageResource.RunAsEmulator(c => c.WithLifetimeOf(surrogateBuilder));
}
else
{
storageResource = storageResource.RunAsEmulator();
}

storageResource = storageResource.RunAsEmulator(c => c.WithLifetime(lifetime));

var storage = storageResource.Resource;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -433,23 +433,15 @@ public static IResourceBuilder<AzureServiceBusResource> RunAsEmulator(this IReso
context.EnvironmentVariables["MSSQL_SA_PASSWORD"] = passwordParameter;
}));

var lifetime = ContainerLifetime.Session;

var surrogate = new AzureServiceBusEmulatorResource(builder.Resource);
var surrogateBuilder = builder.ApplicationBuilder.CreateResourceBuilder(surrogate);

if (configureContainer != null)
{
configureContainer(surrogateBuilder);

if (surrogate.TryGetLastAnnotation<ContainerLifetimeAnnotation>(out var lifetimeAnnotation))
{
lifetime = lifetimeAnnotation.Lifetime;
}
sqlServerResource = sqlServerResource.WithLifetimeOf(surrogateBuilder);
}

sqlServerResource = sqlServerResource.WithLifetime(lifetime);

// RunAsEmulator() can be followed by custom model configuration so we need to delay the creation of the Config.json file
// until all resources are about to be prepared and annotations can't be updated anymore.
surrogateBuilder.WithContainerFiles(
Expand Down
8 changes: 5 additions & 3 deletions src/Aspire.Hosting.DevTunnels/DevTunnelHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,12 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
// Check that expected ports are active
foreach (var portResource in _tunnelResource.Ports)
{
var portStatus = tunnelStatus.Ports?.FirstOrDefault(p => p.PortNumber == portResource.TargetEndpoint.Port);
var tunnelPort = await portResource.GetTunnelPortAsync(cancellationToken).ConfigureAwait(false);
var portStatus = tunnelStatus.Ports?.FirstOrDefault(p => p.PortNumber == tunnelPort);
portResource.LastKnownStatus = portStatus;
if (portStatus?.PortUri is null)
{
return HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelUnhealthy_PortInactive, _tunnelResource.TunnelId, portResource.TargetEndpoint.Port));
return HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelUnhealthy_PortInactive, _tunnelResource.TunnelId, tunnelPort));
}
}

Expand All @@ -49,7 +50,8 @@ public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context
// Get access status for each port
foreach (var portResource in _tunnelResource.Ports)
{
var portAccessStatus = await _devTunnelClient.GetAccessAsync(_tunnelResource.TunnelId, portResource.TargetEndpoint.Port, logger, cancellationToken).ConfigureAwait(false);
var tunnelPort = await portResource.GetTunnelPortAsync(cancellationToken).ConfigureAwait(false);
var portAccessStatus = await _devTunnelClient.GetAccessAsync(_tunnelResource.TunnelId, tunnelPort, logger, cancellationToken).ConfigureAwait(false);
portResource.LastKnownAccessStatus = portAccessStatus;
}

Expand Down
26 changes: 14 additions & 12 deletions src/Aspire.Hosting.DevTunnels/DevTunnelPortHealthCheck.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,42 @@

namespace Aspire.Hosting.DevTunnels;

internal sealed class DevTunnelPortHealthCheck(DevTunnelResource tunnelResource, int port) : IHealthCheck
internal sealed class DevTunnelPortHealthCheck(DevTunnelPortResource portResource) : IHealthCheck
{
private readonly DevTunnelResource _tunnelResource = tunnelResource ?? throw new ArgumentNullException(nameof(tunnelResource));
private readonly DevTunnelPortResource _portResource = portResource ?? throw new ArgumentNullException(nameof(portResource));

private readonly int _port = port;

public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
try
{
var tunnelResource = _portResource.DevTunnel;
var port = await _portResource.GetTunnelPortAsync(cancellationToken).ConfigureAwait(false);
var tunnelStatus = tunnelResource.LastKnownStatus;
if (tunnelStatus is null)
{
return Task.FromResult(HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelPortUnhealthy_StatusUnknown, _port, _tunnelResource.TunnelId)));
return HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelPortUnhealthy_StatusUnknown, port, tunnelResource.TunnelId));
}

if (tunnelStatus.HostConnections == 0)
{
return Task.FromResult(HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelUnhealthy_NoActiveHostConnections, _tunnelResource.TunnelId)));
return HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelUnhealthy_NoActiveHostConnections, tunnelResource.TunnelId));
}

var portStatus = tunnelStatus.Ports?.FirstOrDefault(p => p.PortNumber == _port);
var portStatus = tunnelStatus.Ports?.FirstOrDefault(p => p.PortNumber == port);

// Check that port is active
if (portStatus?.PortUri is null)
{
return Task.FromResult(HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelUnhealthy_PortInactive, _tunnelResource.TunnelId, _port)));
return HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelUnhealthy_PortInactive, tunnelResource.TunnelId, port));
}

return Task.FromResult(HealthCheckResult.Healthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelPortHealthy, _port, _tunnelResource.TunnelId)));
return HealthCheckResult.Healthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelPortHealthy, port, tunnelResource.TunnelId));
}
catch (Exception ex)
{
return Task.FromResult(HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelPortUnhealthy_Error, _port, _tunnelResource.TunnelId, ex.Message), ex));
var tunnelResource = _portResource.DevTunnel;
var port = _portResource.TargetEndpoint.TargetPort?.ToString(CultureInfo.InvariantCulture) ?? "unknown";
return HealthCheckResult.Unhealthy(string.Format(CultureInfo.CurrentCulture, Resources.MessageStrings.DevTunnelPortUnhealthy_Error, port, tunnelResource.TunnelId, ex.Message), ex);
}
}
}
}
40 changes: 40 additions & 0 deletions src/Aspire.Hosting.DevTunnels/DevTunnelResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Globalization;
using Aspire.Hosting.ApplicationModel;

namespace Aspire.Hosting.DevTunnels;
Expand Down Expand Up @@ -85,4 +86,43 @@ public DevTunnelPortResource(
internal EndpointReference TargetEndpoint { get; init; }
internal DevTunnelPort? LastKnownStatus { get; set; }
internal DevTunnelAccessStatus? LastKnownAccessStatus { get; set; }

internal async ValueTask<int> GetTunnelPortAsync(CancellationToken cancellationToken = default)
{
if (TargetEndpoint.Resource.IsContainer())
{
// Dev tunnel hosting runs on the host, so container endpoints must forward the host-reachable
// allocated port rather than the container-internal target port.
return await GetResolvedEndpointPortAsync(EndpointProperty.Port, cancellationToken).ConfigureAwait(false)
?? TargetEndpoint.Port;
}

if (TargetEndpoint.TargetPort is int targetPort)
{
return targetPort;
}

return await GetResolvedEndpointPortAsync(EndpointProperty.TargetPort, cancellationToken).ConfigureAwait(false)
?? TargetEndpoint.Port;
}

private async ValueTask<int?> GetResolvedEndpointPortAsync(EndpointProperty property, CancellationToken cancellationToken)
{
string? resolvedTargetPort = null;
try
{
resolvedTargetPort = await TargetEndpoint.Property(property).GetValueAsync(cancellationToken).ConfigureAwait(false);
}
catch (InvalidOperationException) when (property == EndpointProperty.TargetPort && TargetEndpoint.IsAllocated)
{
// Endpoint references can only resolve targetPort dynamically when DCP reports a target-port expression.
}

if (int.TryParse(resolvedTargetPort, NumberStyles.None, CultureInfo.InvariantCulture, out var port))
{
return port;
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ public static IResourceBuilder<DevTunnelResource> AddDevTunnel(
async Task DeleteUnmodeledPortsAsync()
{
var existingPorts = await devTunnelClient.GetPortListAsync(tunnelResource.TunnelId, logger, ct).ConfigureAwait(false);
var modeledPortNumbers = tunnelResource.Ports.Select(p => p.TargetEndpoint.Port).ToHashSet();
var modeledPortNumbers = (await Task.WhenAll(tunnelResource.Ports.Select(p => p.GetTunnelPortAsync(ct).AsTask())).ConfigureAwait(false)).ToHashSet();
var unmodeledPorts = existingPorts.Ports.Where(p => !modeledPortNumbers.Contains(p.PortNumber)).ToList();
if (unmodeledPorts.Count > 0)
{
Expand All @@ -189,6 +189,7 @@ async Task DeleteUnmodeledPortsAsync()
async Task StartPortAsync(DevTunnelPortResource portResource)
{
var portLogger = e.Services.GetRequiredService<ResourceLoggerService>().GetLogger(portResource);
var tunnelPort = await portResource.GetTunnelPortAsync(ct).ConfigureAwait(false);

// Clear any prior port status
portLogger.LogInformation("Tunnel starting");
Expand All @@ -202,17 +203,17 @@ await notifications.PublishUpdateAsync(portResource, snapshot => snapshot with
{
_ = await devTunnelClient.CreatePortAsync(
portResource.DevTunnel.TunnelId,
portResource.TargetEndpoint.Port,
tunnelPort,
portResource.Options,
portLogger,
ct)
.ConfigureAwait(false);

portLogger.LogInformation("Created dev tunnel port '{Port}' on tunnel '{Tunnel}' targeting endpoint '{Endpoint}' on resource '{TargetResource}'", portResource.TargetEndpoint.Port, portResource.DevTunnel.TunnelId, portResource.TargetEndpoint.EndpointName, portResource.TargetEndpoint.Resource.Name);
portLogger.LogInformation("Created dev tunnel port '{Port}' on tunnel '{Tunnel}' targeting endpoint '{Endpoint}' on resource '{TargetResource}'", tunnelPort, portResource.DevTunnel.TunnelId, portResource.TargetEndpoint.EndpointName, portResource.TargetEndpoint.Resource.Name);
}
catch (Exception ex)
{
portLogger.LogError(ex, "Error trying to create dev tunnel port '{Port}' on tunnel '{Tunnel}': {Error}", portResource.TargetEndpoint.Port, portResource.DevTunnel.TunnelId, ex.Message);
portLogger.LogError(ex, "Error trying to create dev tunnel port '{Port}' on tunnel '{Tunnel}': {Error}", tunnelPort, portResource.DevTunnel.TunnelId, ex.Message);
#pragma warning disable CS0618 // Type or member is obsolete
portResource.TunnelEndpointAnnotation.AllocatedEndpointSnapshot.SetException(ex);
#pragma warning restore CS0618 // Type or member is obsolete
Expand Down Expand Up @@ -589,7 +590,7 @@ private static void AddDevTunnelPort(
var healtCheckKey = $"{portName}-check";
tunnelBuilder.ApplicationBuilder.Services.AddHealthChecks().Add(new HealthCheckRegistration(
healtCheckKey,
services => new DevTunnelPortHealthCheck(tunnel, targetEndpoint.Port),
services => new DevTunnelPortHealthCheck(portResource),
failureStatus: default,
tags: default,
timeout: default));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public static IResourceBuilder<T> PublishAsHostedAgent<T>(
var targetPort = existingHttpEndpoint?.TargetPort ?? 8088;

builder
.WithHttpEndpoint(name: "http", env: "DEFAULT_AD_PORT", targetPort: targetPort)
.WithHttpEndpoint(name: "http", env: "DEFAULT_AD_PORT", targetPort: targetPort, isProxied: true)
.WithUrls((ctx) =>
{
var http = ctx.Urls.FirstOrDefault(u => u.Endpoint?.EndpointName == "http" || u.Endpoint?.EndpointName == "https");
Expand Down
Loading
Loading