Skip to content
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
36a5310
Document launch configuration callback context design
Aug 5, 2026
fc48eb7
docs: add launch configuration callback plan
Aug 6, 2026
ad326ea
Add launch configuration callback context
Aug 6, 2026
bfb9027
Fix launch producer restart handling
Aug 6, 2026
9a8c99a
Reset debug execution before rebuilding arguments
Aug 6, 2026
e9ea853
Migrate debug launch configuration producers
Aug 6, 2026
9efdff8
Finalize debug callback context API
Aug 6, 2026
65a56a7
Migrate debug support playground
Aug 6, 2026
ac32975
Fix debug support documentation whitespace
Aug 6, 2026
b0fcfd3
Address callback context review findings
Aug 6, 2026
8de33ce
Remove agent planning artifacts and restore load-bearing comments
Aug 7, 2026
900a331
Fix MAUI launch configuration regression and rename PrepareObjects
Aug 7, 2026
7d4d8fe
Address launch configuration context review
Aug 8, 2026
0ee07ef
Install Azure Functions Core Tools from release archive
Aug 8, 2026
2db433e
Recompute debug argument callbacks after original snapshot
Aug 8, 2026
6cb79d3
Validate legacy debug producer return type
Aug 8, 2026
1481f78
Revert unrelated Azure Functions tool install change
Aug 8, 2026
dcf4b09
Preserve launch override process args
Aug 8, 2026
024577a
Fix debug executable configuration snapshots
adamint Aug 9, 2026
24588ab
Reuse original argument resolutions across the debug rewrite
adamint Aug 9, 2026
5b8d2f3
Address review findings on debug launch configuration
adamint Aug 9, 2026
92901b8
Fix debug argument rewrite reuse and legacy overload validation
adamint Aug 10, 2026
64dd50d
Address debug launch context review feedback
Aug 10, 2026
3b95b28
Merge branch 'main' into adamint/issue-18956-launch-config-context
adamint Aug 10, 2026
a904ac7
Prevent later arg mutations from stripping debug-injected arguments
Aug 10, 2026
30d272c
Narrow debug launch context to resolved environment
Aug 10, 2026
35bea19
Clarify process-mode launch producer behavior
Aug 11, 2026
fa5c96c
Fix launch-override producer wiring
Aug 11, 2026
c5855b8
Clarify asynchronous debug producer guidance
Aug 11, 2026
7cdb91a
Cover the no-fallback rule when a launch configuration producer throws
Aug 11, 2026
b739005
Merge branch 'main' into adamint/issue-18956-launch-config-context
Aug 12, 2026
629c3ea
Merge remote-tracking branch 'upstream/main' into adamint/issue-18956…
Aug 13, 2026
a506a7a
Restore launch configuration inspection API
Aug 13, 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
44 changes: 28 additions & 16 deletions src/Aspire.Hosting/ApplicationModel/DebugSupportExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,33 +74,45 @@ public static bool SupportsDebugging(this IResource resource, IConfiguration con
}

