Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Aspire.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,9 @@
<Folder Name="/playground/CustomResources/">
<Project Path="playground/CustomResources/CustomResources.AppHost/CustomResources.AppHost.csproj" />
</Folder>
<Folder Name="/playground/DotnetTool/">
<Project Path="playground/DotnetTool/DotnetTool.AppHost/DotnetTool.AppHost.csproj" />
</Folder>
<Folder Name="/playground/DatabaseMigration/">
<Project Path="playground/DatabaseMigration/DatabaseMigration.ApiModel/DatabaseMigration.ApiModel.csproj" />
<Project Path="playground/DatabaseMigration/DatabaseMigration.ApiService/DatabaseMigration.ApiService.csproj" />
Expand Down Expand Up @@ -414,6 +417,7 @@
<Project Path="tests/Aspire.Hosting.Azure.Tests/Aspire.Hosting.Azure.Tests.csproj" />
<Project Path="tests/Aspire.Hosting.Containers.Tests/Aspire.Hosting.Containers.Tests.csproj" />
<Project Path="tests/Aspire.Hosting.DevTunnels.Tests/Aspire.Hosting.DevTunnels.Tests.csproj" />
<Project Path="tests/Aspire.Hosting.DotnetTool.Tests/Aspire.Hosting.DotnetTool.Tests.csproj" />
<Project Path="tests/Aspire.Hosting.Docker.Tests/Aspire.Hosting.Docker.Tests.csproj" />
<Project Path="tests/Aspire.Hosting.Garnet.Tests/Aspire.Hosting.Garnet.Tests.csproj" />
<Project Path="tests/Aspire.Hosting.GitHub.Models.Tests/Aspire.Hosting.GitHub.Models.Tests.csproj" />
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
<PackageVersion Include="AspNetCore.HealthChecks.SqlServer" Version="9.0.0" />
<PackageVersion Include="AspNetCore.HealthChecks.Uris" Version="9.0.0" />
<!-- NuGet dependencies -->
<PackageVersion Include="NuGet.Configuration" Version="6.14.0" />
<PackageVersion Include="NuGet.ProjectModel" Version="6.14.0" />
<!-- external dependencies -->
<PackageVersion Include="Confluent.Kafka" Version="2.12.0" />
Expand Down
118 changes: 118 additions & 0 deletions playground/DotnetTool/DotnetTool.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using DotnetTool.AppHost;
using Microsoft.Extensions.DependencyInjection;

var builder = DistributedApplication.CreateBuilder(args);

var simpleUsage = builder.AddDotnetTool("simpleUsage", "dotnet-ef");

var wildcardVersion = builder.AddDotnetTool("wildcard", "dotnet-ef")
.WithPackageVersion("10.0.*")
.WithParentRelationship(simpleUsage);

var preRelease = builder.AddDotnetTool("prerelease", "dotnet-ef")
.WithPackagePrerelease()
.WithParentRelationship(simpleUsage);

// Multiple versions
var differentVersion = builder.AddDotnetTool("sameToolDifferentVersion1", "dotnet-dump")
.WithArgs("--version")
.WithPackageVersion("9.0.652701");
builder.AddDotnetTool("sameToolDifferentVersion2", "dotnet-dump")
.WithPackageVersion("9.0.621003")
.WithArgs("--version")
.WithParentRelationship(differentVersion);

// Concurrency
IResourceBuilder<DotnetToolResource>? concurrencyParent = null;
for (int i = 0; i < 5; i++)
{
var concurrency = builder.AddDotnetTool($"sametoolconcurrency-{i}", "dotnet-trace")
.WithArgs("--version");

if (concurrencyParent == null)
{
concurrencyParent = concurrency;
}
else
{
concurrency.WithParentRelationship(concurrencyParent);
}
}

// Substitution
var substituted = builder.AddDotnetTool("substituted", "dotnet-ef")
.WithCommand("calc")
.WithIconName("Calculator")
.WithExplicitStart();
foreach(var toolAnnotation in substituted.Resource.Annotations.OfType<DotNetToolAnnotation>().ToList())
{
substituted.Resource.Annotations.Remove(toolAnnotation);
}

// Fake Offline by using "empty" package feeds
var fakeSourcesPath = Path.Combine(Path.GetTempPath(), "does-not-exist", Guid.NewGuid().ToString());
var offline = builder.AddDotnetTool("offlineSimpleUsage", "dotnet-ef")
.WaitForCompletion(simpleUsage)
.WithPackageSource(fakeSourcesPath)
.WithPackageIgnoreExistingFeeds()
.WithPackageIgnoreFailedSources()
;
Comment on lines +60 to +61
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.WithPackageIgnoreFailedSources()
;
.WithPackageIgnoreFailedSources();


