File: ApplicationModel\ArgumentsExecutionConfigurationGatherer.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Immutable;
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.ApplicationModel;
 
/// <summary>
/// Gathers command line arguments for resources.
/// </summary>
internal class ArgumentsExecutionConfigurationGatherer : IExecutionConfigurationGatherer
{
    /// <inheritdoc/>
    public async ValueTask GatherAsync(IExecutionConfigurationGathererContext context, IResource resource, ILogger resourceLogger, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken = default)
    {
        if (resource.TryGetAnnotationsOfType<CommandLineArgsCallbackAnnotation>(out var argumentAnnotations))
        {
            IList<object> args = [.. context.Arguments];
            var callbackContext = new CommandLineArgsCallbackContext(args, resource, cancellationToken)
            {
                Logger = resourceLogger,
                ExecutionContext = executionContext
            };
 
            foreach (var ann in argumentAnnotations)
            {
                // Each annotation operates on a shared context.
                args = await ann.AsCallbackAnnotation().EvaluateOnceAsync(callbackContext).ConfigureAwait(false);
            }
 
            // Take the final result and apply to the gatherer context.
            context.Arguments.Clear();
            context.Arguments.AddRange(args);
        }
 
        await GatherLaunchToolArgumentsAsync(context, resource, resourceLogger, executionContext, cancellationToken).ConfigureAwait(false);
    }
 
    /// <summary>
    /// Evaluates the resource's launch tool arguments (the tool-invocation prefix, e.g. <c>run ./cmd/api</c>) and
    /// records them separately from every other argument.
    /// </summary>
    /// <remarks>
    /// The callback is deliberately evaluated <em>after</em> the ordinary argument callbacks but its result is
    /// resolved <em>before</em> them. Keeping the two segments separate makes the prefix order-independent: no
    /// <c>WithArgs</c> or later configuration callback can observe it, mutate it, or clear it.
    /// </remarks>
    private static async ValueTask GatherLaunchToolArgumentsAsync(IExecutionConfigurationGathererContext context, IResource resource, ILogger resourceLogger, DistributedApplicationExecutionContext executionContext, CancellationToken cancellationToken)
    {
        // Launch tool arguments describe how a local tool (the resource's executable command) invokes the program.
        // A container invokes the program through the image's ENTRYPOINT instead, so the prefix must not be repeated
        // in its arguments. This matters for executables published as a Dockerfile (Go, Python, JavaScript), where
        // PublishAsDockerFile() reuses the executable's annotations for the generated container resource. Note that
        // the container's own `WithArgs(c => c.Args.Clear())` cannot undo a launch prefix because it is evaluated
        // separately, so return before recording that segment.
        if (resource.IsContainer())
        {
            return;
        }
 
        // Only the last annotation applies, mirroring how the active SupportsDebuggingAnnotation is resolved:
        // a resource can be handed launch tool arguments more than once and the most recent declaration wins.
        if (!resource.TryGetLastAnnotation<LaunchToolArgsCallbackAnnotation>(out var launchToolAnnotation))
        {
            return;
        }
 
        var launchToolContext = new CommandLineArgsCallbackContext([], resource, cancellationToken)
        {
            Logger = resourceLogger,
            ExecutionContext = executionContext
        };
 
        var launchToolArgs = await launchToolAnnotation.AsCallbackAnnotation().EvaluateOnceAsync(launchToolContext).ConfigureAwait(false);
        if (launchToolArgs.Count == 0)
        {
            return;
        }
 
        context.AddAdditionalData(new UnresolvedLaunchToolArgumentsData(launchToolArgs.ToImmutableArray(), launchToolAnnotation.ShowInCommandLine));
    }
}