File: ApplicationModel\ProjectLaunchArgsOverrideAnnotation.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.Diagnostics;
using System.Diagnostics.CodeAnalysis;
 
namespace Aspire.Hosting.ApplicationModel;
 
/// <summary>
/// Represents an annotation that overrides the default project launch arguments
/// generated by DCP for a <see cref="ProjectResource"/>.
/// </summary>
/// <remarks>
/// <para>
/// When this annotation is present on a <see cref="ProjectResource"/>, DCP will use the provided
/// arguments as the base command (e.g., <c>build /t:Run</c>) instead of the default <c>run</c> verb.
/// DCP still appends the project path positionally and appends <c>--configuration</c> automatically;
/// only the verb and its options are overridden. This annotation is intended for verbs like
/// <c>build</c> that accept the project path as a positional argument. The <c>--no-launch-profile</c>
/// flag is omitted when this annotation is present.
/// </para>
/// <para>
/// A non-empty launch-tool argument callback replaces this base command. If the callback resolves no
/// arguments, this override remains the Process command.
/// </para>
/// </remarks>
/// <example>
/// Override DCP's default launch to use <c>dotnet build /t:Run</c>:
/// <code>
/// builder.WithAnnotation(new ProjectLaunchArgsOverrideAnnotation(["build", "/t:Run"]));
/// </code>
/// </example>
/// <param name="arguments">The custom launch arguments to use instead of the default project launch arguments.</param>
/// <param name="leadingResourceArgumentToRemove">The optional first resource argument to omit from the final launch arguments when it is already represented by <paramref name="arguments"/>.</param>
[Experimental("ASPIREPROJECTS001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
[DebuggerDisplay("Type = {GetType().Name,nq}, Arguments = {string.Join(\" \", Arguments)}")]
public sealed class ProjectLaunchArgsOverrideAnnotation(IReadOnlyList<string> arguments, string? leadingResourceArgumentToRemove = null) : IResourceAnnotation
{
    /// <summary>
    /// Gets the custom launch arguments to use instead of the default project args.
    /// </summary>
    public IReadOnlyList<string> Arguments { get; } = ValidateArguments(arguments).ToArray();
 
    /// <summary>
    /// Gets the optional first resource argument to omit from the final launch arguments when it is already represented by <see cref="Arguments"/>.
    /// </summary>
    public string? LeadingResourceArgumentToRemove { get; } = ValidateLeadingResourceArgumentToRemove(leadingResourceArgumentToRemove);
 
    internal void Apply(IList<string> projectArgs, string projectPath, string? configuration)
    {
        projectArgs.AddRange(Arguments);
        projectArgs.Add(projectPath);
 
        if (!string.IsNullOrEmpty(configuration))
        {
            projectArgs.AddRange(["--configuration", configuration]);
        }
    }
 
    private static IReadOnlyList<string> ValidateArguments(IReadOnlyList<string> arguments)
    {
        ArgumentNullException.ThrowIfNull(arguments);
 
        if (arguments.Count == 0)
        {
            throw new ArgumentException("Launch arguments must contain at least one entry.", nameof(arguments));
        }
 
        return arguments;
    }
 
    private static string? ValidateLeadingResourceArgumentToRemove(string? leadingResourceArgumentToRemove)
    {
        if (leadingResourceArgumentToRemove is not null && leadingResourceArgumentToRemove.Length == 0)
        {
            throw new ArgumentException("The leading resource argument to remove cannot be empty.", nameof(leadingResourceArgumentToRemove));
        }
 
        return leadingResourceArgumentToRemove;
    }
}