builder.AddDotnetTool("offlineWildcard", "dotnet-ef")
.WithPackageVersion("10.0.*")
.WaitForCompletion(wildcardVersion)
.WithParentRelationship(offline)
.WithPackageSource(fakeSourcesPath)
.WithPackageIgnoreExistingFeeds()
.WithPackageIgnoreFailedSources();

builder.AddDotnetTool("offlinePrerelease", "dotnet-ef")
.WithPackagePrerelease()
.WaitForCompletion(preRelease)
.WithParentRelationship(offline)
.WithPackageSource(fakeSourcesPath)
.WithPackageIgnoreExistingFeeds()
.WithPackageIgnoreFailedSources();

// Some issues only show up when installing for first time, rather than using existing downloaded versions
// Use a specific NUGET_PACKAGES path for these playground tools, so we can easily reset them
builder.Eventing.Subscribe<BeforeStartEvent>(async (evt, _) =>
{
var nugetPackagesPath = Path.Join(evt.Services.GetRequiredService<IAspireStore>().BasePath, "nuget");

foreach (var resource in builder.Resources.OfType<DotnetToolResource>())
{
builder.CreateResourceBuilder(resource)
.WithEnvironment("NUGET_PACKAGES", nugetPackagesPath)
.WithCommand("reset", "Reset Packages", ctx =>
{
try
{
Directory.Delete(nugetPackagesPath, true);
return Task.FromResult(CommandResults.Success());
}
catch (Exception ex)
{
return Task.FromResult(CommandResults.Failure(ex));
}
}, new CommandOptions
{
IconName = "Delete"
});
}
});

#if !SKIP_DASHBOARD_REFERENCE
// This project is only added in playground projects to support development/debugging
// of the dashboard. It is not required in end developer code. Comment out this code
// or build with `/p:SkipDashboardReference=true`, to test end developer
// dashboard launch experience, Refer to Directory.Build.props for the path to
// the dashboard binary (defaults to the Aspire.Dashboard bin output in the
// artifacts dir).
builder.AddProject<Projects.Aspire_Dashboard>(KnownResourceNames.AspireDashboard);
#endif

builder.Build().Run();

16 changes: 16 additions & 0 deletions playground/DotnetTool/DotnetTool.AppHost/DotNetToolAnnotation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#pragma warning disable IDE0005 // Using directive is unnecessary (needed when file is linked to test project)
using Aspire.Hosting.ApplicationModel;
#pragma warning restore IDE0005

namespace DotnetTool.AppHost;

public class DotNetToolAnnotation : IResourceAnnotation
{
public required string PackageId { get; set; }
public string? Version { get; set; }
public bool Prerelease { get; set; }
public List<string> Sources { get; } = [];
public bool IgnoreExistingFeeds { get; set; }
public bool IgnoreFailedSources { get; set; }
public bool AllowDowngrade { get; set; }
}
112 changes: 112 additions & 0 deletions playground/DotnetTool/DotnetTool.AppHost/DotNetToolExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#pragma warning disable IDE0005 // Using directive is unnecessary (needed when file is linked to test project)
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
#pragma warning restore IDE0005

namespace DotnetTool.AppHost;

