Skip to content
Open
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
30 changes: 30 additions & 0 deletions playground/Stress/Stress.ApiService/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,36 @@
return $"Sent requests to {string.Join(';', urls)}";
});

app.MapGet("/http-command-json-result", () =>
{
return Results.Json(new
{
token = Convert.ToBase64String(Guid.NewGuid().ToByteArray()),
issuedAt = DateTimeOffset.UtcNow,
expiresIn = 3600
});
});

app.MapGet("/http-command-auto-result", () =>
{
if (Random.Shared.Next(2) == 0)
{
return Results.Json(new
{
token = Convert.ToBase64String(Guid.NewGuid().ToByteArray()),
issuedAt = DateTimeOffset.UtcNow,
expiresIn = 3600
});
}

return Results.Text($"Generated text token: {Convert.ToBase64String(Guid.NewGuid().ToByteArray())}");
});

app.MapGet("/http-command-text-result", () =>
{
return Results.Text($"Generated text token: {Convert.ToBase64String(Guid.NewGuid().ToByteArray())}");
});

app.MapGet("/log-message-limit", async ([FromServices] ILogger<Program> logger) =>
{
const int LogCount = 10_000;
Expand Down
3 changes: 3 additions & 0 deletions playground/Stress/Stress.AppHost/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@
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" });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,7 @@ private string GetWrapperOrHandleName(string typeId)
/// </summary>
private static string GetDtoInterfaceName(string typeId)
{
// Extract simple type name and use as interface name
var simpleTypeName = ExtractSimpleTypeName(typeId);
return simpleTypeName;
return ExtractSimpleTypeName(typeId);
}

/// <summary>
Expand Down Expand Up @@ -517,7 +515,7 @@ private string GenerateAspireSdk(AtsContext context)
foreach (var cap in builder.Capabilities)
{
var (_, optionalParams) = SeparateParameters(cap.Parameters);
if (optionalParams.Count > 0)
if (optionalParams.Count > 0 && !TryGetDirectOptionsParameter(optionalParams, out _))
{
RegisterOptionsInterface(cap.CapabilityId, cap.MethodName, optionalParams);
}
Expand Down Expand Up @@ -742,6 +740,28 @@ private static (List<AtsParameterInfo> Required, List<AtsParameterInfo> Optional
return (required, optional);
}

private static bool TryGetDirectOptionsParameter(List<AtsParameterInfo> optionalParams, out AtsParameterInfo? directOptionsParam)
{
directOptionsParam = null;

// When ATS already exposes a single DTO parameter named "options", reuse that DTO type
// directly so the generated TypeScript API stays flat instead of wrapping it in another
// generated options object.
if (optionalParams.Count != 1)
{
return false;
}

var candidate = optionalParams[0];
if (!string.Equals(candidate.Name, "options", StringComparison.Ordinal) || candidate.Type?.Category != AtsTypeCategory.Dto)
{
return false;
}

directOptionsParam = candidate;
return true;
}

/// <summary>
/// Registers an options interface to be generated later.
/// Uses method name to create the interface name. When methods share a name but have
Expand Down Expand Up @@ -974,7 +994,8 @@ private void GenerateBuilderMethod(BuilderModel builder, AtsCapabilityInfo capab
// Separate required and optional parameters
var (requiredParams, optionalParams) = SeparateParameters(capability.Parameters);
var hasOptionals = optionalParams.Count > 0;
var optionsInterfaceName = ResolveOptionsInterfaceName(capability);
var hasDirectOptionsParameter = TryGetDirectOptionsParameter(optionalParams, out var directOptionsParam);
var optionsTypeName = hasDirectOptionsParameter ? MapParameterToTypeScript(directOptionsParam!) : ResolveOptionsInterfaceName(capability);

// Build parameter list for public method
var publicParamDefs = new List<string>();
Expand All @@ -985,7 +1006,7 @@ private void GenerateBuilderMethod(BuilderModel builder, AtsCapabilityInfo capab
}
if (hasOptionals)
{
publicParamDefs.Add($"options?: {optionsInterfaceName}");
publicParamDefs.Add($"options?: {optionsTypeName}");
}
var publicParamsString = string.Join(", ", publicParamDefs);

Expand Down Expand Up @@ -1039,7 +1060,7 @@ private void GenerateBuilderMethod(BuilderModel builder, AtsCapabilityInfo capab
WriteLine($"): Promise<{returnType}> {{");

// Extract optional params from options object
foreach (var param in optionalParams)
foreach (var param in hasDirectOptionsParameter ? [] : optionalParams)
{
WriteLine($" const {param.Name} = options?.{param.Name};");
}
Expand Down Expand Up @@ -1122,7 +1143,7 @@ private void GenerateBuilderMethod(BuilderModel builder, AtsCapabilityInfo capab
WriteLine();

// Extract optional params from options object and forward to internal method
foreach (var param in optionalParams)
foreach (var param in hasDirectOptionsParameter ? [] : optionalParams)
{
WriteLine($" const {param.Name} = options?.{param.Name};");
}
Expand Down Expand Up @@ -1195,7 +1216,8 @@ private void GenerateThenableClass(BuilderModel builder)
// Separate required and optional parameters
var (requiredParams, optionalParams) = SeparateParameters(capability.Parameters);
var hasOptionals = optionalParams.Count > 0;
var optionsInterfaceName = ResolveOptionsInterfaceName(capability);
var hasDirectOptionsParameter = TryGetDirectOptionsParameter(optionalParams, out var directOptionsParam);
var optionsTypeName = hasDirectOptionsParameter ? MapParameterToTypeScript(directOptionsParam!) : ResolveOptionsInterfaceName(capability);

// Build parameter list using options pattern
var publicParamDefs = new List<string>();
Expand All @@ -1206,7 +1228,7 @@ private void GenerateThenableClass(BuilderModel builder)
}
if (hasOptionals)
{
publicParamDefs.Add($"options?: {optionsInterfaceName}");
publicParamDefs.Add($"options?: {optionsTypeName}");
}
var paramsString = string.Join(", ", publicParamDefs);

Expand Down
86 changes: 85 additions & 1 deletion src/Aspire.Hosting/ApplicationModel/HttpCommandOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,33 @@
namespace Aspire.Hosting.ApplicationModel;

/// <summary>
/// Optional configuration for resource HTTP commands added with <see cref="ResourceBuilderExtensions.WithHttpCommand{TResource}(Aspire.Hosting.ApplicationModel.IResourceBuilder{TResource}, string, string, string?, string?, Aspire.Hosting.ApplicationModel.HttpCommandOptions?)"/>."/>
/// Specifies how an HTTP command should surface the HTTP response body as command result data.
/// </summary>
public enum HttpCommandResultMode
{
/// <summary>
/// Do not capture the HTTP response body as command result data.
/// </summary>
None,

/// <summary>
/// Infer the command result format from the HTTP response content type.
/// </summary>
Auto,

/// <summary>
/// Return the HTTP response body as JSON command result data.
/// </summary>
Json,

/// <summary>
/// Return the HTTP response body as plain text command result data.
/// </summary>
Text
}

/// <summary>
/// Optional configuration for resource HTTP commands added with <see cref="ResourceBuilderExtensions.WithHttpCommand{TResource}(Aspire.Hosting.ApplicationModel.IResourceBuilder{TResource}, string, string, string?, string?, Aspire.Hosting.ApplicationModel.HttpCommandOptions?)"/>.
/// </summary>
public class HttpCommandOptions : CommandOptions
{
Expand Down Expand Up @@ -34,4 +60,62 @@ public class HttpCommandOptions : CommandOptions
/// Gets or sets a callback to be invoked after the response is received to determine the result of the command invocation.
/// </summary>
public Func<HttpCommandResultContext, Task<ExecuteCommandResult>>? GetCommandResult { get; set; }

/// <summary>
/// Gets or sets how the HTTP response content should be returned as command result data
/// when <see cref="GetCommandResult"/> is not specified. The default is <see cref="HttpCommandResultMode.None"/>.
/// </summary>
public HttpCommandResultMode ResultMode { get; set; }
}

/// <summary>
/// ATS-friendly configuration for resource HTTP commands.
/// </summary>
[AspireDto]
internal sealed class HttpCommandExportOptions
{
/// <summary>
/// Optional description of the command, to be shown in the UI.
/// </summary>
public string? Description { get; set; }

/// <summary>
/// When a confirmation message is specified, the UI will prompt with an OK/Cancel dialog before starting the command.
/// </summary>
public string? ConfirmationMessage { get; set; }

/// <summary>
/// The icon name for the command.
/// </summary>
public string? IconName { get; set; }

/// <summary>
/// The icon variant.
/// </summary>
public IconVariant? IconVariant { get; set; }

/// <summary>
/// A flag indicating whether the command is highlighted in the UI.
/// </summary>
public bool IsHighlighted { get; set; }

/// <summary>
/// Gets or sets the command name.
/// </summary>
public string? CommandName { get; set; }

/// <summary>
/// Gets or sets the HTTP endpoint name to send the request to when the command is invoked.
/// </summary>
public string? EndpointName { get; set; }

/// <summary>
/// Gets or sets the HTTP method name to use when sending the request.
/// </summary>
public string? MethodName { get; set; }

/// <summary>
/// Gets or sets how the HTTP response content should be returned as command result data.
/// </summary>
public HttpCommandResultMode ResultMode { get; set; }
}
Loading
Loading