Is there an existing issue for this?
Is your feature request related to a problem? Please describe the problem.
WithDebugSupport takes an optional argsCallback that integrations use to strip the tool entrypoint (cargo run … --, go run <pkg>, python -m <mod>) when an IDE debugs the produced binary directly. It applies that callback by registering an ordinary WithArgs callback on the resource:
https://github.com/microsoft/aspire/blob/main/src/Aspire.Hosting/ResourceBuilderExtensions.cs#L4778
if (argsCallback is not null && builder is IResourceBuilder<IResourceWithArgs> resourceWithArgs)
{
resourceWithArgs.WithArgs(ctx =>
{
if (resourceWithArgs.Resource.SupportsDebugging(...) && ReferenceEquals(activeAnnotation, supportsDebuggingAnnotation))
{
argsCallback(ctx);
}
});
}
This creates three problems.
1. A silent registration-order requirement. CommandLineArgsCallbackAnnotation callbacks run in registration order, each mutating one shared list (ResourceExtensions.cs#L282). Because the debug callback removes items, it only works if registered after the callback that adds the entrypoint prefix. Nothing enforces or documents this. Registered earlier it sees an empty list, hits its own guard, and silently no-ops — leaking the entrypoint tokens to the debugged binary.
This was hit for real in Aspire.Hosting.Rust (#18906): the binary launched as myapp.exe run -- --login … and failed with error: unexpected argument 'run' found. Go (L91 / L185) and Python carry the same unwritten contract and happen to be ordered correctly. No tests cover it.
2. The API's shape invites the more dangerous position. A "rewrite arguments for debugging" hook reads as post-processing, so the intuitive place to register it is last. But last is strictly worse: the callback would then have to decide which -- is the integration's separator and which is a user argument. WithArgs("--", "--login", "user") is exactly that ambiguity. It currently works only because it runs before user arguments exist — the opposite of what the name suggests. Conversely, an integration that genuinely needs the final argument list has no correct position at all.
3. It mutates the app model, degrading non-debug paths. The rewrite applies to the resource's arguments generally, leaving them valid only for IDE launch. SupportsDebuggingAnnotation.RewritesArgumentsForDebugging exists purely to compensate, by suppressing Process fallback:
https://github.com/microsoft/aspire/blob/main/src/Aspire.Hosting/Dcp/ExecutableCreator.cs#L289
// A Process fallback runs the DCP Executable Spec's command and args "as is". When the debug
// support rewrites the resource's arguments for debugging ... those args are valid only for
// IDE launch, so a Process fallback would run a broken command. Skip the fallback in that case.
if (!supportsDebuggingAnnotation.RewritesArgumentsForDebugging)
{
exe.Spec.FallbackExecutionTypes = [ExecutionType.Process];
}
Also at L203 and L454. So any resource using an argsCallback permanently loses Process fallback — a capability sacrificed to work around the design.
Describe the solution you'd like
Compose the command line per execution mode instead of subtracting from it.
The information needed is already modelled separately. Aspire.Hosting.Rust keeps cargo arguments in their own annotation and already declares them structurally in the launch configuration:
return new RustLaunchConfiguration
{
Cargo = new RustCargoLaunchTarget { Args = ["build", .. cargoArgs] }
};
It then also concatenates run <cargo args> -- into the command line and textually re-derives that prefix to delete it. The structured form is discarded and reconstructed by string scanning. Go (build_flags, program) and Python (module, program_path) have the same split available in their launch configurations.
If a resource can express "tool invocation" and "program arguments" as distinct concepts, the command line is just assembled differently per mode:
- Process:
cargo run <cargoArgs> -- <programArgs>
- IDE:
<programArgs> only, with the tool arguments carried in the launch configuration where they already live
That removes the ordering requirement, the -- ambiguity, and the need for RewritesArgumentsForDebugging and its fallback suppression — and the app model stays truthful about what the resource runs.
A smaller intermediate step, if the full change is too invasive: keep argsCallback but store it on SupportsDebuggingAnnotation rather than registering it as a WithArgs callback, and apply it in ExecutableCreator when populating the IDE launch arguments. That alone fixes the ordering trap and gives the callback the fully-evaluated list.
One thing worth confirming first: the DCP Executable spec has a single args field (Executable.cs#L28), and launch configurations carry build/launch information rather than program arguments. Retaining both the rewritten and un-rewritten forms may need a DCP-side change.
Additional context
Karol Zadora-Przylecki (@karolz-ms) — flagging because this may interact with #18918. That PR makes the debugging APIs public so language integrations can live outside Aspire.Hosting and third-party language support becomes possible. If WithDebugSupport ships publicly in its current shape, the registration-order requirement becomes a public contract that external integration authors have to know about but can't discover — and the failure mode is silent, producing a broken command line at debug time rather than an error at build time. Worth deciding whether to reshape the API before it's public, or at minimum document the ordering requirement on the argsCallback parameter, whose XML doc currently says only:
Optional callback to add or modify command line arguments when running in an extension host. Useful if the entrypoint is usually provided as an argument to the resource executable.
Affected integrations: Aspire.Hosting.Rust, Aspire.Hosting.Go, Aspire.Hosting.Python. Aspire.Hosting.JavaScript passes no argsCallback and is unaffected.
Suggested follow-up regardless of approach: tests covering WithDebugSupport registered both before and after the entrypoint WithArgs callback — currently no test would catch the regression.
Is there an existing issue for this?
Is your feature request related to a problem? Please describe the problem.
WithDebugSupporttakes an optionalargsCallbackthat integrations use to strip the tool entrypoint (cargo run … --,go run <pkg>,python -m <mod>) when an IDE debugs the produced binary directly. It applies that callback by registering an ordinaryWithArgscallback on the resource:https://github.com/microsoft/aspire/blob/main/src/Aspire.Hosting/ResourceBuilderExtensions.cs#L4778
This creates three problems.
1. A silent registration-order requirement.
CommandLineArgsCallbackAnnotationcallbacks run in registration order, each mutating one shared list (ResourceExtensions.cs#L282). Because the debug callback removes items, it only works if registered after the callback that adds the entrypoint prefix. Nothing enforces or documents this. Registered earlier it sees an empty list, hits its own guard, and silently no-ops — leaking the entrypoint tokens to the debugged binary.This was hit for real in
Aspire.Hosting.Rust(#18906): the binary launched asmyapp.exe run -- --login …and failed witherror: unexpected argument 'run' found. Go (L91/L185) and Python carry the same unwritten contract and happen to be ordered correctly. No tests cover it.2. The API's shape invites the more dangerous position. A "rewrite arguments for debugging" hook reads as post-processing, so the intuitive place to register it is last. But last is strictly worse: the callback would then have to decide which
--is the integration's separator and which is a user argument.WithArgs("--", "--login", "user")is exactly that ambiguity. It currently works only because it runs before user arguments exist — the opposite of what the name suggests. Conversely, an integration that genuinely needs the final argument list has no correct position at all.3. It mutates the app model, degrading non-debug paths. The rewrite applies to the resource's arguments generally, leaving them valid only for IDE launch.
SupportsDebuggingAnnotation.RewritesArgumentsForDebuggingexists purely to compensate, by suppressing Process fallback:https://github.com/microsoft/aspire/blob/main/src/Aspire.Hosting/Dcp/ExecutableCreator.cs#L289
Also at
L203andL454. So any resource using anargsCallbackpermanently loses Process fallback — a capability sacrificed to work around the design.Describe the solution you'd like
Compose the command line per execution mode instead of subtracting from it.
The information needed is already modelled separately.
Aspire.Hosting.Rustkeeps cargo arguments in their own annotation and already declares them structurally in the launch configuration:It then also concatenates
run <cargo args> --into the command line and textually re-derives that prefix to delete it. The structured form is discarded and reconstructed by string scanning. Go (build_flags,program) and Python (module,program_path) have the same split available in their launch configurations.If a resource can express "tool invocation" and "program arguments" as distinct concepts, the command line is just assembled differently per mode:
cargo run <cargoArgs> -- <programArgs><programArgs>only, with the tool arguments carried in the launch configuration where they already liveThat removes the ordering requirement, the
--ambiguity, and the need forRewritesArgumentsForDebuggingand its fallback suppression — and the app model stays truthful about what the resource runs.A smaller intermediate step, if the full change is too invasive: keep
argsCallbackbut store it onSupportsDebuggingAnnotationrather than registering it as aWithArgscallback, and apply it inExecutableCreatorwhen populating the IDE launch arguments. That alone fixes the ordering trap and gives the callback the fully-evaluated list.One thing worth confirming first: the DCP
Executablespec has a singleargsfield (Executable.cs#L28), and launch configurations carry build/launch information rather than program arguments. Retaining both the rewritten and un-rewritten forms may need a DCP-side change.Additional context
Karol Zadora-Przylecki (@karolz-ms) — flagging because this may interact with #18918. That PR makes the debugging APIs public so language integrations can live outside
Aspire.Hostingand third-party language support becomes possible. IfWithDebugSupportships publicly in its current shape, the registration-order requirement becomes a public contract that external integration authors have to know about but can't discover — and the failure mode is silent, producing a broken command line at debug time rather than an error at build time. Worth deciding whether to reshape the API before it's public, or at minimum document the ordering requirement on theargsCallbackparameter, whose XML doc currently says only:Affected integrations:
Aspire.Hosting.Rust,Aspire.Hosting.Go,Aspire.Hosting.Python.Aspire.Hosting.JavaScriptpasses noargsCallbackand is unaffected.Suggested follow-up regardless of approach: tests covering
WithDebugSupportregistered both before and after the entrypointWithArgscallback — currently no test would catch the regression.