/// <summary>
/// Creates the launch configuration that this resource sends to the IDE for the given launch mode.
/// Creates the launch configuration that this resource sends to the IDE using a callback context.
/// </summary>
/// <param name="resource">The resource to inspect. It must carry a <see cref="SupportsDebuggingAnnotation"/>.</param>
/// <param name="mode">The launch mode, one of the values on <see cref="ExecutableLaunchMode"/>.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <param name="context">The callback context containing the resolved environment and launch data.</param>
/// <returns>The launch configuration, typically an <see cref="ExecutableLaunchConfiguration"/>.</returns>
/// <exception cref="ArgumentException"><paramref name="context"/> belongs to a different resource.</exception>
/// <exception cref="InvalidOperationException">The resource does not declare debug launch support.</exception>
/// <remarks>
/// <para>
/// Launch configuration is created by invoking the producer callback passed to
/// <see cref="ResourceBuilderExtensions.WithDebugSupport{T, TLaunchConfiguration}(IResourceBuilder{T}, Func{string, TLaunchConfiguration}, string, Action{CommandLineArgsCallbackContext})"/>
/// (or its asynchronous overload),
/// which owns the complete configuration; Aspire serializes the result as-is.
/// The configuration is produced fresh on each call; it is not a singleton.
/// Aspire may call the producer several times for the same resource.
/// Launch configuration is created by invoking the producer callback passed to <see cref="ResourceBuilderExtensions.WithDebugSupport{T, TLaunchConfiguration}(IResourceBuilder{T}, Func{LaunchConfigurationCallbackContext, Task{TLaunchConfiguration}}, string, Action{CommandLineArgsCallbackContext})"/>,
/// which owns the complete configuration; Aspire serializes the result as-is.
/// </para>
/// <para>
/// This describes the launch configuration itself, not whether one is going to be used.
/// Depending on how the application is started, or how a resource is configured,
/// Aspire may or may not run the resource under a debugger. Use <see cref="SupportsDebugging"/> to test for that.
/// This method never resolves environment variables. Aspire creates <paramref name="context"/>
/// when the active debug-support annotation is producing a launch configuration for an executable creation.
/// </para>
/// <para>
/// Deliberately internal. <see cref="LaunchConfigurationCallbackContext"/> has no public constructor, and
/// AppHost code only ever receives one while this resource's own producer is running - where calling this
/// would re-enter that producer. Passing another resource's context is rejected below, and holding a context
/// past the callback describes a launch that already happened. A public overload would therefore promise an
/// inspection flow that no caller outside this assembly can reach; exposing it needs a supported way to build
/// a context first.
/// </para>
/// </remarks>
[AspireExportIgnore(Reason = "Debug support inspection is a local .NET helper and is not part of the ATS surface.")]
public static Task<object> CreateLaunchConfigurationAsync(this IResource resource, string mode, CancellationToken cancellationToken = default)
internal static Task<object> CreateLaunchConfigurationAsync(
Comment thread
adamint marked this conversation as resolved.
Resolved
this IResource resource,
LaunchConfigurationCallbackContext context)
{
ArgumentNullException.ThrowIfNull(resource);
ArgumentNullException.ThrowIfNull(mode);
ArgumentNullException.ThrowIfNull(context);

if (!ReferenceEquals(resource, context.Resource))
Comment thread
adamint marked this conversation as resolved.
Resolved
{
throw new ArgumentException(
$"The launch configuration callback context belongs to resource '{context.Resource.Name}', " +
$"but launch configuration was requested for resource '{resource.Name}'.",
nameof(context));
}

if (!resource.TryGetLastAnnotation<SupportsDebuggingAnnotation>(out var supportsDebuggingAnnotation))
{
Expand All @@ -110,7 +122,7 @@ public static Task<object> CreateLaunchConfigurationAsync(this IResource resourc
$"Note that it only adds the annotation in run mode.");
}

return supportsDebuggingAnnotation.LaunchConfigurationProducer(mode, cancellationToken);
return supportsDebuggingAnnotation.LaunchConfigurationProducer(context);
}

private static string[]? GetSupportedLaunchConfigurations(IConfiguration configuration)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,7 @@ public static class KnownLaunchConfigurationTypes
/// </para>
/// <para>
/// Integrations create a derived type and supply it through
/// <see cref="ResourceBuilderExtensions.WithDebugSupport{T, TLaunchConfiguration}(IResourceBuilder{T}, Func{string, TLaunchConfiguration}, string, Action{CommandLineArgsCallbackContext})"/>
/// or its asynchronous overload.
/// one of the <c>WithDebugSupport</c> overloads on <see cref="ResourceBuilderExtensions"/>.
/// </para>
/// </remarks>
/// <param name="type">The launch configuration type identifier, for example <see cref="KnownLaunchConfigurationTypes.Project"/>.</param>
Expand Down Expand Up @@ -90,8 +89,8 @@ public class ExecutableLaunchConfiguration(string type)
/// <remarks>
/// Defaults to <see cref="ExecutableLaunchMode.Debug"/> when a debugger is attached to the app host
/// and <see cref="ExecutableLaunchMode.NoDebug"/> otherwise. The mode requested by the IDE for the
/// current debug session is passed to the producer callback of
/// <see cref="ResourceBuilderExtensions.WithDebugSupport{T, TLaunchConfiguration}(IResourceBuilder{T}, Func{string, TLaunchConfiguration}, string, Action{CommandLineArgsCallbackContext})"/>.
/// current debug session is passed directly to mode-based producers and is available to context-based
/// producers through <see cref="LaunchConfigurationCallbackContext.Mode"/>.
/// </remarks>
[JsonPropertyName("mode")]
public string Mode { get; set; } = System.Diagnostics.Debugger.IsAttached ? ExecutableLaunchMode.Debug : ExecutableLaunchMode.NoDebug;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// 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.ApplicationModel;

