diff --git a/src/Aspire.Cli/Backchannel/AppHostAuxiliaryBackchannel.cs b/src/Aspire.Cli/Backchannel/AppHostAuxiliaryBackchannel.cs index b1dfa951ae3..4c461b91fbc 100644 --- a/src/Aspire.Cli/Backchannel/AppHostAuxiliaryBackchannel.cs +++ b/src/Aspire.Cli/Backchannel/AppHostAuxiliaryBackchannel.cs @@ -464,6 +464,7 @@ public async Task GetResourcesV2Async(GetResourcesRequest? { // Fall back to v1 var snapshots = await GetResourceSnapshotsAsync(cancellationToken).ConfigureAwait(false); + var includeHidden = request?.IncludeHidden is true; // Apply filter if specified if (!string.IsNullOrEmpty(request?.Filter)) @@ -472,6 +473,11 @@ public async Task GetResourcesV2Async(GetResourcesRequest? snapshots = snapshots.Where(s => s.Name.Contains(filter, StringComparison.OrdinalIgnoreCase)).ToList(); } + if (!includeHidden) + { + snapshots = snapshots.Where(s => !s.IsHidden).ToList(); + } + return new GetResourcesResponse { Resources = snapshots.ToArray() @@ -503,12 +509,19 @@ public async IAsyncEnumerable WatchResourcesV2Async( { // Fall back to v1 var filter = request?.Filter; + var includeHidden = request?.IncludeHidden is true; await foreach (var snapshot in WatchResourceSnapshotsAsync(cancellationToken).ConfigureAwait(false)) { if (!string.IsNullOrEmpty(filter) && !snapshot.Name.Contains(filter, StringComparison.OrdinalIgnoreCase)) { continue; } + + if (!includeHidden && snapshot.IsHidden) + { + continue; + } + yield return snapshot; } yield break; diff --git a/src/Aspire.Cli/Backchannel/IAppHostAuxiliaryBackchannel.cs b/src/Aspire.Cli/Backchannel/IAppHostAuxiliaryBackchannel.cs index b97adaf08fe..5a3b73dc80d 100644 --- a/src/Aspire.Cli/Backchannel/IAppHostAuxiliaryBackchannel.cs +++ b/src/Aspire.Cli/Backchannel/IAppHostAuxiliaryBackchannel.cs @@ -55,6 +55,14 @@ internal interface IAppHostAuxiliaryBackchannel : IDisposable /// A list of resource snapshots representing current state. Task> GetResourceSnapshotsAsync(CancellationToken cancellationToken = default); + /// + /// Gets resources using the v2 API. + /// + /// The request with optional filtering and hidden-resource inclusion. + /// Cancellation token. + /// The resources response. + Task GetResourcesV2Async(GetResourcesRequest? request = null, CancellationToken cancellationToken = default); + /// /// Watches for resource snapshot changes and streams them from the AppHost. /// @@ -62,6 +70,14 @@ internal interface IAppHostAuxiliaryBackchannel : IDisposable /// An async enumerable of resource snapshots as they change. IAsyncEnumerable WatchResourceSnapshotsAsync(CancellationToken cancellationToken = default); + /// + /// Watches resources using the v2 API. + /// + /// The request with optional filtering and hidden-resource inclusion. + /// Cancellation token. + /// An async enumerable of resource snapshots. + IAsyncEnumerable WatchResourcesV2Async(WatchResourcesRequest? request = null, CancellationToken cancellationToken = default); + /// /// Gets resource log lines from the AppHost. /// diff --git a/src/Aspire.Cli/Commands/DescribeCommand.cs b/src/Aspire.Cli/Commands/DescribeCommand.cs index 0fdc9fdd292..f1eecf255a2 100644 --- a/src/Aspire.Cli/Commands/DescribeCommand.cs +++ b/src/Aspire.Cli/Commands/DescribeCommand.cs @@ -88,6 +88,10 @@ internal sealed class DescribeCommand : BaseCommand { Description = DescribeCommandStrings.JsonOptionDescription }; + private static readonly Option s_includeHiddenOption = new("--include-hidden") + { + Description = DescribeCommandStrings.IncludeHiddenOptionDescription + }; public DescribeCommand( IInteractionService interactionService, @@ -109,6 +113,7 @@ public DescribeCommand( Options.Add(s_appHostOption); Options.Add(s_followOption); Options.Add(s_formatOption); + Options.Add(s_includeHiddenOption); } protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken) @@ -119,6 +124,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell var passedAppHostProjectFile = parseResult.GetValue(s_appHostOption); var follow = parseResult.GetValue(s_followOption); var format = parseResult.GetValue(s_formatOption); + var includeHidden = parseResult.GetValue(s_includeHiddenOption); var result = await _connectionResolver.ResolveConnectionAsync( passedAppHostProjectFile, @@ -139,12 +145,15 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell // Get dashboard URL and resource snapshots in parallel before // dispatching to the snapshot or watch path. var dashboardUrlsTask = connection.GetDashboardUrlsAsync(cancellationToken); - var snapshotsTask = connection.GetResourceSnapshotsAsync(cancellationToken); + var snapshotsTask = connection.GetResourcesV2Async(new GetResourcesRequest + { + IncludeHidden = includeHidden + }, cancellationToken); await Task.WhenAll(dashboardUrlsTask, snapshotsTask).ConfigureAwait(false); var dashboardBaseUrl = TelemetryCommandHelpers.ExtractDashboardBaseUrl((await dashboardUrlsTask.ConfigureAwait(false))?.BaseUrlWithLoginToken); - var snapshots = await snapshotsTask.ConfigureAwait(false); + var snapshots = (await snapshotsTask.ConfigureAwait(false)).Resources; // Pre-resolve colors for all resource names so that assignment is // deterministic regardless of which resources are displayed. @@ -152,7 +161,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell if (follow) { - return await ExecuteWatchAsync(connection, snapshots, dashboardBaseUrl, resourceName, format, cancellationToken); + return await ExecuteWatchAsync(connection, snapshots, dashboardBaseUrl, resourceName, format, includeHidden, cancellationToken); } else { @@ -192,7 +201,7 @@ private int ExecuteSnapshot(IReadOnlyList snapshots, string? d return ExitCodeConstants.Success; } - private async Task ExecuteWatchAsync(IAppHostAuxiliaryBackchannel connection, IReadOnlyList initialSnapshots, string? dashboardBaseUrl, string? resourceName, OutputFormat format, CancellationToken cancellationToken) + private async Task ExecuteWatchAsync(IAppHostAuxiliaryBackchannel connection, IReadOnlyList initialSnapshots, string? dashboardBaseUrl, string? resourceName, OutputFormat format, bool includeHidden, CancellationToken cancellationToken) { // Maintain a dictionary of the current state per resource for relationship resolution // and display name deduplication. Keyed by snapshot.Name so each resource has exactly @@ -208,7 +217,10 @@ private async Task ExecuteWatchAsync(IAppHostAuxiliaryBackchannel connectio var lastDisplayedContent = new Dictionary(StringComparers.ResourceName); // Stream resource snapshots - await foreach (var snapshot in connection.WatchResourceSnapshotsAsync(cancellationToken).ConfigureAwait(false)) + await foreach (var snapshot in connection.WatchResourcesV2Async(new WatchResourcesRequest + { + IncludeHidden = includeHidden + }, cancellationToken).ConfigureAwait(false)) { // Update the dictionary with the latest state for this resource allResources[snapshot.Name] = snapshot; diff --git a/src/Aspire.Cli/Resources/DescribeCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/DescribeCommandStrings.Designer.cs index 5af196f709e..4ee2c62ed17 100644 --- a/src/Aspire.Cli/Resources/DescribeCommandStrings.Designer.cs +++ b/src/Aspire.Cli/Resources/DescribeCommandStrings.Designer.cs @@ -63,6 +63,12 @@ public static string JsonOptionDescription { } } + public static string IncludeHiddenOptionDescription { + get { + return ResourceManager.GetString("IncludeHiddenOptionDescription", resourceCulture); + } + } + public static string NoAppHostFound { get { return ResourceManager.GetString("NoAppHostFound", resourceCulture); diff --git a/src/Aspire.Cli/Resources/DescribeCommandStrings.resx b/src/Aspire.Cli/Resources/DescribeCommandStrings.resx index 02ae09ec64a..e987e4e4a2a 100644 --- a/src/Aspire.Cli/Resources/DescribeCommandStrings.resx +++ b/src/Aspire.Cli/Resources/DescribeCommandStrings.resx @@ -126,6 +126,9 @@ Output format (Table or Json) + + Include hidden resources in the output + No AppHost project found. diff --git a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.cs.xlf index 21a8ade34a2..b58d91c8154 100644 --- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.cs.xlf +++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.cs.xlf @@ -37,6 +37,11 @@ Typ + + Include hidden resources in the output + Include hidden resources in the output + + Output format (Table or Json) Výstup ve formátu JSON pro spotřebu počítače. diff --git a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.de.xlf index 67dbf06b15c..71de6f20058 100644 --- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.de.xlf +++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.de.xlf @@ -37,6 +37,11 @@ Typ + + Include hidden resources in the output + Include hidden resources in the output + + Output format (Table or Json) Ausgabe im JSON-Format zur maschinellen Verarbeitung. diff --git a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.es.xlf index e7fc87444d6..0c9e17791a5 100644 --- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.es.xlf +++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.es.xlf @@ -37,6 +37,11 @@ Tipo + + Include hidden resources in the output + Include hidden resources in the output + + Output format (Table or Json) Salida en formato JSON para el consumo de la máquina. diff --git a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.fr.xlf index d86dfd96846..de914f1d8f3 100644 --- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.fr.xlf +++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.fr.xlf @@ -37,6 +37,11 @@ Type + + Include hidden resources in the output + Include hidden resources in the output + + Output format (Table or Json) Générer la sortie au format JSON pour traitement automatique. diff --git a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.it.xlf index 0ad4ae7e3a4..1963cbb8ebf 100644 --- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.it.xlf +++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.it.xlf @@ -37,6 +37,11 @@ Tipo + + Include hidden resources in the output + Include hidden resources in the output + + Output format (Table or Json) Output in formato JSON per l'utilizzo da parte del computer. diff --git a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ja.xlf index 79ac2a7a571..f8826532c6a 100644 --- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ja.xlf +++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ja.xlf @@ -37,6 +37,11 @@ + + Include hidden resources in the output + Include hidden resources in the output + + Output format (Table or Json) 機械処理用の JSON 形式で出力します。 @@ -64,4 +69,4 @@ - + \ No newline at end of file diff --git a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ko.xlf index 60fac2f9f1f..91e7176191f 100644 --- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ko.xlf +++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ko.xlf @@ -37,6 +37,11 @@ 형식 + + Include hidden resources in the output + Include hidden resources in the output + + Output format (Table or Json) 컴퓨터 사용량에 대한 JSON형식의 출력입니다. diff --git a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.pl.xlf index 0df2633b17e..d55ad48be3d 100644 --- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.pl.xlf +++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.pl.xlf @@ -37,6 +37,11 @@ Typ + + Include hidden resources in the output + Include hidden resources in the output + + Output format (Table or Json) Wynik w formacie JSON do przetwarzania przez maszynę. diff --git a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.pt-BR.xlf index e3aaa9ba42c..37c182e1277 100644 --- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.pt-BR.xlf +++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.pt-BR.xlf @@ -37,6 +37,11 @@ Tipo + + Include hidden resources in the output + Include hidden resources in the output + + Output format (Table or Json) Saída no formato JSON para consumo do computador. diff --git a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ru.xlf index 56763629412..a3cc80b1b73 100644 --- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ru.xlf +++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ru.xlf @@ -37,6 +37,11 @@ Тип + + Include hidden resources in the output + Include hidden resources in the output + + Output format (Table or Json) Вывод в формате JSON для потребления компьютером. diff --git a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.tr.xlf index 54b0a58fa7e..5dad3c0ba9e 100644 --- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.tr.xlf +++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.tr.xlf @@ -37,6 +37,11 @@ Tür + + Include hidden resources in the output + Include hidden resources in the output + + Output format (Table or Json) Makine tarafından kullanılmak üzere JSON biçiminde çıktı ver. diff --git a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.zh-Hans.xlf index bf95d804387..93a668d2292 100644 --- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.zh-Hans.xlf +++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.zh-Hans.xlf @@ -37,6 +37,11 @@ 类型 + + Include hidden resources in the output + Include hidden resources in the output + + Output format (Table or Json) 以 JSON 格式输出,供计算机使用。 diff --git a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.zh-Hant.xlf index f789855b264..45d05384832 100644 --- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.zh-Hant.xlf +++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.zh-Hant.xlf @@ -37,6 +37,11 @@ 類型 + + Include hidden resources in the output + Include hidden resources in the output + + Output format (Table or Json) 輸出為 JSON 格式供機器使用。 diff --git a/src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs b/src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs index 9b642300c6c..ca43f27f48a 100644 --- a/src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs +++ b/src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs @@ -106,7 +106,7 @@ public async Task GetDashboardInfoAsync(GetDashboardIn /// The resources response containing snapshots. public async Task GetResourcesAsync(GetResourcesRequest? request = null, CancellationToken cancellationToken = default) { - var snapshots = await GetResourceSnapshotsAsync(cancellationToken).ConfigureAwait(false); + var snapshots = await GetResourceSnapshotsCoreAsync(request?.IncludeHidden is true, cancellationToken).ConfigureAwait(false); // Apply filter if specified if (!string.IsNullOrEmpty(request?.Filter)) @@ -131,7 +131,7 @@ public async IAsyncEnumerable WatchResourcesAsync(WatchResourc { var filter = request?.Filter; - await foreach (var snapshot in WatchResourceSnapshotsAsync(cancellationToken).ConfigureAwait(false)) + await foreach (var snapshot in WatchResourceSnapshotsCoreAsync(request?.IncludeHidden is true, cancellationToken).ConfigureAwait(false)) { // Apply filter if specified if (!string.IsNullOrEmpty(filter) && !snapshot.Name.Contains(filter, StringComparison.OrdinalIgnoreCase)) @@ -404,7 +404,12 @@ public async Task GetDashboardUrlsAsync(CancellationToken ca /// /// A cancellation token. /// A list of resource snapshots. - public async Task> GetResourceSnapshotsAsync(CancellationToken cancellationToken = default) + public Task> GetResourceSnapshotsAsync(CancellationToken cancellationToken = default) + { + return GetResourceSnapshotsCoreAsync(includeHiddenResources: false, cancellationToken); + } + + private async Task> GetResourceSnapshotsCoreAsync(bool includeHiddenResources, CancellationToken cancellationToken) { var appModel = serviceProvider.GetService(); var notificationService = serviceProvider.GetRequiredService(); @@ -418,26 +423,20 @@ public async Task> GetResourceSnapshotsAsync(Cancellation // Get current state for each resource directly using TryGetCurrentState foreach (var resource in appModel.Resources) { - // Skip the dashboard resource - if (string.Equals(resource.Name, KnownResourceNames.AspireDashboard, StringComparisons.ResourceName)) - { - continue; - } - foreach (var instanceName in resource.GetResolvedResourceNames()) { - await AddResult(instanceName).ConfigureAwait(false); + await AddResult(instanceName, includeHiddenResources).ConfigureAwait(false); } } return results; - async Task AddResult(string resourceName) + async Task AddResult(string resourceName, bool includeHidden) { if (notificationService.TryGetCurrentState(resourceName, out var resourceEvent)) { var snapshot = await CreateResourceSnapshotFromEventAsync(resourceEvent, cancellationToken).ConfigureAwait(false); - if (snapshot is not null) + if (snapshot is not null && (includeHidden || !snapshot.IsHidden)) { results.Add(snapshot); } @@ -450,7 +449,12 @@ async Task AddResult(string resourceName) /// /// A cancellation token. /// An async enumerable of resource snapshots as they change. - public async IAsyncEnumerable WatchResourceSnapshotsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + public IAsyncEnumerable WatchResourceSnapshotsAsync(CancellationToken cancellationToken = default) + { + return WatchResourceSnapshotsCoreAsync(includeHiddenResources: false, cancellationToken); + } + + private async IAsyncEnumerable WatchResourceSnapshotsCoreAsync(bool includeHiddenResources, [EnumeratorCancellation] CancellationToken cancellationToken) { var notificationService = serviceProvider.GetRequiredService(); @@ -458,14 +462,8 @@ public async IAsyncEnumerable WatchResourceSnapshotsAsync([Enu await foreach (var resourceEvent in resourceEvents.WithCancellation(cancellationToken).ConfigureAwait(false)) { - // Skip the dashboard resource - if (string.Equals(resourceEvent.Resource.Name, KnownResourceNames.AspireDashboard, StringComparisons.ResourceName)) - { - continue; - } - var snapshot = await CreateResourceSnapshotFromEventAsync(resourceEvent, cancellationToken).ConfigureAwait(false); - if (snapshot is not null) + if (snapshot is not null && (includeHiddenResources || !snapshot.IsHidden)) { yield return snapshot; } @@ -597,6 +595,7 @@ public async IAsyncEnumerable WatchResourceSnapshotsAsync([Enu DisplayName = resource.Name, ResourceType = snapshot.ResourceType, State = snapshot.State?.Text, + IsHidden = snapshot.IsHidden, StateStyle = snapshot.State?.Style, HealthStatus = snapshot.HealthStatus?.ToString(), ExitCode = snapshot.ExitCode, diff --git a/src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs b/src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs index 350906a0988..31b89daa752 100644 --- a/src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs +++ b/src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs @@ -142,6 +142,11 @@ internal sealed class GetResourcesRequest /// Gets an optional filter pattern for resource names. /// public string? Filter { get; init; } + + /// + /// Gets whether hidden resources should be included. + /// + public bool? IncludeHidden { get; init; } } /// @@ -164,6 +169,11 @@ internal sealed class WatchResourcesRequest /// Gets an optional filter pattern for resource names. /// public string? Filter { get; init; } + + /// + /// Gets whether hidden resources should be included. + /// + public bool? IncludeHidden { get; init; } } /// @@ -742,6 +752,11 @@ public string? Type /// public string? State { get; init; } + /// + /// Gets a value indicating whether the resource is hidden. + /// + public bool IsHidden { get; init; } + /// /// Gets the state style hint (e.g., "success", "error", "warning"). /// diff --git a/tests/Aspire.Cli.Tests/Commands/DescribeCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/DescribeCommandTests.cs index 58c0a7eb120..f5cf67dc6b3 100644 --- a/tests/Aspire.Cli.Tests/Commands/DescribeCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/DescribeCommandTests.cs @@ -155,6 +155,21 @@ public async Task DescribeCommand_FollowAndFormat_CanBeCombined() Assert.Equal(ExitCodeConstants.Success, exitCode); } + [Fact] + public async Task DescribeCommand_IncludeHiddenOption_CanBeUsedWithHelp() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper); + var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("describe --include-hidden --help"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(ExitCodeConstants.Success, exitCode); + } + [Fact] public async Task DescribeCommand_ResourceNameArgument_CanBeParsed() { @@ -390,6 +405,57 @@ public async Task DescribeCommand_Follow_JsonFormat_StripsLoginPathFromDashboard Assert.Equal("http://localhost:18888/?resource=redis", resource.DashboardUrl); } + [Fact] + public async Task DescribeCommand_DefaultOutput_ExcludesHiddenResources() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var outputWriter = new TestOutputTextWriter(outputHelper); + var provider = CreateDescribeTestServices(workspace, outputWriter, [ + new ResourceSnapshot { Name = "frontend", DisplayName = "frontend", ResourceType = "Project", State = "Running" }, + new ResourceSnapshot { Name = "aspire-dashboard", DisplayName = "aspire-dashboard", ResourceType = "Executable", State = "Running", IsHidden = true }, + ]); + + var command = provider.GetRequiredService(); + var result = command.Parse("describe --format json"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(ExitCodeConstants.Success, exitCode); + + var jsonOutput = string.Join("", outputWriter.Logs); + var deserialized = JsonSerializer.Deserialize(jsonOutput, ResourcesCommandJsonContext.RelaxedEscaping.ResourcesOutput); + + Assert.NotNull(deserialized); + Assert.Single(deserialized.Resources); + Assert.Equal("frontend", deserialized.Resources[0].Name); + } + + [Fact] + public async Task DescribeCommand_IncludeHiddenOutput_IncludesHiddenResources() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var outputWriter = new TestOutputTextWriter(outputHelper); + var provider = CreateDescribeTestServices(workspace, outputWriter, [ + new ResourceSnapshot { Name = "frontend", DisplayName = "frontend", ResourceType = "Project", State = "Running" }, + new ResourceSnapshot { Name = "aspire-dashboard", DisplayName = "aspire-dashboard", ResourceType = "Executable", State = "Running", IsHidden = true }, + ]); + + var command = provider.GetRequiredService(); + var result = command.Parse("describe --format json --include-hidden"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(ExitCodeConstants.Success, exitCode); + + var jsonOutput = string.Join("", outputWriter.Logs); + var deserialized = JsonSerializer.Deserialize(jsonOutput, ResourcesCommandJsonContext.RelaxedEscaping.ResourcesOutput); + + Assert.NotNull(deserialized); + Assert.Equal(2, deserialized.Resources.Length); + Assert.Contains(deserialized.Resources, r => r.Name == "frontend"); + Assert.Contains(deserialized.Resources, r => r.Name == "aspire-dashboard"); + } + private ServiceProvider CreateDescribeTestServices( TemporaryWorkspace workspace, TestOutputTextWriter outputWriter, diff --git a/tests/Aspire.Cli.Tests/TestServices/TestAppHostAuxiliaryBackchannel.cs b/tests/Aspire.Cli.Tests/TestServices/TestAppHostAuxiliaryBackchannel.cs index 16d1eee74eb..3093efa7fd9 100644 --- a/tests/Aspire.Cli.Tests/TestServices/TestAppHostAuxiliaryBackchannel.cs +++ b/tests/Aspire.Cli.Tests/TestServices/TestAppHostAuxiliaryBackchannel.cs @@ -63,16 +63,39 @@ public Task> GetResourceSnapshotsAsync(CancellationToken return GetResourceSnapshotsHandler(cancellationToken); } - return Task.FromResult(ResourceSnapshots); + return Task.FromResult(ApplyResourceFilters(ResourceSnapshots, filter: null, includeHidden: false).ToList()); + } + + public async Task GetResourcesV2Async(GetResourcesRequest? request = null, CancellationToken cancellationToken = default) + { + if (GetResourceSnapshotsHandler is not null) + { + return new GetResourcesResponse + { + Resources = ApplyResourceFilters(await GetResourceSnapshotsHandler(cancellationToken).ConfigureAwait(false), request).ToArray() + }; + } + + return new GetResourcesResponse + { + Resources = ApplyResourceFilters(ResourceSnapshots, request).ToArray() + }; } public async IAsyncEnumerable WatchResourceSnapshotsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) { - foreach (var snapshot in ResourceSnapshots) + foreach (var snapshot in ApplyResourceFilters(ResourceSnapshots, filter: null, includeHidden: false)) + { + yield return snapshot; + } + } + + public async IAsyncEnumerable WatchResourcesV2Async(WatchResourcesRequest? request = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + foreach (var snapshot in ApplyResourceFilters(ResourceSnapshots, request)) { yield return snapshot; } - await Task.CompletedTask; } public async IAsyncEnumerable GetResourceLogsAsync( @@ -110,6 +133,29 @@ public Task ExecuteResourceCommandAsync( return Task.FromResult(ExecuteResourceCommandResult); } + private static IEnumerable ApplyResourceFilters(IEnumerable snapshots, GetResourcesRequest? request) + => ApplyResourceFilters(snapshots, request?.Filter, request?.IncludeHidden is true); + + private static IEnumerable ApplyResourceFilters(IEnumerable snapshots, WatchResourcesRequest? request) + => ApplyResourceFilters(snapshots, request?.Filter, request?.IncludeHidden is true); + + private static IEnumerable ApplyResourceFilters(IEnumerable snapshots, string? filter, bool includeHidden) + { + var result = snapshots; + + if (!string.IsNullOrEmpty(filter)) + { + result = result.Where(r => r.Name.Contains(filter, StringComparison.OrdinalIgnoreCase)); + } + + if (!includeHidden) + { + result = result.Where(r => !r.IsHidden); + } + + return result; + } + /// /// Gets or sets the result to return from WaitForResourceAsync. ///