Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5c6e36e
Add resource command argument inputs
davidfowl May 2, 2026
4ce85d7
Improve resource command arguments
davidfowl May 2, 2026
393ce6b
Use resource command positional arguments
davidfowl May 2, 2026
276f25c
Add resource command visibility
davidfowl May 3, 2026
e9cb65d
Restore resource command annotation constructor compatibility
davidfowl May 3, 2026
5f6b136
Update polyglot command option snapshots
davidfowl May 3, 2026
9396b65
Add Stress playground command argument sample
davidfowl May 3, 2026
a8fd3dc
Expand Stress command argument samples
davidfowl May 3, 2026
6da63c6
Expose Stress command argument UI sample
davidfowl May 3, 2026
2a4585d
Use interaction inputs for command arguments
davidfowl May 3, 2026
dac4bc0
Rename command argument options
davidfowl May 3, 2026
956ad80
Synchronize API docs test fetcher
davidfowl May 3, 2026
9d35361
Add resource command argument validation
davidfowl May 4, 2026
6d64f05
Forward resource command arguments by order
davidfowl May 4, 2026
bff95ab
Update command argument code generation snapshots
davidfowl May 4, 2026
1b99a41
Support polyglot resource command arguments
davidfowl May 4, 2026
77eba8e
Fix ATS object unmarshalling and TS input typing
davidfowl May 4, 2026
c7f03d6
Treat init-only ATS properties as read-only
davidfowl May 4, 2026
440d5fb
Update polyglot validation samples for getter APIs
davidfowl May 4, 2026
f4f7c96
Update reusable TypeScript E2E sample for getter API
davidfowl May 5, 2026
2df960d
Address PR feedback on command validation tests
davidfowl May 7, 2026
fe26b35
Address command argument PR feedback
davidfowl May 7, 2026
c38acd9
Address resource command execution feedback
davidfowl May 7, 2026
3ae9803
Move resource command prompts to AppHost
davidfowl May 7, 2026
4efa5f4
Address resource command argument review feedback
davidfowl May 7, 2026
a79e590
Fix rebase conflict fallout
davidfowl May 8, 2026
c9f3c46
Refactor stress apphost to organize commands into nested resource groups
JamesNK May 8, 2026
417f8d5
Fix dashboard command validation test
davidfowl May 8, 2026
cf27985
Address resource command review feedback
davidfowl May 8, 2026
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
181 changes: 3 additions & 178 deletions playground/Stress/Stress.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;

Expand Down Expand Up @@ -72,29 +71,6 @@
.WithEnvironment("HOST", $"{serviceBuilder.GetEndpoint("http").Property(EndpointProperty.Host)}")
.WithEnvironment("PORT", $"{serviceBuilder.GetEndpoint("http").Property(EndpointProperty.Port)}")
.WithEnvironment("URL", $"{serviceBuilder.GetEndpoint("http").Property(EndpointProperty.Url)}");
serviceBuilder.WithCommand(
name: "icon-test",
displayName: "Icon test",
executeCommand: (c) =>
{
return Task.FromResult(CommandResults.Success());
},
commandOptions: new CommandOptions
{
IconName = "CloudDatabase"
});
serviceBuilder.WithCommand(
name: "icon-test-highlighted",
displayName: "Icon test highlighted",
executeCommand: (c) =>
{
return Task.FromResult(CommandResults.Success());
},
commandOptions: new CommandOptions
{
IconName = "CloudDatabase",
IsHighlighted = true
});

serviceBuilder.WithHttpEndpoint(5180, name: $"http");
for (var i = 1; i <= 30; i++)
Expand All @@ -103,145 +79,12 @@
serviceBuilder.WithHttpEndpoint(port, name: $"http-{port}");
}