/// <summary>
/// Provides the runtime data used to create a launch configuration for a resource.
/// </summary>
/// <remarks>
/// Aspire creates this context after resolving the execution configuration for a specific executable
/// creation. Environment variable values may contain secrets; only copy values into the launch
/// configuration when the IDE requires them.
/// </remarks>
[Experimental("ASPIREEXTENSION001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public sealed class LaunchConfigurationCallbackContext
{
internal LaunchConfigurationCallbackContext(
string mode,
IResource resource,
IReadOnlyDictionary<string, string> environmentVariables,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(mode);
ArgumentNullException.ThrowIfNull(resource);
ArgumentNullException.ThrowIfNull(environmentVariables);

Mode = mode;
Resource = resource;
EnvironmentVariables = environmentVariables;
CancellationToken = cancellationToken;
}

/// <summary>
/// Gets the requested launch mode, one of the values on <see cref="ExecutableLaunchMode"/>.
/// </summary>
public string Mode { get; }

/// <summary>
/// Gets the resource being launched.
/// </summary>
public IResource Resource { get; }

/// <summary>
/// Gets the resolved environment variables used for this executable creation.
/// </summary>
/// <remarks>
/// Values can contain secrets. Aspire serializes only the launch configuration returned by the
/// producer; integrations should copy only values required by the IDE.
/// </remarks>
public IReadOnlyDictionary<string, string> EnvironmentVariables { get; }

/// <summary>
/// Gets the cancellation token for this executable creation.
/// </summary>
public CancellationToken CancellationToken { get; }
}
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/Dcp/DcpExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ public async Task RunApplicationAsync(CancellationToken ct = default)
{
containers = _containerCreator.PrepareObjects().ToArray();
_containerCreator.PrepareContainerExecutables();
executables = (await _executableCreator.PrepareObjectsAsync(ct).ConfigureAwait(false)).ToArray();
executables = _executableCreator.PrepareObjects(ct).ToArray();

prepareResourcesActivity.SetDcpPreparedResourceCounts(containers.Length, executables.Length);
}
Expand Down
120 changes: 52 additions & 68 deletions src/Aspire.Hosting/Dcp/ExecutableCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,11 @@ public ExecutableCreator(
_appResources = appResources;
}

public async Task<IEnumerable<RenderedModelResource<Executable>>> PrepareObjectsAsync(CancellationToken cancellationToken)
public IEnumerable<RenderedModelResource<Executable>> PrepareObjects(CancellationToken cancellationToken)
{
await PrepareProjectExecutablesAsync(cancellationToken).ConfigureAwait(false);
PrepareProjectExecutables(cancellationToken);
PreparePlainExecutables();

return _appResources.Get().OfType<RenderedModelResource<Executable>>();
}

Expand Down Expand Up @@ -158,61 +159,64 @@ public async Task CreateObjectAsync(RenderedModelResource<Executable> er, EmptyC
throw new FailedToApplyEnvironmentException($"Failed to apply configuration to executable {er.ModelResource.Name}", configuration.Exception);
}

// Invoke the debug configuration callback now that endpoints are allocated.
// This allows launch configurations to access endpoint URLs that were not
// available during PrepareExecutables().
// "project" launch types on ProjectResources configure their launch configs in
// PrepareProjectExecutables() directly. Plain executables that carry IProjectMetadata and a
// "project" SupportsDebuggingAnnotation (e.g. DotnetProjectResource) are prepared as plain executables,
// so their "project" launch configuration is applied here for IDE/F5 parity with AddProject.
// All other types (plain executables and project subtypes like azure-functions) are also handled here.
// Invoke the active launch configuration producer only after the resource execution configuration
// has been resolved. This lets the producer reuse the exact arguments and environment values applied
// to this executable creation without evaluating resource callbacks a second time.
if (!er.ModelResource.HasAnnotationOfType<ForceProcessExecutionAnnotation>()
&& er.ModelResource.SupportsDebugging(_configuration, out var supportsDebuggingAnnotation))
&& er.ModelResource.SupportsDebugging(_configuration, out var supportsDebuggingAnnotation)
&& !(er.ModelResource is ProjectResource
&& HasProjectLaunchArgsOverride(er.ModelResource)
&& supportsDebuggingAnnotation.LaunchConfigurationType is KnownLaunchConfigurationTypes.Project))
{
if (supportsDebuggingAnnotation.LaunchConfigurationType is KnownLaunchConfigurationTypes.Project)
var isProjectLaunchConfiguration =
supportsDebuggingAnnotation.LaunchConfigurationType is KnownLaunchConfigurationTypes.Project;

if (isProjectLaunchConfiguration && !er.ModelResource.TryGetProjectMetadata(out _))
{
// ProjectResources already applied the "project" launch config in PrepareProjectExecutables().
// Only plain executables carrying project metadata need it applied here.
if (er.ModelResource is not ProjectResource)
{
if (er.ModelResource.TryGetProjectMetadata(out var plainProjectMetadata))
{
// Clear and re-apply the launch configuration to ensure proper restart behavior.
await ApplyProjectLaunchConfigurationAsync(exe, er.ModelResource, plainProjectMetadata, supportsDebuggingAnnotation, cancellationToken).ConfigureAwait(false);
}
else
{
throw new FailedToApplyEnvironmentException(
$"Resource '{er.ModelResource.Name}' declares \"project\" debug launch support (WithDebugSupport) but has no project metadata. " +
$"The \"project\" launch configuration type is reserved for .NET project resources; use a resource that carries {nameof(IProjectMetadata)} or a different launch configuration type.");
}
}
throw new FailedToApplyEnvironmentException(
$"Resource '{er.ModelResource.Name}' declares \"project\" debug launch support (WithDebugSupport) but has no project metadata. " +
$"The \"project\" launch configuration type is reserved for .NET project resources; use a resource that carries {nameof(IProjectMetadata)} or a different launch configuration type.");
}
else
{
// We have non-project Executable that supports debugging; need to annotate it properly.

var mode = _configuration[KnownConfigNames.DebugSessionRunMode] ?? ExecutableLaunchMode.NoDebug;
try
{
// Clear any existing launch configurations (needed for restart scenarios).
exe.Annotate(Executable.LaunchConfigurationsAnnotation, string.Empty);
await supportsDebuggingAnnotation.LaunchConfigurationAnnotator(exe, mode, cancellationToken).ConfigureAwait(false);
}
// Only fall back to Process when Spec.Args still forms a runnable command.
catch (Exception ex) when (!supportsDebuggingAnnotation.RewritesArgumentsForDebugging)
{
_logger.LogWarning(ex, "Failed to apply launch configuration for resource '{ResourceName}'. Falling back to process execution.", er.ModelResource.Name);
exe.Spec.ExecutionType = ExecutionType.Process;
}
var mode = isProjectLaunchConfiguration
? GetProjectLaunchConfigurationMode()
: _configuration[KnownConfigNames.DebugSessionRunMode] ?? ExecutableLaunchMode.NoDebug;
var callbackContext = new LaunchConfigurationCallbackContext(
mode,
er.ModelResource,
configuration.EnvironmentVariables.ToDictionary(
static variable => variable.Key,
static variable => variable.Value,
StringComparer.Ordinal),
cancellationToken);

try
{
// Executable objects are reused for restarts, so clear the prior launch configuration before
// applying the freshly resolved producer result.
exe.Annotate(Executable.LaunchConfigurationsAnnotation, string.Empty);
await supportsDebuggingAnnotation
.LaunchConfigurationAnnotator(exe, callbackContext)
.ConfigureAwait(false);
}
catch (Exception exception) when (
!isProjectLaunchConfiguration
&& !supportsDebuggingAnnotation.RewritesArgumentsForDebugging)
{
_logger.LogWarning(
exception,
"Failed to apply launch configuration for resource '{ResourceName}'. Falling back to process execution.",
er.ModelResource.Name);
exe.Spec.ExecutionType = ExecutionType.Process;
}
}

await factory.CreateDcpObjectsAsync([exe], cancellationToken).ConfigureAwait(false);
}

private async Task PrepareProjectExecutablesAsync(CancellationToken cancellationToken)
private void PrepareProjectExecutables(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var modelProjectResources = _model.GetProjectResources();

foreach (var project in modelProjectResources)
Expand Down Expand Up @@ -291,14 +295,8 @@ private async Task PrepareProjectExecutablesAsync(CancellationToken cancellation
exe.Spec.FallbackExecutionTypes = [ExecutionType.Process];
}

if (supportsDebuggingAnnotation.LaunchConfigurationType is KnownLaunchConfigurationTypes.Project)
{
// We want this annotation even if we are not using IDE execution; see ToSnapshot() for details.
await ApplyProjectLaunchConfigurationAsync(exe, project, projectMetadata, supportsDebuggingAnnotation, cancellationToken).ConfigureAwait(false);
}
// Non-project launch types (e.g. azure-functions) have their launch configuration
// applied later in CreateExecutableAsync() after endpoints are allocated,
// unless the IDE didn't send DEBUG_SESSION_INFO (handled by the fallback branch below).
// The active launch configuration producer runs later in CreateObjectAsync, after the
// resource's arguments and environment variables have been resolved.

// File-based apps (.cs files) are not supported by all IDEs (e.g. Visual Studio
// returns 500 for them). Populate fallback process args so that when the IDE
Expand Down Expand Up @@ -337,7 +335,7 @@ private async Task PrepareProjectExecutablesAsync(CancellationToken cancellation
exe.Spec.ExecutionType = ExecutionType.IDE;
exe.Spec.FallbackExecutionTypes = [ExecutionType.Process];

await ApplyProjectLaunchConfigurationAsync(exe, project, projectMetadata, supportsDebuggingAnnotation: null, cancellationToken).ConfigureAwait(false);
exe.SetProjectLaunchConfiguration(CreateProjectLaunchConfiguration(project, projectMetadata));
}
else
{
Expand Down Expand Up @@ -813,20 +811,6 @@ private bool ShouldFallBackToIdeExecution(bool isInDebugSession, SupportsDebuggi
return true;
}

private async Task ApplyProjectLaunchConfigurationAsync(Executable exe, IResource project, IProjectMetadata projectMetadata, SupportsDebuggingAnnotation? supportsDebuggingAnnotation, CancellationToken cancellationToken)
{
if (supportsDebuggingAnnotation?.LaunchConfigurationType is KnownLaunchConfigurationTypes.Project)
{
// The producer builds the complete configuration, so it is annotated as-is. Clearing first is
// what makes restarts (where the Executable object is reused) end up with a single entry.
exe.Annotate(Executable.LaunchConfigurationsAnnotation, string.Empty);
await supportsDebuggingAnnotation.LaunchConfigurationAnnotator(exe, GetProjectLaunchConfigurationMode(), cancellationToken).ConfigureAwait(false);
return;
}

exe.SetProjectLaunchConfiguration(CreateProjectLaunchConfiguration(project, projectMetadata));
}

private ProjectLaunchConfiguration CreateProjectLaunchConfiguration(IResource project, IProjectMetadata projectMetadata)
{
return ProjectLaunchConfigurationFactory.Create(project, projectMetadata, GetProjectLaunchConfigurationMode());
Expand Down
Loading
Loading