File: DotnetToolResourceExtensions.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.CodeAnalysis;
using Aspire.Dashboard.Model;
using Aspire.Hosting.ApplicationModel;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Hosting;
 
/// <summary>
/// Provides extension methods for adding Dotnet Tool resources to the application model.
/// </summary>
[Experimental("ASPIREDOTNETTOOL", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public static class DotnetToolResourceExtensions
{
    internal const string ArgumentSeparator = "--";
 
    /// <summary>
    /// Adds a .NET tool resource to the application model.
    /// </summary>
    /// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
    /// <param name="name">The name of the resource.</param>
    /// <param name="packageId">The package id of the tool.</param>
    /// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
    public static IResourceBuilder<DotnetToolResource> AddDotnetTool(this IDistributedApplicationBuilder builder, [ResourceName] string name, string packageId)
        => builder.AddDotnetTool(new DotnetToolResource(name, packageId));
 
    /// <summary>
    /// Adds a .NET tool resource to the distributed application model and configures it for execution via the <c>dotnet
    /// tool exec</c> command.
    /// </summary>
    /// <typeparam name="T">The type of the .NET tool resource to add. Must inherit from <see cref="DotnetToolResource"/>.</typeparam>
    /// <param name="builder">The distributed application builder to which the .NET tool resource will be added.</param>
    /// <param name="resource">The .NET tool resource instance to add and configure.</param>
    /// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
    public static IResourceBuilder<T> AddDotnetTool<T>(this IDistributedApplicationBuilder builder, T resource)
        where T : DotnetToolResource
    {
        return builder.AddResource(resource)
            .WithInitialState(new CustomResourceSnapshot
            {
                ResourceType = KnownResourceTypes.Tool,
                Properties = []
            })
            .WithIconName("Toolbox")
            .WithCommand("dotnet")
            .WithArgs(BuildToolExecArguments)
            .OnBeforeResourceStarted(BuildToolProperties);
 
        void BuildToolExecArguments(CommandLineArgsCallbackContext x)
        {
            var toolConfig = resource.ToolConfiguration;
            if (toolConfig == null)
            {
                // If the annotation has been removed, don't add any dotnet tool arguments.
                return;
            }
 
            x.Args.Add("tool");
            x.Args.Add("exec");
            x.Args.Add(toolConfig.PackageId);
 
            var sourceArg = toolConfig.IgnoreExistingFeeds ? "--source" : "--add-source";
 
            foreach (var source in toolConfig.Sources)
            {
                x.Args.Add(sourceArg);
                x.Args.Add(source);
            }
 
            if (toolConfig.IgnoreFailedSources)
            {
                x.Args.Add("--ignore-failed-sources");
            }
 
            if (toolConfig.Version is not null)
            {
                x.Args.Add("--version");
                x.Args.Add(toolConfig.Version);
            }
            else if (toolConfig.Prerelease)
            {
                x.Args.Add("--prerelease");
            }
 
            x.Args.Add("--yes");
            x.Args.Add(ArgumentSeparator);
        }
 
        //TODO: Move to WithConfigurationFinalizer once merged - https://github.com/dotnet/aspire/pull/13200
        async Task BuildToolProperties(T resource, BeforeResourceStartedEvent evt, CancellationToken ct)
        {
            var rns = evt.Services.GetRequiredService<ResourceNotificationService>();
            var toolConfig = resource.ToolConfiguration;
            if (toolConfig == null)
            {
                return;
            }
 
            await rns.PublishUpdateAsync(resource, x => x with
            {
                Properties = [
                        ..x.Properties,
                        new (KnownProperties.Tool.Package, toolConfig.PackageId),
                        new (KnownProperties.Tool.Version, toolConfig.Version),
                        new (KnownProperties.Resource.Source, resource.ToolConfiguration?.PackageId)
                        ]
            }).ConfigureAwait(false);
        }
    }
 
    /// <summary>
    /// Sets the package identifier for the tool configuration associated with the resource builder.
    /// </summary>
    /// <typeparam name="T">The Dotnet Tool resource type</typeparam>
    /// <param name="builder">The <see cref="IResourceBuilder{T}"/>.</param>
    /// <param name="packageId">The package identifier to assign to the tool configuration. Cannot be null.</param>
    /// <returns>The <see cref="IResourceBuilder{T}"/> for chaining.</returns>
    public static IResourceBuilder<T> WithToolPackage<T>(this IResourceBuilder<T> builder, string packageId)
        where T : DotnetToolResource
    {
        builder.Resource.ToolConfiguration?.PackageId = packageId;
        return builder;
    }
 
    /// <summary>
    /// Sets the package version for a tool to use.
    /// </summary>
    /// <typeparam name="T">The Dotnet Tool resource type</typeparam>
    /// <param name="builder">The <see cref="IResourceBuilder{T}"/>.</param>
    /// <param name="version">The package version to use</param>
    /// <returns>The <see cref="IResourceBuilder{T}"/> for chaining.</returns>
    public static IResourceBuilder<T> WithToolVersion<T>(this IResourceBuilder<T> builder, string version)
        where T : DotnetToolResource
    {
        builder.Resource.ToolConfiguration?.Version = version;
        return builder;
    }
 
    /// <summary>
    /// Allows prerelease versions of the tool to be used
    /// </summary>
    /// <typeparam name="T">The type of resource being built. Must inherit from DotnetToolResource.</typeparam>
    /// <param name="builder">The <see cref="IResourceBuilder{T}"/>.</param>
    /// <returns>The <see cref="IResourceBuilder{T}"/> for chaining.</returns>
    public static IResourceBuilder<T> WithToolPrerelease<T>(this IResourceBuilder<T> builder)
        where T : DotnetToolResource
    {
        builder.Resource.ToolConfiguration?.Prerelease = true;
        return builder;
    }
 
    /// <summary>
    /// Adds a NuGet package source for tool acquisition.
    /// </summary>
    /// <typeparam name="T">The Dotnet Tool resource type</typeparam>
    /// <param name="builder">The <see cref="IResourceBuilder{T}"/>.</param>
    /// <param name="source">The source to add.</param>
    /// <returns>The <see cref="IResourceBuilder{T}"/> for chaining.</returns>
    public static IResourceBuilder<T> WithToolSource<T>(this IResourceBuilder<T> builder, string source)
        where T : DotnetToolResource
    {
        builder.Resource.ToolConfiguration?.Sources.Add(source);
        return builder;
    }
 
    /// <summary>
    /// Configures the tool to use only the specified package sources, ignoring existing NuGet configuration.
    /// </summary>
    /// <typeparam name="T">The Dotnet Tool resource type</typeparam>
    /// <param name="builder">The <see cref="IResourceBuilder{T}"/>.</param>
    /// <returns>The <see cref="IResourceBuilder{T}"/> for chaining.</returns>
    public static IResourceBuilder<T> WithToolIgnoreExistingFeeds<T>(this IResourceBuilder<T> builder)
        where T : DotnetToolResource
    {
        builder.Resource.ToolConfiguration?.IgnoreExistingFeeds = true;
        return builder;
    }
 
    /// <summary>
    /// Configures the resource to treat package source failures as warnings.
    /// </summary>
    /// <typeparam name="T">The Dotnet Tool resource type</typeparam>
    /// <param name="builder">The <see cref="IResourceBuilder{T}"/>.</param>
    /// <returns>The <see cref="IResourceBuilder{T}"/> for chaining.</returns>
    public static IResourceBuilder<T> WithToolIgnoreFailedSources<T>(this IResourceBuilder<T> builder)
        where T : DotnetToolResource
    {
        builder.Resource.ToolConfiguration?.IgnoreFailedSources = true;
        return builder;
    }
}