serviceBuilder.WithHttpCommand("/write-console", "Write to console", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
serviceBuilder.WithHttpCommand("/write-console-large", "Write to console large", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
serviceBuilder.WithHttpCommand("/increment-counter", "Increment counter", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
serviceBuilder.WithHttpCommand("/big-trace", "Big trace", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
serviceBuilder.WithHttpCommand("/trace-limit", "Trace limit", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
serviceBuilder.WithHttpCommand("/log-message", "Log message", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
serviceBuilder.WithHttpCommand("/log-message-limit", "Log message limit", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
serviceBuilder.WithHttpCommand("/log-message-limit-large", "Log message limit large", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
serviceBuilder.WithHttpCommand("/http-command-auto-result", "HTTP command auto result", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning", ResultMode = HttpCommandResultMode.Auto, Description = "Run an HTTP command and infer the result format from the response content type" });
serviceBuilder.WithHttpCommand("/http-command-json-result", "HTTP command JSON result", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning", ResultMode = HttpCommandResultMode.Json, Description = "Run an HTTP command and flow the JSON response back to the caller" });
serviceBuilder.WithHttpCommand("/http-command-text-result", "HTTP command text result", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning", ResultMode = HttpCommandResultMode.Text, Description = "Run an HTTP command and flow the plain-text response back to the caller" });
serviceBuilder.WithHttpCommand("/multiple-traces-linked", "Multiple traces linked", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
serviceBuilder.WithHttpCommand("/overflow-counter", "Overflow counter", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
serviceBuilder.WithHttpCommand("/nested-trace-spans", "Out of order nested spans", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
serviceBuilder.WithHttpCommand("/exemplars-no-span", "Examplars with no span", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
serviceBuilder.WithHttpCommand("/genai-trace", "Gen AI trace", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
serviceBuilder.WithHttpCommand("/genai-langchain-trace", "Gen AI LangChain trace", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
serviceBuilder.WithHttpCommand("/genai-trace-display-error", "Gen AI trace display error", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
serviceBuilder.WithHttpCommand("/genai-evaluations", "Gen AI evaluations", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
serviceBuilder.WithHttpCommand("/log-formatting", "Log formatting", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });
serviceBuilder.WithHttpCommand("/big-nested-trace", "Big nested trace", commandOptions: new() { Method = HttpMethod.Get, IconName = "ContentViewGalleryLightning" });

builder.AddProject<Projects.Stress_TelemetryService>("stress-telemetryservice")
var telemetryBuilder = builder.AddProject<Projects.Stress_TelemetryService>("stress-telemetryservice")
.WithUrls(c => c.Urls.Add(new() { Url = "https://someplace.com", DisplayText = "Some place" }))
.WithUrl("https://someotherplace.com/some-path", "Some other place")
.WithUrl("https://extremely-long-url.com/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz//abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmno")
.AddInteractionCommands()
.WithCommand(
name: "long-command",
displayName: "This is a custom command with a very long command display name",
executeCommand: (c) =>
{
return Task.FromResult(CommandResults.Success());
},
commandOptions: new() { IconName = "CloudDatabase" })
.WithCommand(
name: "resource-stop-all",
displayName: "Stop all resources",
executeCommand: async (c) =>
{
await ExecuteCommandForAllResourcesAsync(c.ServiceProvider, KnownResourceCommands.StopCommand, c.CancellationToken);
return CommandResults.Success();
},
commandOptions: new() { IconName = "Stop", IconVariant = IconVariant.Filled })
.WithCommand(
name: "resource-start-all",
displayName: "Start all resources",
executeCommand: async (c) =>
{
await ExecuteCommandForAllResourcesAsync(c.ServiceProvider, KnownResourceCommands.StartCommand, c.CancellationToken);
return CommandResults.Success();
},
commandOptions: new() { IconName = "Play", IconVariant = IconVariant.Filled })
.WithCommand(
name: "generate-token",
displayName: "Generate Token",
executeCommand: (c) =>
{
var token = new
{
accessToken = Convert.ToBase64String(Guid.NewGuid().ToByteArray()) + Convert.ToBase64String(Guid.NewGuid().ToByteArray()),
tokenType = "Bearer",
expiresIn = 3600,
scope = "api.read api.write",
issuedAt = DateTime.UtcNow
};
var json = JsonSerializer.Serialize(token, new JsonSerializerOptions { WriteIndented = true });
var resultData = new CommandResultData
{
Value = json,
Format = CommandResultFormat.Json
};
return Task.FromResult(CommandResults.Success("Generated token.", resultData));
},
commandOptions: new() { IconName = "Key", Description = "Generate a temporary access token" })
.WithCommand(
name: "get-connection-string",
displayName: "Get Connection String",
executeCommand: (c) =>
{
var connectionString = $"Server=localhost,1433;Database=StressDb;User Id=sa;Password={Guid.NewGuid():N};TrustServerCertificate=true";
var message = """
Retrieved connection string. The database connection was established successfully
after verifying TLS certificates and negotiating encryption parameters.

The server responded with protocol version 7.4 and confirmed support for multiple
active result sets. Connection pooling is enabled with a maximum pool size of 100
connections and a minimum of 10 idle connections maintained.

The login handshake completed in 42ms with SSPI authentication. All pre-login
checks passed including network library validation and instance name resolution.
""";
return Task.FromResult(CommandResults.Success(message, new CommandResultData { Value = connectionString, DisplayImmediately = true }));
},
commandOptions: new() { IconName = "LinkMultiple", Description = "Get the connection string for this resource" })
.WithCommand(
name: "validate-config",
displayName: "Validate Config",
executeCommand: (c) =>
{
var errors = new { errors = new[] { new { field = "connectionString", message = "Invalid host" }, new { field = "timeout", message = "Must be positive" } } };
var json = JsonSerializer.Serialize(errors, new JsonSerializerOptions { WriteIndented = true });
return Task.FromResult(CommandResults.Failure("Validation failed", json, CommandResultFormat.Json));
},
commandOptions: new() { IconName = "Warning", Description = "Validate resource configuration (always fails with details)" })
.WithCommand(
name: "check-health",
displayName: "Check Health",
executeCommand: (c) =>
{
return Task.FromResult(CommandResults.Failure("Health check failed", "Connection refused: ECONNREFUSED 127.0.0.1:5432\nRetries exhausted after 3 attempts", CommandResultFormat.Text));
},
commandOptions: new() { IconName = "HeartBroken", Description = "Check resource health (always fails with details)" })
.WithCommand(
name: "migrate-database",
displayName: "Migrate Database",
executeCommand: (c) =>
{
var markdown = """
# ⚙️ Database Migration Summary
.WithUrl("https://extremely-long-url.com/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz//abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmnopqrstuvwxyz/abcdefghijklmno");

| Table | Result |
|------------|----------------------------|
| Customers | ✅ 1,200 rows |
| Products | ✅ 850 rows |
| Orders | ✅ 3,400 rows |
| OrderItems | ✅ 8,750 rows |
| Categories | ✅ 45 rows |
| Reviews | ❌ FK constraint violation |
| Inventory | ✅ 850 rows |
| Shipping | ✅ 3,400 rows |
| Payments | ❌ Timeout after 30s |
| Coupons | ✅ 120 rows |

**Summary:** 8 of 10 tables migrated successfully. 2 tables failed.
""";
return Task.FromResult(CommandResults.Success("Database migrated.", new CommandResultData { Value = markdown, Format = CommandResultFormat.Markdown }));
},
commandOptions: new() { IconName = "CloudDatabase", Description = "Migrate the database with sample store data" });
builder.AddCommandResources(serviceBuilder, telemetryBuilder);

#if !SKIP_DASHBOARD_REFERENCE
// This project is only added in playground projects to support development/debugging
Expand Down Expand Up @@ -279,21 +122,3 @@ checks passed including network library validation and instance name resolution.
.WithArgs("arg_from_apphost");

builder.Build().Run();

static async Task ExecuteCommandForAllResourcesAsync(IServiceProvider serviceProvider, string commandName, CancellationToken cancellationToken)
{
var commandService = serviceProvider.GetRequiredService<ResourceCommandService>();
var model = serviceProvider.GetRequiredService<DistributedApplicationModel>();

var resources = model.Resources
.Where(r => r.IsContainer() || r is ProjectResource || r is ExecutableResource)
.Where(r => r.Name != KnownResourceNames.AspireDashboard)
.ToList();

var commandTasks = new List<Task>();
foreach (var r in resources)
{
commandTasks.Add(commandService.ExecuteCommandAsync(r, commandName, cancellationToken));
}
await Task.WhenAll(commandTasks).ConfigureAwait(false);
}
Loading
Loading