Skip to content
Merged
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
69 changes: 69 additions & 0 deletions src/Aspire.Cli/Backchannel/AppHostAuxiliaryBackchannel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ namespace Aspire.Cli.Backchannel;
/// </summary>
internal sealed class AppHostAuxiliaryBackchannel : IAppHostAuxiliaryBackchannel
{
private static readonly string[] s_clientCapabilities =
[
AuxiliaryBackchannelCapabilities.V1,
AuxiliaryBackchannelCapabilities.V2,
AuxiliaryBackchannelCapabilities.V3
];

private readonly ILogger _logger;
private JsonRpc? _rpc;
private bool _disposed;
Expand Down Expand Up @@ -332,6 +339,22 @@ [new WaitForAppHostReadyRequest()],
/// <inheritdoc />
public async Task<List<ResourceSnapshot>> GetResourceSnapshotsAsync(bool includeHidden, CancellationToken cancellationToken = default)
{
if (SupportsV2)
{
var response = await GetResourcesV2Async(new GetResourcesRequest
{
ClientCapabilities = s_clientCapabilities
}, cancellationToken).ConfigureAwait(false);
var snapshots = response.Resources.ToList();

if (!includeHidden)
{
snapshots = snapshots.Where(s => !ResourceSnapshotMapper.IsHiddenResource(s)).ToList();
}

return snapshots.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase).ToList();
}

var rpc = EnsureConnected();

_logger.LogDebug("Getting resource snapshots");
Expand Down Expand Up @@ -363,6 +386,24 @@ public async Task<List<ResourceSnapshot>> GetResourceSnapshotsAsync(bool include
/// <inheritdoc />
public async IAsyncEnumerable<ResourceSnapshot> WatchResourceSnapshotsAsync(bool includeHidden, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
if (SupportsV2)
{
await foreach (var snapshot in WatchResourcesV2Async(new WatchResourcesRequest
{
ClientCapabilities = s_clientCapabilities
}, cancellationToken).ConfigureAwait(false))
{
if (!includeHidden && ResourceSnapshotMapper.IsHiddenResource(snapshot))
{
continue;
}

yield return snapshot;
}

yield break;
}

var rpc = EnsureConnected();

_logger.LogDebug("Starting resource snapshots watch");
Expand Down Expand Up @@ -555,6 +596,8 @@ [new GetDashboardInfoRequest()],
/// <returns>The resources response.</returns>
public async Task<GetResourcesResponse> GetResourcesV2Async(GetResourcesRequest? request = null, CancellationToken cancellationToken = default)
{
request = AddClientCapabilities(request);

if (!SupportsV2)
{
// Fall back to v1
Expand Down Expand Up @@ -596,6 +639,8 @@ public async IAsyncEnumerable<ResourceSnapshot> WatchResourcesV2Async(
WatchResourcesRequest? request = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
request = AddClientCapabilities(request);

if (!SupportsV2)
{
// Fall back to v1
Expand Down Expand Up @@ -641,6 +686,30 @@ public async IAsyncEnumerable<ResourceSnapshot> WatchResourcesV2Async(
}
}

private static GetResourcesRequest AddClientCapabilities(GetResourcesRequest? request)
{
return request is null
? new GetResourcesRequest { ClientCapabilities = s_clientCapabilities }
: new GetResourcesRequest
{
TraceContext = request.TraceContext,
Filter = request.Filter,
ClientCapabilities = s_clientCapabilities
};
}

private static WatchResourcesRequest AddClientCapabilities(WatchResourcesRequest? request)
{
return request is null
? new WatchResourcesRequest { ClientCapabilities = s_clientCapabilities }
: new WatchResourcesRequest
{
TraceContext = request.TraceContext,
Filter = request.Filter,
ClientCapabilities = s_clientCapabilities
};
}

/// <summary>
/// Gets console logs using the v2 API.
/// Falls back to v1 if not supported.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,7 @@ private void UpdateResourceActionsMenu()
IsCommandExecuting,
showViewDetails: false,
showConsoleLogsItem: true,
showUrls: true,
showStartCommand: false);
showUrls: true);
}

private async Task ExecuteResourceCommandAsync(ResourceViewModel resource, CommandViewModel command)
Expand Down
8 changes: 3 additions & 5 deletions src/Aspire.Dashboard/Model/ResourceMenuBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,7 @@ public void AddMenuItems(
Func<ResourceViewModel, CommandViewModel, bool> isCommandExecuting,
bool showViewDetails,
bool showConsoleLogsItem,
bool showUrls,
bool showStartCommand = true)
bool showUrls)
{
if (showViewDetails)
{
Expand Down Expand Up @@ -166,7 +165,7 @@ await _aiContextProvider.LaunchAssistantSidebarAsync(

AddTelemetryMenuItems(menuItems, resource, resourceByName);

AddCommandMenuItems(menuItems, resource, commandSelected, isCommandExecuting, showStartCommand);
AddCommandMenuItems(menuItems, resource, commandSelected, isCommandExecuting);

if (showUrls)
{
Expand Down Expand Up @@ -283,10 +282,9 @@ private void AddTelemetryMenuItems(List<MenuButtonItem> menuItems, ResourceViewM
}
}

private void AddCommandMenuItems(List<MenuButtonItem> menuItems, ResourceViewModel resource, EventCallback<CommandViewModel> commandSelected, Func<ResourceViewModel, CommandViewModel, bool> isCommandExecuting, bool showStartCommand)
private void AddCommandMenuItems(List<MenuButtonItem> menuItems, ResourceViewModel resource, EventCallback<CommandViewModel> commandSelected, Func<ResourceViewModel, CommandViewModel, bool> isCommandExecuting)
{
var menuCommands = resource.Commands
.Where(c => showStartCommand || !c.Name.Equals(CommandViewModel.StartCommand, StringComparisons.CommandName))
.Where(c => c.State != CommandViewModelState.Hidden)
.ToList();

Expand Down
71 changes: 66 additions & 5 deletions src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Nodes;
Expand Down Expand Up @@ -118,7 +119,7 @@ public async Task<GetDashboardInfoResponse> GetDashboardInfoAsync(GetDashboardIn
public async Task<GetResourcesResponse> GetResourcesAsync(GetResourcesRequest? request = null, CancellationToken cancellationToken = default)
{
using var activity = profilingTelemetry.StartJsonRpcServerCall(nameof(GetResourcesAsync), streaming: false, request?.TraceContext);
var snapshots = await GetResourceSnapshotsAsync(cancellationToken).ConfigureAwait(false);
var snapshots = await GetResourceSnapshotsAsync(SupportsJsonResourceProperties(request?.ClientCapabilities), cancellationToken).ConfigureAwait(false);

// Apply filter if specified
if (!string.IsNullOrEmpty(request?.Filter))
Expand Down Expand Up @@ -147,7 +148,7 @@ public async IAsyncEnumerable<ResourceSnapshot> WatchResourcesAsync(WatchResourc

try
{
await foreach (var snapshot in WatchResourceSnapshotsAsync(cancellationToken).ConfigureAwait(false))
await foreach (var snapshot in WatchResourceSnapshotsAsync(SupportsJsonResourceProperties(request?.ClientCapabilities), cancellationToken).ConfigureAwait(false))
{
// Apply filter if specified
if (!string.IsNullOrEmpty(filter) && !snapshot.Name.Contains(filter, StringComparison.OrdinalIgnoreCase))
Expand Down Expand Up @@ -734,6 +735,11 @@ public async Task<WaitForAppHostReadyResponse> WaitForAppHostReadyAsync(WaitForA
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>A list of resource snapshots.</returns>
public async Task<List<ResourceSnapshot>> GetResourceSnapshotsAsync(CancellationToken cancellationToken = default)
{
return await GetResourceSnapshotsAsync(resourcePropertiesAsJson: false, cancellationToken).ConfigureAwait(false);
}

private async Task<List<ResourceSnapshot>> GetResourceSnapshotsAsync(bool resourcePropertiesAsJson, CancellationToken cancellationToken)
{
var appModel = serviceProvider.GetRequiredService<DistributedApplicationModel>();
var notificationService = serviceProvider.GetRequiredService<ResourceNotificationService>();
Expand All @@ -754,7 +760,7 @@ async Task AddResult(string resourceName)
{
if (notificationService.TryGetCurrentState(resourceName, out var resourceEvent))
{
var snapshot = await CreateResourceSnapshotFromEventAsync(resourceEvent, cancellationToken).ConfigureAwait(false);
var snapshot = await CreateResourceSnapshotFromEventAsync(resourceEvent, resourcePropertiesAsJson, cancellationToken).ConfigureAwait(false);
if (snapshot is not null)
{
results.Add(snapshot);
Expand All @@ -769,14 +775,24 @@ async Task AddResult(string resourceName)
/// <param name="cancellationToken">A cancellation token.</param>
/// <returns>An async enumerable of resource snapshots as they change.</returns>
public async IAsyncEnumerable<ResourceSnapshot> WatchResourceSnapshotsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var snapshot in WatchResourceSnapshotsAsync(resourcePropertiesAsJson: false, cancellationToken).ConfigureAwait(false))
{
yield return snapshot;
}
}

private async IAsyncEnumerable<ResourceSnapshot> WatchResourceSnapshotsAsync(
bool resourcePropertiesAsJson,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
var notificationService = serviceProvider.GetRequiredService<ResourceNotificationService>();

var resourceEvents = notificationService.WatchAsync(cancellationToken);

await foreach (var resourceEvent in resourceEvents.WithCancellation(cancellationToken).ConfigureAwait(false))
{
var snapshot = await CreateResourceSnapshotFromEventAsync(resourceEvent, cancellationToken).ConfigureAwait(false);
var snapshot = await CreateResourceSnapshotFromEventAsync(resourceEvent, resourcePropertiesAsJson, cancellationToken).ConfigureAwait(false);
if (snapshot is not null)
{
yield return snapshot;
Expand All @@ -786,6 +802,7 @@ public async IAsyncEnumerable<ResourceSnapshot> WatchResourceSnapshotsAsync([Enu

private async Task<ResourceSnapshot?> CreateResourceSnapshotFromEventAsync(
ResourceEvent resourceEvent,
bool resourcePropertiesAsJson,
CancellationToken cancellationToken)
{
var resource = resourceEvent.Resource;
Expand Down Expand Up @@ -881,7 +898,9 @@ public async IAsyncEnumerable<ResourceSnapshot> WatchResourceSnapshotsAsync([Enu
continue;
}

properties[prop.Name] = ConvertPropertyValueToJsonNode(prop.Value);
properties[prop.Name] = resourcePropertiesAsJson
? ConvertPropertyValueToJsonNode(prop.Value)
: ConvertPropertyValueToLegacyJsonNode(prop.Value);

if (string.Equals(prop.Name, KnownProperties.Resource.WaitingFor, StringComparisons.ResourcePropertyName))
{
Expand Down Expand Up @@ -971,6 +990,43 @@ private static ResourceSnapshotCommandArgument CreateCommandArgument(Interaction
};
}

private static JsonNode? ConvertPropertyValueToLegacyJsonNode(object? value)
{
var stringValue = ConvertPropertyValueToString(value);

return stringValue is null ? null : JsonValue.Create(stringValue);
}

private static string? ConvertPropertyValueToString(object? value)
{
return value switch
{
null => null,
JsonValue jsonValue when jsonValue.TryGetValue<string>(out var stringValue) => stringValue,
JsonValue jsonValue when jsonValue.TryGetValue<bool>(out var boolValue) => boolValue.ToString(CultureInfo.InvariantCulture),
JsonValue jsonValue when jsonValue.TryGetValue<IFormattable>(out var formattableValue) => formattableValue.ToString(null, CultureInfo.InvariantCulture),
JsonNode jsonNode => jsonNode.ToJsonString(),
string stringValue => stringValue,
IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture),
System.Collections.IEnumerable enumerable => ConvertEnumerablePropertyValueToString(enumerable),
_ => value.ToString()
};
}

private static string ConvertEnumerablePropertyValueToString(System.Collections.IEnumerable enumerable)
{
var values = new List<string>();
foreach (var value in enumerable)
{
if (ConvertPropertyValueToString(value) is { } stringValue)
{
values.Add(stringValue);
}
}

return string.Join(',', values);
}

private static JsonArray ConvertEnumerablePropertyValueToJsonArray(System.Collections.IEnumerable enumerable)
{
var array = new JsonArray();
Expand All @@ -982,6 +1038,11 @@ private static JsonArray ConvertEnumerablePropertyValueToJsonArray(System.Collec
return array;
}

private static bool SupportsJsonResourceProperties(string[]? clientCapabilities)
{
return clientCapabilities?.Contains(AuxiliaryBackchannelCapabilities.V3, StringComparer.Ordinal) == true;
}

private static string[]? GetStringArrayPropertyValue(object? value)
{
return value switch
Expand Down
19 changes: 16 additions & 3 deletions src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ internal static class AuxiliaryBackchannelCapabilities
public const string V2 = "aux.v2";

/// <summary>
/// Version 3 capabilities: Batched console log streaming and AppHost startup readiness wait.
/// Version 3 capabilities: Batched console log streaming, AppHost startup readiness wait,
/// and JSON-valued resource properties when requested by the client.
/// </summary>
public const string V3 = "aux.v3";
}
Expand Down Expand Up @@ -240,11 +241,17 @@ internal sealed class GetResourcesRequest : BackchannelRequest
/// </summary>
public string? Filter { get; init; }

/// <summary>
/// Gets the auxiliary backchannel capabilities supported by the client.
/// </summary>
public string[] ClientCapabilities { get; init; } = [];

/// <inheritdoc />
public override GetResourcesRequest WithTraceContext(BackchannelTraceContext traceContext) => new()
{
TraceContext = traceContext,
Filter = Filter
Filter = Filter,
ClientCapabilities = ClientCapabilities
};
}

Expand All @@ -269,11 +276,17 @@ internal sealed class WatchResourcesRequest : BackchannelRequest
/// </summary>
public string? Filter { get; init; }

/// <summary>
/// Gets the auxiliary backchannel capabilities supported by the client.
/// </summary>
public string[] ClientCapabilities { get; init; } = [];

/// <inheritdoc />
public override WatchResourcesRequest WithTraceContext(BackchannelTraceContext traceContext) => new()
{
TraceContext = traceContext,
Filter = Filter
Filter = Filter,
ClientCapabilities = ClientCapabilities
};
}

Expand Down
Loading
Loading