| 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; using Aspire.Hosting.Utils; 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> /// <ats-returns>The resource builder.</ats-returns> [AspireExport] 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> /// <remarks>This method is not available in polyglot app hosts. Use the overload with name and packageId instead.</remarks> [AspireExportIgnore(Reason = "Open generic IResource constraint — not ATS-compatible.")] public static IResourceBuilder<T> AddDotnetTool<T>(this IDistributedApplicationBuilder builder, T resource) where T : DotnetToolResource { #pragma warning disable ASPIREEXTENSION001 // WithLaunchToolArgs is experimental. return builder.AddResource(resource) .WithInitialState(new CustomResourceSnapshot { ResourceType = KnownResourceTypes.Tool, Properties = [] }) .WithIconName("Toolbox") .WithCommand("dotnet") .WithLaunchToolArgs(BuildToolExecArguments, showInCommandLine: false) .WithRequiredCommand("dotnet", context => ValidateDotnetSdkVersionAsync(context, resource.WorkingDirectory)) .OnBeforeResourceStarted(BeforeResourceStarted); #pragma warning restore ASPIREEXTENSION001 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/microsoft/aspire/pull/13200 async Task BeforeResourceStarted(T resource, BeforeResourceStartedEvent evt, CancellationToken ct) { var rns = evt.Services.GetRequiredService<ResourceNotificationService>(); var properties = resource.CreateSnapshotProperties().ToArray(); if (properties.Length == 0) { return; } await rns.PublishUpdateAsync(resource, x => x with { Properties = x.Properties.SetResourcePropertyRange(properties) }).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> /// <ats-returns>The resource builder.</ats-returns> [AspireExport] 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> /// <ats-returns>The resource builder.</ats-returns> [AspireExport] 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> /// <ats-returns>The resource builder.</ats-returns> [AspireExport] 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> /// <ats-returns>The resource builder.</ats-returns> [AspireExport] 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> /// <ats-returns>The resource builder.</ats-returns> [AspireExport] 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> /// <ats-returns>The resource builder.</ats-returns> [AspireExport] public static IResourceBuilder<T> WithToolIgnoreFailedSources<T>(this IResourceBuilder<T> builder) where T : DotnetToolResource { builder.Resource.ToolConfiguration?.IgnoreFailedSources = true; return builder; } internal static async Task<RequiredCommandValidationResult> ValidateDotnetSdkVersionAsync(RequiredCommandValidationContext _, string workingDirectory) { var version = await DotnetSdkUtils.TryGetVersionAsync(workingDirectory).ConfigureAwait(false); if (version?.Major < 10) { return RequiredCommandValidationResult.Failure($"DotnetToolResource requires dotnet SDK 10 or higher to run. Detected version: {version}"); } return RequiredCommandValidationResult.Success(); } }