/// <summary>
/// Provides extension methods for adding Dotnet Tool resources to the application model.
/// </summary>
public static class DotNetToolExtensions
{
public static IResourceBuilder<DotnetToolResource> AddDotnetTool(this IDistributedApplicationBuilder builder, string name, string packageId)
=> builder.AddDotnetTool(new DotnetToolResource(name, packageId));

public static IResourceBuilder<T> AddDotnetTool<T>(this IDistributedApplicationBuilder builder, T resource)
where T : DotnetToolResource
{
return builder.AddResource(resource)
.WithIconName("Toolbox")
.WithCommand("dotnet")
.WithArgs(x =>
{
if (!x.Resource.TryGetLastAnnotation<DotNetToolAnnotation>(out var toolConfig))
{
// 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("--verbosity");
x.Args.Add("detailed");
Comment on lines +57 to +58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verbose output made sense when dotnet tool install was it's own resource, but for dotnet tool exec, it just adds noise a user may not be expecting.

Suggested change
x.Args.Add("--verbosity");
x.Args.Add("detailed");

x.Args.Add("--yes");
x.Args.Add("--");
});
}

public static IResourceBuilder<T> WithPackageId<T>(this IResourceBuilder<T> builder, string packageId)
where T : DotnetToolResource
{
builder.Resource.ToolConfiguration.PackageId = packageId;
return builder;
}

/// <summary>
/// Set the package version for a tool to use
/// </summary>
/// <typeparam name="T">The Dotnet Tool resource type</typeparam>
/// <param name="builder">The <see cref="IDistributedApplicationBuilder"/>.</param>
/// <param name="version">The package version to use</param>
/// <returns>The <see cref="IResourceBuilder{T}"/> for chaining.</returns>
public static IResourceBuilder<T> WithPackageVersion<T>(this IResourceBuilder<T> builder, string version)
where T : DotnetToolResource
{
builder.Resource.ToolConfiguration.Version = version;
return builder;
}

public static IResourceBuilder<T> WithPackagePrerelease<T>(this IResourceBuilder<T> builder)
where T : DotnetToolResource
{
builder.Resource.ToolConfiguration.Prerelease = true;
return builder;
}

public static IResourceBuilder<T> WithPackageSource<T>(this IResourceBuilder<T> builder, string source)
where T : DotnetToolResource
{
builder.Resource.ToolConfiguration.Sources.Add(source);
return builder;
}

public static IResourceBuilder<T> WithPackageIgnoreExistingFeeds<T>(this IResourceBuilder<T> builder)
where T : DotnetToolResource
{
builder.Resource.ToolConfiguration.IgnoreExistingFeeds = true;
return builder;
}

public static IResourceBuilder<T> WithPackageIgnoreFailedSources<T>(this IResourceBuilder<T> builder)
where T : DotnetToolResource
{
builder.Resource.ToolConfiguration.IgnoreFailedSources = true;
return builder;
}
}
23 changes: 23 additions & 0 deletions playground/DotnetTool/DotnetTool.AppHost/DotnetTool.AppHost.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>$(DefaultTargetFramework)</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
</PropertyGroup>

<ItemGroup>
<Compile Include="..\..\KnownResourceNames.cs" Link="KnownResourceNames.cs" />
</ItemGroup>

<ItemGroup>
<AspireProjectOrPackageReference Include="Aspire.Hosting.AppHost" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="NuGet.Configuration" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reference isn't needed after moving from dotnet tool install to dotnet tool exec. Remove it, and the reference in Directory.Packages.props

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot do that

</ItemGroup>

</Project>
35 changes: 35 additions & 0 deletions playground/DotnetTool/DotnetTool.AppHost/DotnetToolResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma warning disable IDE0005 // Using directive is unnecessary (needed when file is linked to test project)
using Aspire.Hosting.ApplicationModel;
#pragma warning restore IDE0005

namespace DotnetTool.AppHost;

/// <summary>
/// Represents a .NET tool resource that encapsulates metadata about a .NET CLI tool, including its name, package ID,
/// and command.
/// </summary>
/// <remarks>This class is used to define and manage resources for .NET CLI tools. It associates a tool's name and
/// command with its package ID, and ensures that the required metadata is properly annotated.</remarks>
public class DotnetToolResource : ExecutableResource
{
/// <param name="name">The name of the resource.</param>
/// <param name="packageId">The package id of the tool</param>
public DotnetToolResource(string name, string packageId)
: base(name, "dotnet", ".")
{
ArgumentException.ThrowIfNullOrWhiteSpace(packageId, nameof(packageId));
Annotations.Add(new DotNetToolAnnotation { PackageId = packageId });
}

internal DotNetToolAnnotation ToolConfiguration
{
get
{
if (!this.TryGetLastAnnotation<DotNetToolAnnotation>(out var toolConfig))
{
throw new InvalidOperationException("DotNetToolAnnotation is missing");
}
return toolConfig;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:16179;http://localhost:16180",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:17119",
"ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:18036",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:17119",
"ASPIRE_SHOW_DASHBOARD_RESOURCES": "true"
}
},
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:16180",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:17120",
"ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18037",
"ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:17120",
"ASPIRE_SHOW_DASHBOARD_RESOURCES": "true",
"ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true"
}
},
"generate-manifest": {
"commandName": "Project",
"dotnetRunMessages": true,
"commandLineArgs": "--publisher manifest --output-path aspire-manifest.json",
"applicationUrl": "http://localhost:16180",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"DOTNET_ENVIRONMENT": "Development",
"ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:17120"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
9 changes: 9 additions & 0 deletions playground/DotnetTool/DotnetTool.AppHost/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
}
}
Loading