diff --git a/src/Aspire.Cli/Backchannel/AppHostAuxiliaryBackchannel.cs b/src/Aspire.Cli/Backchannel/AppHostAuxiliaryBackchannel.cs
index b1dfa951ae3..530e5e207b1 100644
--- a/src/Aspire.Cli/Backchannel/AppHostAuxiliaryBackchannel.cs
+++ b/src/Aspire.Cli/Backchannel/AppHostAuxiliaryBackchannel.cs
@@ -256,7 +256,7 @@ await rpc.InvokeWithCancellationAsync(
}
///
- public async Task> GetResourceSnapshotsAsync(CancellationToken cancellationToken = default)
+ public async Task> GetResourceSnapshotsAsync(bool includeHidden, CancellationToken cancellationToken = default)
{
var rpc = EnsureConnected();
@@ -269,10 +269,13 @@ public async Task> GetResourceSnapshotsAsync(Cancellation
[],
cancellationToken).ConfigureAwait(false) ?? [];
- // Sort resources by name for consistent ordering.
- snapshots.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
+ if (!includeHidden)
+ {
+ snapshots = snapshots.Where(s => !ResourceSnapshotMapper.IsHiddenResource(s)).ToList();
+ }
- return snapshots;
+ // Sort resources by name for consistent ordering.
+ return snapshots.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase).ToList();
}
catch (RemoteMethodNotFoundException ex)
{
@@ -282,7 +285,7 @@ public async Task> GetResourceSnapshotsAsync(Cancellation
}
///
- public async IAsyncEnumerable WatchResourceSnapshotsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
+ public async IAsyncEnumerable WatchResourceSnapshotsAsync(bool includeHidden, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var rpc = EnsureConnected();
@@ -309,6 +312,11 @@ public async IAsyncEnumerable WatchResourceSnapshotsAsync([Enu
await foreach (var snapshot in snapshots.WithCancellation(cancellationToken).ConfigureAwait(false))
{
+ if (!includeHidden && ResourceSnapshotMapper.IsHiddenResource(snapshot))
+ {
+ continue;
+ }
+
yield return snapshot;
}
}
@@ -463,7 +471,7 @@ public async Task GetResourcesV2Async(GetResourcesRequest?
if (!SupportsV2)
{
// Fall back to v1
- var snapshots = await GetResourceSnapshotsAsync(cancellationToken).ConfigureAwait(false);
+ var snapshots = await GetResourceSnapshotsAsync(includeHidden: true, cancellationToken).ConfigureAwait(false);
// Apply filter if specified
if (!string.IsNullOrEmpty(request?.Filter))
@@ -503,7 +511,7 @@ public async IAsyncEnumerable WatchResourcesV2Async(
{
// Fall back to v1
var filter = request?.Filter;
- await foreach (var snapshot in WatchResourceSnapshotsAsync(cancellationToken).ConfigureAwait(false))
+ await foreach (var snapshot in WatchResourceSnapshotsAsync(includeHidden: true, cancellationToken).ConfigureAwait(false))
{
if (!string.IsNullOrEmpty(filter) && !snapshot.Name.Contains(filter, StringComparison.OrdinalIgnoreCase))
{
diff --git a/src/Aspire.Cli/Backchannel/IAppHostAuxiliaryBackchannel.cs b/src/Aspire.Cli/Backchannel/IAppHostAuxiliaryBackchannel.cs
index b97adaf08fe..3e48ff9d251 100644
--- a/src/Aspire.Cli/Backchannel/IAppHostAuxiliaryBackchannel.cs
+++ b/src/Aspire.Cli/Backchannel/IAppHostAuxiliaryBackchannel.cs
@@ -51,16 +51,18 @@ internal interface IAppHostAuxiliaryBackchannel : IDisposable
///
/// Gets the current resource snapshots from the AppHost.
///
+ /// When , includes resources with hidden state.
/// Cancellation token.
/// A list of resource snapshots representing current state.
- Task> GetResourceSnapshotsAsync(CancellationToken cancellationToken = default);
+ Task> GetResourceSnapshotsAsync(bool includeHidden, CancellationToken cancellationToken = default);
///
/// Watches for resource snapshot changes and streams them from the AppHost.
///
+ /// When , includes resources with hidden state.
/// Cancellation token.
/// An async enumerable of resource snapshots as they change.
- IAsyncEnumerable WatchResourceSnapshotsAsync(CancellationToken cancellationToken = default);
+ IAsyncEnumerable WatchResourceSnapshotsAsync(bool includeHidden, CancellationToken cancellationToken = default);
///
/// Gets resource log lines from the AppHost.
diff --git a/src/Aspire.Cli/Backchannel/ResourceSnapshotMapper.cs b/src/Aspire.Cli/Backchannel/ResourceSnapshotMapper.cs
index 9fd0cb212ae..58739420189 100644
--- a/src/Aspire.Cli/Backchannel/ResourceSnapshotMapper.cs
+++ b/src/Aspire.Cli/Backchannel/ResourceSnapshotMapper.cs
@@ -221,4 +221,42 @@ public static string GetResourceName(ResourceSnapshot resource, IEnumerable
+ /// Determines whether a resource snapshot represents a hidden resource.
+ /// A resource is hidden if its flag is set
+ /// or its is "Hidden".
+ ///
+ internal static bool IsHiddenResource(ResourceSnapshot snapshot)
+ {
+ return snapshot.IsHidden || string.Equals(snapshot.State, "Hidden", StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Filters a list of all resource snapshots based on hidden-resource visibility,
+ /// returning the visible snapshot list and a set of hidden resource names for log filtering.
+ /// When is or
+ /// is specified, all resources are included and the hidden set is empty.
+ ///
+ /// All resource snapshots (including hidden).
+ /// Whether the user explicitly requested hidden resources.
+ /// The specific resource name requested, or for all.
+ /// The effective include-hidden flag, the filtered snapshot list, and the set of hidden resource names.
+ internal static (bool EffectiveIncludeHidden, List Snapshots, HashSet HiddenResourceNames) FilterHiddenResources(
+ IReadOnlyList allSnapshots,
+ bool includeHidden,
+ string? resourceName)
+ {
+ var effectiveIncludeHidden = includeHidden || resourceName is not null;
+
+ var hiddenResourceNames = effectiveIncludeHidden
+ ? new HashSet(StringComparers.ResourceName)
+ : new HashSet(allSnapshots.Where(IsHiddenResource).Select(s => s.Name), StringComparers.ResourceName);
+
+ var snapshots = effectiveIncludeHidden
+ ? allSnapshots.ToList()
+ : allSnapshots.Where(s => !IsHiddenResource(s)).ToList();
+
+ return (effectiveIncludeHidden, snapshots, hiddenResourceNames);
+ }
}
diff --git a/src/Aspire.Cli/Backchannel/ResourceSnapshotWatcher.cs b/src/Aspire.Cli/Backchannel/ResourceSnapshotWatcher.cs
new file mode 100644
index 00000000000..cc8003b3d40
--- /dev/null
+++ b/src/Aspire.Cli/Backchannel/ResourceSnapshotWatcher.cs
@@ -0,0 +1,132 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Concurrent;
+
+namespace Aspire.Cli.Backchannel;
+
+///
+/// Watches for resource snapshot changes from an AppHost backchannel connection
+/// and maintains an up-to-date collection of resources.
+///
+internal sealed class ResourceSnapshotWatcher : IDisposable
+{
+ private readonly IAppHostAuxiliaryBackchannel _connection;
+ private readonly ConcurrentDictionary _resources = new(StringComparers.ResourceName);
+ private readonly CancellationTokenSource _cts = new();
+ private readonly TaskCompletionSource _initialLoadTcs = new(TaskCreationOptions.RunContinuationsAsynchronously);
+ private readonly Task _watchTask;
+ private volatile Exception? _watchException;
+
+ public ResourceSnapshotWatcher(IAppHostAuxiliaryBackchannel connection, bool includeHidden = false)
+ {
+ _connection = connection;
+ IncludeHidden = includeHidden;
+ _watchTask = WatchAsync(_cts.Token);
+ }
+
+ ///
+ /// Gets a value indicating whether hidden resources are included by default in .
+ ///
+ public bool IncludeHidden { get; }
+
+ ///
+ /// Waits until the initial resource snapshot load is complete.
+ ///
+ public Task WaitForInitialLoadAsync(CancellationToken cancellationToken = default)
+ {
+ return _initialLoadTcs.Task.WaitAsync(cancellationToken);
+ }
+
+ private async Task WatchAsync(CancellationToken cancellationToken)
+ {
+ try
+ {
+ var snapshots = await _connection.GetResourceSnapshotsAsync(includeHidden: true, cancellationToken).ConfigureAwait(false);
+
+ foreach (var snapshot in snapshots)
+ {
+ _resources[snapshot.Name] = snapshot;
+ }
+
+ _initialLoadTcs.TrySetResult();
+
+ await foreach (var snapshot in _connection.WatchResourceSnapshotsAsync(includeHidden: true, cancellationToken).ConfigureAwait(false))
+ {
+ _resources[snapshot.Name] = snapshot;
+ }
+ }
+ catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
+ {
+ _initialLoadTcs.TrySetCanceled(cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ if (!_initialLoadTcs.TrySetException(ex))
+ {
+ // Initial load already completed; store for callers to detect.
+ _watchException = ex;
+ }
+ }
+ }
+
+ ///
+ /// Gets the exception that terminated the watch loop after the initial load, or if the watch is still running.
+ ///
+ public Exception? WatchException => _watchException;
+
+ private void EnsureInitialLoadComplete()
+ {
+ if (!_initialLoadTcs.Task.IsCompletedSuccessfully)
+ {
+ throw new InvalidOperationException("Initial resource snapshot load has not completed. Call WaitForInitialLoadAsync first.");
+ }
+ }
+
+ ///
+ /// Gets a resource snapshot by name, or if not found.
+ ///
+ public ResourceSnapshot? GetResource(string name)
+ {
+ EnsureInitialLoadComplete();
+ return _resources.GetValueOrDefault(name);
+ }
+
+ ///
+ /// Gets all current resource snapshots, using to determine visibility.
+ ///
+ /// Resource snapshots, ordered by name.
+ public IEnumerable GetResources()
+ {
+ return GetResources(IncludeHidden);
+ }
+
+ ///
+ /// Gets all current resource snapshots, including hidden resources.
+ ///
+ /// All resource snapshots, ordered by name.
+ public IEnumerable GetAllResources()
+ {
+ return GetResources(includeHidden: true);
+ }
+
+ private IEnumerable GetResources(bool includeHidden)
+ {
+ EnsureInitialLoadComplete();
+
+ var snapshots = _resources.Values.AsEnumerable();
+
+ if (!includeHidden)
+ {
+ snapshots = snapshots.Where(s => !ResourceSnapshotMapper.IsHiddenResource(s));
+ }
+
+ return snapshots.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase);
+ }
+
+ public void Dispose()
+ {
+ _cts.Cancel();
+ _cts.Dispose();
+ }
+}
diff --git a/src/Aspire.Cli/Commands/DescribeCommand.cs b/src/Aspire.Cli/Commands/DescribeCommand.cs
index 564db60ea06..056fdc5f32a 100644
--- a/src/Aspire.Cli/Commands/DescribeCommand.cs
+++ b/src/Aspire.Cli/Commands/DescribeCommand.cs
@@ -89,6 +89,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,
@@ -110,6 +114,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)
@@ -120,6 +125,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,
@@ -137,27 +143,28 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell
var connection = result.Connection!;
- // Get dashboard URL and resource snapshots in parallel before
- // dispatching to the snapshot or watch path.
+ // Get dashboard URL while the watcher loads initial snapshots.
+ // When a specific resource is requested, always include hidden resources
+ // so the user can describe any resource by name.
+ var effectiveIncludeHidden = includeHidden || resourceName is not null;
var dashboardUrlsTask = connection.GetDashboardUrlsAsync(cancellationToken);
- var snapshotsTask = connection.GetResourceSnapshotsAsync(cancellationToken);
-
- await Task.WhenAll(dashboardUrlsTask, snapshotsTask).ConfigureAwait(false);
+ using var resourceWatcher = new ResourceSnapshotWatcher(connection, effectiveIncludeHidden);
+ await resourceWatcher.WaitForInitialLoadAsync(cancellationToken).ConfigureAwait(false);
var dashboardBaseUrl = TelemetryCommandHelpers.ExtractDashboardBaseUrl((await dashboardUrlsTask.ConfigureAwait(false))?.BaseUrlWithLoginToken);
- var snapshots = await snapshotsTask.ConfigureAwait(false);
// Pre-resolve colors for all resource names so that assignment is
// deterministic regardless of which resources are displayed.
- _resourceColorMap.ResolveAll(snapshots.Select(s => ResourceSnapshotMapper.GetResourceName(s, snapshots)));
+ var allSnapshots = resourceWatcher.GetAllResources();
+ _resourceColorMap.ResolveAll(allSnapshots.Select(s => ResourceSnapshotMapper.GetResourceName(s, allSnapshots)));
if (follow)
{
- return await ExecuteWatchAsync(connection, snapshots, dashboardBaseUrl, resourceName, format, cancellationToken);
+ return await ExecuteWatchAsync(connection, resourceWatcher, dashboardBaseUrl, resourceName, format, cancellationToken);
}
else
{
- return ExecuteSnapshot(snapshots, dashboardBaseUrl, resourceName, format);
+ return ExecuteSnapshot(resourceWatcher.GetResources().ToList(), dashboardBaseUrl, resourceName, format);
}
}
@@ -193,28 +200,23 @@ 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, ResourceSnapshotWatcher resourceWatcher, string? dashboardBaseUrl, string? resourceName, OutputFormat format, 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
- // one entry representing its latest state.
- var allResources = new Dictionary(StringComparers.ResourceName);
- foreach (var snapshot in initialSnapshots)
- {
- allResources[snapshot.Name] = snapshot;
- }
-
// Cache the last displayed content per resource to avoid duplicate output.
// Values are either a string (JSON mode) or a ResourceDisplayState (non-JSON mode).
var lastDisplayedContent = new Dictionary(StringComparers.ResourceName);
- // Stream resource snapshots
- await foreach (var snapshot in connection.WatchResourceSnapshotsAsync(cancellationToken).ConfigureAwait(false))
+ // Stream resource snapshots. The watcher keeps its dictionary up to date in the
+ // background, so we use it for relationship resolution and display name deduplication.
+ await foreach (var snapshot in connection.WatchResourceSnapshotsAsync(includeHidden: true, cancellationToken).ConfigureAwait(false))
{
- // Update the dictionary with the latest state for this resource
- allResources[snapshot.Name] = snapshot;
+ // Skip hidden resources when not included
+ if (!resourceWatcher.IncludeHidden && ResourceSnapshotMapper.IsHiddenResource(snapshot))
+ {
+ continue;
+ }
- var currentSnapshots = allResources.Values.ToList();
+ var currentSnapshots = resourceWatcher.GetAllResources().ToList();
// Filter by resource name if specified
if (resourceName is not null)
diff --git a/src/Aspire.Cli/Commands/ExportCommand.cs b/src/Aspire.Cli/Commands/ExportCommand.cs
index e635f4b4a22..9da2c7d83a8 100644
--- a/src/Aspire.Cli/Commands/ExportCommand.cs
+++ b/src/Aspire.Cli/Commands/ExportCommand.cs
@@ -41,6 +41,11 @@ internal sealed class ExportCommand : BaseCommand
private static readonly Option s_dashboardUrlOption = TelemetryCommandHelpers.CreateDashboardUrlOption();
private static readonly Option s_apiKeyOption = TelemetryCommandHelpers.CreateApiKeyOption();
+ private static readonly Option s_includeHiddenOption = new("--include-hidden")
+ {
+ Description = ExportCommandStrings.IncludeHiddenOptionDescription
+ };
+
private static readonly Argument s_resourceArgument = new("resource")
{
Description = ExportCommandStrings.ResourceOptionDescription,
@@ -70,6 +75,7 @@ public ExportCommand(
Options.Add(s_outputOption);
Options.Add(s_dashboardUrlOption);
Options.Add(s_apiKeyOption);
+ Options.Add(s_includeHiddenOption);
}
protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
@@ -81,6 +87,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell
var outputPath = parseResult.GetValue(s_outputOption);
var dashboardUrl = parseResult.GetValue(s_dashboardUrlOption);
var apiKey = parseResult.GetValue(s_apiKeyOption);
+ var includeHidden = parseResult.GetValue(s_includeHiddenOption);
// Validate mutual exclusivity of --apphost and --dashboard-url
if (passedAppHostProjectFile is not null && dashboardUrl is not null)
@@ -118,7 +125,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell
try
{
- return await ExportDataAsync(resourceName, dashboardApi.Connection, dashboardApi.BaseUrl, dashboardApi.ApiToken, outputPath, cancellationToken);
+ return await ExportDataAsync(resourceName, includeHidden, dashboardApi.Connection, dashboardApi.BaseUrl, dashboardApi.ApiToken, outputPath, cancellationToken);
}
catch (HttpRequestException ex) when (dashboardUrl is not null)
{
@@ -137,6 +144,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell
private async Task ExportDataAsync(
string? resourceName,
+ bool includeHidden,
IAppHostAuxiliaryBackchannel? connection,
string? baseUrl,
string? apiToken,
@@ -147,18 +155,23 @@ private async Task ExportDataAsync(
using var client = isDashboardAvailable ? TelemetryCommandHelpers.CreateApiClient(_httpClientFactory, apiToken!) : null;
+ // Always fetch all snapshots so we know which resources are hidden.
+
// Get telemetry resources and resource snapshots
- var (telemetryResources, snapshots) = await _interactionService.ShowStatusAsync(ExportCommandStrings.GatheringResources, async () =>
+ var (telemetryResources, allSnapshots) = await _interactionService.ShowStatusAsync(ExportCommandStrings.GatheringResources, async () =>
{
var resources = isDashboardAvailable
? await TelemetryCommandHelpers.GetAllResourcesAsync(client!, baseUrl!, cancellationToken).ConfigureAwait(false)
: [];
IReadOnlyList snaps = connection is not null
- ? await connection.GetResourceSnapshotsAsync(cancellationToken).ConfigureAwait(false)
+ ? await connection.GetResourceSnapshotsAsync(includeHidden: true, cancellationToken).ConfigureAwait(false)
: [];
return (resources, snaps);
});
+ // Filter hidden resources, deriving the visible list and the hidden set for log filtering.
+ var (_, snapshots, hiddenResourceNames) = ResourceSnapshotMapper.FilterHiddenResources(allSnapshots, includeHidden, resourceName);
+
// Validate resource name exists (match by Name or DisplayName since users may pass either)
if (resourceName is not null && snapshots.Count > 0)
{
@@ -197,7 +210,7 @@ private async Task ExportDataAsync(
{
await _interactionService.ShowStatusAsync(ExportCommandStrings.GatheringConsoleLogs, async () =>
{
- await AddConsoleLogsAsync(exportArchive, connection, resourceName, snapshots, cancellationToken).ConfigureAwait(false);
+ await AddConsoleLogsAsync(exportArchive, connection, resourceName, snapshots, hiddenResourceNames, cancellationToken).ConfigureAwait(false);
return true;
});
}
@@ -250,12 +263,18 @@ private static async Task AddConsoleLogsAsync(
IAppHostAuxiliaryBackchannel connection,
string? resourceName,
IReadOnlyList snapshots,
+ HashSet hiddenResourceNames,
CancellationToken cancellationToken)
{
var logLinesByResource = new Dictionary>();
await foreach (var logLine in connection.GetResourceLogsAsync(resourceName, follow: false, cancellationToken).ConfigureAwait(false))
{
+ // When exporting all resources, skip logs from hidden resources
+ if (resourceName is null && hiddenResourceNames.Contains(logLine.ResourceName))
+ {
+ continue;
+ }
if (!logLinesByResource.TryGetValue(logLine.ResourceName, out var lines))
{
lines = [];
diff --git a/src/Aspire.Cli/Commands/LogsCommand.cs b/src/Aspire.Cli/Commands/LogsCommand.cs
index 0c88ab4e875..7bc248fc010 100644
--- a/src/Aspire.Cli/Commands/LogsCommand.cs
+++ b/src/Aspire.Cli/Commands/LogsCommand.cs
@@ -100,6 +100,10 @@ internal sealed class LogsCommand : BaseCommand
{
Description = LogsCommandStrings.TimestampsOptionDescription
};
+ private static readonly Option s_includeHiddenOption = new("--include-hidden")
+ {
+ Description = LogsCommandStrings.IncludeHiddenOptionDescription
+ };
private readonly ResourceColorMap _resourceColorMap;
@@ -125,6 +129,7 @@ public LogsCommand(
Options.Add(s_formatOption);
Options.Add(s_tailOption);
Options.Add(s_timestampsOption);
+ Options.Add(s_includeHiddenOption);
}
protected override async Task ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
@@ -137,6 +142,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell
var format = parseResult.GetValue(s_formatOption);
var tail = parseResult.GetValue(s_tailOption);
var timestamps = parseResult.GetValue(s_timestampsOption);
+ var includeHidden = parseResult.GetValue(s_includeHiddenOption);
// Validate --tail value
if (tail.HasValue && tail.Value < 1)
@@ -160,18 +166,19 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell
}
var connection = result.Connection!;
-
- // Fetch snapshots for resource name resolution
- var snapshots = await connection.GetResourceSnapshotsAsync(cancellationToken).ConfigureAwait(false);
+ var effectiveIncludeHidden = includeHidden || resourceName is not null;
+ using var resourceWatcher = new ResourceSnapshotWatcher(connection, effectiveIncludeHidden);
+ await resourceWatcher.WaitForInitialLoadAsync(cancellationToken).ConfigureAwait(false);
// Pre-resolve colors for all resource names so that assignment is
// deterministic regardless of which resources are displayed.
- _resourceColorMap.ResolveAll(snapshots.Select(s => ResourceSnapshotMapper.GetResourceName(s, snapshots)));
+ var allSnapshots = resourceWatcher.GetAllResources();
+ _resourceColorMap.ResolveAll(allSnapshots.Select(s => ResourceSnapshotMapper.GetResourceName(s, allSnapshots)));
// Validate resource name exists (match by Name or DisplayName since users may pass either)
if (resourceName is not null)
{
- if (!ResourceSnapshotMapper.WhereMatchesResourceName(snapshots, resourceName).Any())
+ if (!ResourceSnapshotMapper.WhereMatchesResourceName(resourceWatcher.GetAllResources(), resourceName).Any())
{
_interactionService.DisplayError(string.Format(CultureInfo.CurrentCulture, LogsCommandStrings.ResourceNotFound, resourceName));
return ExitCodeConstants.InvalidCommand;
@@ -179,7 +186,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell
}
else
{
- if (snapshots.Count == 0)
+ if (!resourceWatcher.GetResources().Any())
{
_interactionService.DisplayMessage(KnownEmojis.Information, LogsCommandStrings.NoResourcesFound);
return ExitCodeConstants.Success;
@@ -188,27 +195,27 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell
if (follow)
{
- return await ExecuteWatchAsync(connection, resourceName, format, tail, timestamps, snapshots, cancellationToken);
+ return await ExecuteWatchAsync(connection, resourceWatcher, resourceName, format, tail, timestamps, cancellationToken);
}
else
{
- return await ExecuteGetAsync(connection, resourceName, format, tail, timestamps, snapshots, cancellationToken);
+ return await ExecuteGetAsync(connection, resourceWatcher, resourceName, format, tail, timestamps, cancellationToken);
}
}
private async Task ExecuteGetAsync(
IAppHostAuxiliaryBackchannel connection,
+ ResourceSnapshotWatcher resourceWatcher,
string? resourceName,
OutputFormat format,
int? tail,
bool timestamps,
- IReadOnlyList snapshots,
CancellationToken cancellationToken)
{
// Collect all logs, parsing into LogEntry with resolved resource names sorted by timestamp
var entries = await _interactionService.ShowStatusAsync(
LogsCommandStrings.GettingLogs,
- async () => await CollectLogsAsync(connection, resourceName, snapshots, cancellationToken).ConfigureAwait(false));
+ async () => await CollectLogsAsync(connection, resourceWatcher, resourceName, cancellationToken).ConfigureAwait(false));
// Apply tail filter (tail.Value is guaranteed >= 1 by earlier validation)
if (tail.HasValue && entries.Count > tail.Value)
@@ -247,11 +254,11 @@ private async Task ExecuteGetAsync(
private async Task ExecuteWatchAsync(
IAppHostAuxiliaryBackchannel connection,
+ ResourceSnapshotWatcher resourceWatcher,
string? resourceName,
OutputFormat format,
int? tail,
bool timestamps,
- IReadOnlyList snapshots,
CancellationToken cancellationToken)
{
var logParser = new LogParser(ConsoleColor.Black);
@@ -261,7 +268,7 @@ private async Task ExecuteWatchAsync(
{
var entries = await _interactionService.ShowStatusAsync(
LogsCommandStrings.GettingLogs,
- async () => await CollectLogsAsync(connection, resourceName, snapshots, cancellationToken).ConfigureAwait(false));
+ async () => await CollectLogsAsync(connection, resourceWatcher, resourceName, cancellationToken).ConfigureAwait(false));
// Output last N lines
var tailedEntries = entries.Count > tail.Value
@@ -277,7 +284,19 @@ private async Task ExecuteWatchAsync(
// Now stream new logs
await foreach (var logLine in connection.GetResourceLogsAsync(resourceName, follow: true, cancellationToken).ConfigureAwait(false))
{
- var entry = ParseLogLine(logLine, logParser, snapshots);
+ // When streaming all resources, skip logs from hidden resources.
+ // We filter by exclusion so that new resources appearing after the
+ // initial snapshot are included by default.
+ if (resourceName is null && !resourceWatcher.IncludeHidden)
+ {
+ var resource = resourceWatcher.GetResource(logLine.ResourceName);
+ if (resource is not null && ResourceSnapshotMapper.IsHiddenResource(resource))
+ {
+ continue;
+ }
+ }
+
+ var entry = ParseLogLine(logLine, logParser, resourceWatcher.GetAllResources());
OutputLogLine(entry, format, timestamps);
}
@@ -291,15 +310,27 @@ private async Task ExecuteWatchAsync(
///
private static async Task> CollectLogsAsync(
IAppHostAuxiliaryBackchannel connection,
+ ResourceSnapshotWatcher resourceWatcher,
string? resourceName,
- IReadOnlyList snapshots,
CancellationToken cancellationToken)
{
var logParser = new LogParser(ConsoleColor.Black);
var logEntries = new LogEntries(int.MaxValue) { BaseLineNumber = 1 };
+ // Snapshot the resource list once for the non-follow path since it doesn't change.
+ var allSnapshots = resourceWatcher.GetAllResources().ToList();
await foreach (var logLine in connection.GetResourceLogsAsync(resourceName, follow: false, cancellationToken).ConfigureAwait(false))
{
- logEntries.InsertSorted(ParseLogLine(logLine, logParser, snapshots));
+ // When streaming all resources, skip logs from hidden resources
+ if (resourceName is null && !resourceWatcher.IncludeHidden)
+ {
+ var resource = resourceWatcher.GetResource(logLine.ResourceName);
+ if (resource is not null && ResourceSnapshotMapper.IsHiddenResource(resource))
+ {
+ continue;
+ }
+ }
+
+ logEntries.InsertSorted(ParseLogLine(logLine, logParser, allSnapshots));
}
return logEntries.GetEntries();
}
@@ -308,7 +339,7 @@ private static async Task> CollectLogsAsync(
/// Parses a into a with the resolved resource name
/// set on .
///
- private static LogEntry ParseLogLine(ResourceLogLine logLine, LogParser logParser, IReadOnlyList snapshots)
+ private static LogEntry ParseLogLine(ResourceLogLine logLine, LogParser logParser, IEnumerable snapshots)
{
var resolvedName = ResolveResourceName(logLine.ResourceName, snapshots);
return logParser.CreateLogEntry(logLine.Content, logLine.IsError, resolvedName);
@@ -348,7 +379,7 @@ private static string FormatTimestamp(DateTime timestamp)
return timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffK", CultureInfo.InvariantCulture);
}
- private static string ResolveResourceName(string resourceName, IReadOnlyList snapshots)
+ private static string ResolveResourceName(string resourceName, IEnumerable snapshots)
{
var snapshot = snapshots.FirstOrDefault(s => string.Equals(s.Name, resourceName, StringComparisons.ResourceName));
if (snapshot is not null)
diff --git a/src/Aspire.Cli/Commands/McpToolsCommand.cs b/src/Aspire.Cli/Commands/McpToolsCommand.cs
index 155fc2737cf..183a12b1286 100644
--- a/src/Aspire.Cli/Commands/McpToolsCommand.cs
+++ b/src/Aspire.Cli/Commands/McpToolsCommand.cs
@@ -67,7 +67,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell
}
var connection = result.Connection!;
- var snapshots = await connection.GetResourceSnapshotsAsync(cancellationToken);
+ var snapshots = await connection.GetResourceSnapshotsAsync(includeHidden: true, cancellationToken);
var resourcesWithTools = snapshots.Where(r => r.McpServer is not null).ToList();
if (resourcesWithTools.Count == 0)
diff --git a/src/Aspire.Cli/Commands/PsCommand.cs b/src/Aspire.Cli/Commands/PsCommand.cs
index f62fb8057cb..fac791217f0 100644
--- a/src/Aspire.Cli/Commands/PsCommand.cs
+++ b/src/Aspire.Cli/Commands/PsCommand.cs
@@ -175,7 +175,7 @@ private async Task> GatherAppHostInfosAsync(List r.McpServer is not null).ToList();
_logger.LogDebug("Resources with MCP tools received: {Count}", resourcesWithTools.Count);
diff --git a/src/Aspire.Cli/Mcp/Tools/ListResourcesTool.cs b/src/Aspire.Cli/Mcp/Tools/ListResourcesTool.cs
index 8ba6db60122..6020ffc9707 100644
--- a/src/Aspire.Cli/Mcp/Tools/ListResourcesTool.cs
+++ b/src/Aspire.Cli/Mcp/Tools/ListResourcesTool.cs
@@ -67,7 +67,7 @@ public override async ValueTask CallToolAsync(CallToolContext co
{
// Get dashboard URL and resource snapshots in parallel
var dashboardUrlsTask = connection.GetDashboardUrlsAsync(cancellationToken);
- var snapshotsTask = connection.GetResourceSnapshotsAsync(cancellationToken);
+ var snapshotsTask = connection.GetResourceSnapshotsAsync(includeHidden: true, cancellationToken);
await Task.WhenAll(dashboardUrlsTask, snapshotsTask).ConfigureAwait(false);
diff --git a/src/Aspire.Cli/Resources/DescribeCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/DescribeCommandStrings.Designer.cs
index 02fe13cb375..0e032be3849 100644
--- a/src/Aspire.Cli/Resources/DescribeCommandStrings.Designer.cs
+++ b/src/Aspire.Cli/Resources/DescribeCommandStrings.Designer.cs
@@ -111,6 +111,12 @@ public static string HeaderHealth {
}
}
+ public static string IncludeHiddenOptionDescription {
+ get {
+ return ResourceManager.GetString("IncludeHiddenOptionDescription", resourceCulture);
+ }
+ }
+
public static string HeaderURLs {
get {
return ResourceManager.GetString("HeaderURLs", resourceCulture);
diff --git a/src/Aspire.Cli/Resources/DescribeCommandStrings.resx b/src/Aspire.Cli/Resources/DescribeCommandStrings.resx
index 30a4a7c5afb..1245c5ff667 100644
--- a/src/Aspire.Cli/Resources/DescribeCommandStrings.resx
+++ b/src/Aspire.Cli/Resources/DescribeCommandStrings.resx
@@ -150,6 +150,9 @@
Health
+
+ Include hidden resources in the output
+
URLs
diff --git a/src/Aspire.Cli/Resources/ExportCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/ExportCommandStrings.Designer.cs
index edc8789627f..c75333e4cb9 100644
--- a/src/Aspire.Cli/Resources/ExportCommandStrings.Designer.cs
+++ b/src/Aspire.Cli/Resources/ExportCommandStrings.Designer.cs
@@ -104,6 +104,12 @@ internal static string GatheringConsoleLogs {
return ResourceManager.GetString("GatheringConsoleLogs", resourceCulture);
}
}
+
+ internal static string IncludeHiddenOptionDescription {
+ get {
+ return ResourceManager.GetString("IncludeHiddenOptionDescription", resourceCulture);
+ }
+ }
///
/// Looks up a localized string similar to Gathering resource data....
diff --git a/src/Aspire.Cli/Resources/ExportCommandStrings.resx b/src/Aspire.Cli/Resources/ExportCommandStrings.resx
index 1bfdb10611b..8bc473b0bb4 100644
--- a/src/Aspire.Cli/Resources/ExportCommandStrings.resx
+++ b/src/Aspire.Cli/Resources/ExportCommandStrings.resx
@@ -79,4 +79,7 @@
Dashboard is not available. Telemetry data (structured logs, traces) will not be included in the export.
+
+ Include hidden resources in the output
+
diff --git a/src/Aspire.Cli/Resources/LogsCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/LogsCommandStrings.Designer.cs
index 71f61d4cd1c..5545be5d208 100644
--- a/src/Aspire.Cli/Resources/LogsCommandStrings.Designer.cs
+++ b/src/Aspire.Cli/Resources/LogsCommandStrings.Designer.cs
@@ -105,6 +105,12 @@ public static string TimestampsOptionDescription {
}
}
+ public static string IncludeHiddenOptionDescription {
+ get {
+ return ResourceManager.GetString("IncludeHiddenOptionDescription", resourceCulture);
+ }
+ }
+
public static string SelectAppHostAction {
get {
return ResourceManager.GetString("SelectAppHostAction", resourceCulture);
diff --git a/src/Aspire.Cli/Resources/LogsCommandStrings.resx b/src/Aspire.Cli/Resources/LogsCommandStrings.resx
index a0679a756b2..868e505ee3a 100644
--- a/src/Aspire.Cli/Resources/LogsCommandStrings.resx
+++ b/src/Aspire.Cli/Resources/LogsCommandStrings.resx
@@ -150,6 +150,9 @@
Show timestamps for each log line
+
+ Include hidden resources in the output
+
stream logs from
diff --git a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.cs.xlf
index d606717c04c..473811b70de 100644
--- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.cs.xlf
+++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.cs.xlf
@@ -1,4 +1,4 @@
-
+
@@ -37,6 +37,11 @@
URLs
+
+ 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 d0ec199f29b..b5bca772062 100644
--- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.de.xlf
+++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.de.xlf
@@ -1,4 +1,4 @@
-
+
@@ -37,6 +37,11 @@
URLs
+
+ 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 babaf1b13e9..de999fdd55c 100644
--- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.es.xlf
+++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.es.xlf
@@ -1,4 +1,4 @@
-
+
@@ -37,6 +37,11 @@
URLs
+
+ 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 6f71980e160..91ac2620785 100644
--- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.fr.xlf
+++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.fr.xlf
@@ -1,4 +1,4 @@
-
+
@@ -37,6 +37,11 @@
URLs
+
+ 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 627f1054130..3a71576e61f 100644
--- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.it.xlf
+++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.it.xlf
@@ -1,4 +1,4 @@
-
+
@@ -37,6 +37,11 @@
URLs
+
+ 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 4b8b08498ef..642585f44db 100644
--- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ja.xlf
+++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ja.xlf
@@ -1,4 +1,4 @@
-
+
@@ -37,6 +37,11 @@
URLs
+
+ 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.ko.xlf b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ko.xlf
index 7d29cca2696..8e1d8607359 100644
--- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ko.xlf
+++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ko.xlf
@@ -1,4 +1,4 @@
-
+
@@ -37,6 +37,11 @@
URLs
+
+ 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 3560eeefbd2..b0da1b7fab2 100644
--- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.pl.xlf
+++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.pl.xlf
@@ -1,4 +1,4 @@
-
+
@@ -37,6 +37,11 @@
URLs
+
+ 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 106058f3a25..0fb81399431 100644
--- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.pt-BR.xlf
+++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.pt-BR.xlf
@@ -1,4 +1,4 @@
-
+
@@ -37,6 +37,11 @@
URLs
+
+ 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 e62e6f1d9b7..87d309257a2 100644
--- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ru.xlf
+++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.ru.xlf
@@ -1,4 +1,4 @@
-
+
@@ -37,6 +37,11 @@
URLs
+
+ 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 e9b7b4312fc..b900d3b9c73 100644
--- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.tr.xlf
+++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.tr.xlf
@@ -1,4 +1,4 @@
-
+
@@ -37,6 +37,11 @@
URLs
+
+ 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 c616459aa14..0c93d956d61 100644
--- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.zh-Hans.xlf
+++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.zh-Hans.xlf
@@ -1,4 +1,4 @@
-
+
@@ -37,6 +37,11 @@
URLs
+
+ 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 cd09b7acec2..be0deb048db 100644
--- a/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.zh-Hant.xlf
+++ b/src/Aspire.Cli/Resources/xlf/DescribeCommandStrings.zh-Hant.xlf
@@ -1,4 +1,4 @@
-
+
@@ -37,6 +37,11 @@
URLs
+
+ 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/ExportCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.cs.xlf
index 0b91bf80612..20100960002 100644
--- a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.cs.xlf
+++ b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.cs.xlf
@@ -42,6 +42,11 @@
Gathering traces...
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
No resources found.
No resources found.
diff --git a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.de.xlf
index 8cece099933..e2c99ceacfb 100644
--- a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.de.xlf
+++ b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.de.xlf
@@ -42,6 +42,11 @@
Gathering traces...
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
No resources found.
No resources found.
diff --git a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.es.xlf
index 8a4f7767f8d..f94008ec935 100644
--- a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.es.xlf
+++ b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.es.xlf
@@ -42,6 +42,11 @@
Gathering traces...
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
No resources found.
No resources found.
diff --git a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.fr.xlf
index e9e79002dc4..17a5ce3d34d 100644
--- a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.fr.xlf
+++ b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.fr.xlf
@@ -42,6 +42,11 @@
Gathering traces...
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
No resources found.
No resources found.
diff --git a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.it.xlf
index eff9f3b80b1..1eb8816ee48 100644
--- a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.it.xlf
+++ b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.it.xlf
@@ -42,6 +42,11 @@
Gathering traces...
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
No resources found.
No resources found.
diff --git a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.ja.xlf
index c298d54c926..995b3a572da 100644
--- a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.ja.xlf
+++ b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.ja.xlf
@@ -42,6 +42,11 @@
Gathering traces...
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
No resources found.
No resources found.
diff --git a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.ko.xlf
index be205ceb44c..bcd751026b5 100644
--- a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.ko.xlf
+++ b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.ko.xlf
@@ -42,6 +42,11 @@
Gathering traces...
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
No resources found.
No resources found.
diff --git a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.pl.xlf
index e4e3cba4b23..e9f9dd72ba5 100644
--- a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.pl.xlf
+++ b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.pl.xlf
@@ -42,6 +42,11 @@
Gathering traces...
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
No resources found.
No resources found.
diff --git a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.pt-BR.xlf
index f83b1c3a875..25b095e5b1b 100644
--- a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.pt-BR.xlf
+++ b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.pt-BR.xlf
@@ -42,6 +42,11 @@
Gathering traces...
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
No resources found.
No resources found.
diff --git a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.ru.xlf
index 75aa6657137..ac8593e0252 100644
--- a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.ru.xlf
+++ b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.ru.xlf
@@ -42,6 +42,11 @@
Gathering traces...
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
No resources found.
No resources found.
diff --git a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.tr.xlf
index aae59954879..e68babc6592 100644
--- a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.tr.xlf
+++ b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.tr.xlf
@@ -42,6 +42,11 @@
Gathering traces...
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
No resources found.
No resources found.
diff --git a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.zh-Hans.xlf
index 625dbb92a14..aded62c5242 100644
--- a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.zh-Hans.xlf
+++ b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.zh-Hans.xlf
@@ -42,6 +42,11 @@
Gathering traces...
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
No resources found.
No resources found.
diff --git a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.zh-Hant.xlf
index 9ea5ddcf333..87f8bec99ef 100644
--- a/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.zh-Hant.xlf
+++ b/src/Aspire.Cli/Resources/xlf/ExportCommandStrings.zh-Hant.xlf
@@ -42,6 +42,11 @@
Gathering traces...
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
No resources found.
No resources found.
diff --git a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.cs.xlf
index 53364acd8e2..fe59dee289d 100644
--- a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.cs.xlf
+++ b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.cs.xlf
@@ -12,6 +12,11 @@
Streamujte protokoly v reálném čase při jejich zápisu.
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
Output format (Table or Json)
Výstupní protokoly ve formátu JSON (NDJSON).
diff --git a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.de.xlf
index 928705e5577..b26e592315d 100644
--- a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.de.xlf
+++ b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.de.xlf
@@ -12,6 +12,11 @@
Streamen Sie Protokolle in Echtzeit, während sie geschrieben werden.
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
Output format (Table or Json)
Protokolle im JSON-Format (NDJSON) ausgeben.
diff --git a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.es.xlf
index cbb8d385020..9a120cdbac7 100644
--- a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.es.xlf
+++ b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.es.xlf
@@ -12,6 +12,11 @@
Transmita los registros en tiempo real a medida que se escriben.
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
Output format (Table or Json)
Registros de salida en formato JSON (NDJSON).
diff --git a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.fr.xlf
index 2d48ce14aa1..c87fa0ee82d 100644
--- a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.fr.xlf
+++ b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.fr.xlf
@@ -12,6 +12,11 @@
Diffuser les journaux en temps réel au fur et à mesure de leur écriture.
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
Output format (Table or Json)
Générer les journaux au format JSON (NDJSON).
diff --git a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.it.xlf
index 275390951dd..38b8a05e55b 100644
--- a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.it.xlf
+++ b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.it.xlf
@@ -12,6 +12,11 @@
Consente di trasmettere i log in tempo reale man mano che vengono scritti.
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
Output format (Table or Json)
Log di output in formato JSON (NDJSON).
diff --git a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.ja.xlf
index fd45f8c7cba..b7bd1e1879e 100644
--- a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.ja.xlf
+++ b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.ja.xlf
@@ -12,6 +12,11 @@
ログが書き込まれると同時にリアルタイムでストリームします。
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
Output format (Table or Json)
ログを JSON 形式 (NDJSON) で出力します。
diff --git a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.ko.xlf
index fae2373b452..90b8ff0e5c9 100644
--- a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.ko.xlf
+++ b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.ko.xlf
@@ -12,6 +12,11 @@
로그가 기록되는 대로 실시간으로 스트리밍합니다.
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
Output format (Table or Json)
JSON 형식(NDJSON)으로 로그를 출력합니다.
diff --git a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.pl.xlf
index 73b8c623514..1686f0aabae 100644
--- a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.pl.xlf
+++ b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.pl.xlf
@@ -12,6 +12,11 @@
Przesyłaj dzienniki w czasie rzeczywistym podczas ich zapisywania.
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
Output format (Table or Json)
Dzienniki wyjściowe w formacie JSON (NDJSON).
diff --git a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.pt-BR.xlf
index ee2a5fad800..1ff2a342cde 100644
--- a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.pt-BR.xlf
+++ b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.pt-BR.xlf
@@ -12,6 +12,11 @@
Transmita logs em tempo real conforme eles são gravados.
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
Output format (Table or Json)
Logs de saída no formato JSON (NDJSON).
diff --git a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.ru.xlf
index 7fd22a96d9c..6965bf5acf3 100644
--- a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.ru.xlf
+++ b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.ru.xlf
@@ -12,6 +12,11 @@
Потоковая передача журналов в реальном времени по мере их записи.
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
Output format (Table or Json)
Вывод журналов в формате JSON (NDJSON).
diff --git a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.tr.xlf
index 6d53beea746..0b9d7fc990a 100644
--- a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.tr.xlf
+++ b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.tr.xlf
@@ -12,6 +12,11 @@
Günlükleri yazıldıkça gerçek zamanlı olarak akışla aktar.
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
Output format (Table or Json)
Günlükleri JSON biçiminde (NDJSON) çıkar.
diff --git a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.zh-Hans.xlf
index b23d787fe3b..c1affe419f4 100644
--- a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.zh-Hans.xlf
+++ b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.zh-Hans.xlf
@@ -12,6 +12,11 @@
在写入日志时实时流式传输这些日志。
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
Output format (Table or Json)
以 JSON 格式输出日志(NDJSON)。
diff --git a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.zh-Hant.xlf
index 4d5bbaf40a8..920af8a64fe 100644
--- a/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.zh-Hant.xlf
+++ b/src/Aspire.Cli/Resources/xlf/LogsCommandStrings.zh-Hant.xlf
@@ -12,6 +12,11 @@
在寫入記錄時即時串流這些記錄。
+
+ Include hidden resources in the output
+ Include hidden resources in the output
+
+
Output format (Table or Json)
輸出記錄採用 JSON 格式 (NDJSON)。
diff --git a/src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs b/src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs
index 082abb7252c..e0fa2747212 100644
--- a/src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs
+++ b/src/Aspire.Hosting/Backchannel/AuxiliaryBackchannelRpcTarget.cs
@@ -425,12 +425,6 @@ 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);
@@ -465,12 +459,6 @@ 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)
{
@@ -605,6 +593,7 @@ public async IAsyncEnumerable WatchResourceSnapshotsAsync([Enu
ResourceType = snapshot.ResourceType,
State = snapshot.State?.Text,
StateStyle = snapshot.State?.Style,
+ IsHidden = snapshot.IsHidden,
HealthStatus = snapshot.HealthStatus?.ToString(),
ExitCode = snapshot.ExitCode,
CreatedAt = snapshot.CreationTimeStamp,
@@ -654,12 +643,6 @@ public async IAsyncEnumerable GetResourceLogsAsync(
{
foreach (var resource in appModel.Resources)
{
- // Skip the dashboard
- if (string.Equals(resource.Name, KnownResourceNames.AspireDashboard, StringComparisons.ResourceName))
- {
- continue;
- }
-
resourcesToLog.AddRange(resource.GetResolvedResourceNames());
}
}
diff --git a/src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs b/src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs
index 1f6d4f1e7d2..43cf58efcd5 100644
--- a/src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs
+++ b/src/Aspire.Hosting/Backchannel/BackchannelDataTypes.cs
@@ -857,6 +857,11 @@ public string? Type
///
public Dictionary Properties { get; init; } = [];
+ ///
+ /// Gets a value indicating whether this resource is hidden.
+ ///
+ public bool IsHidden { get; init; }
+
///
/// Gets the MCP server information if the resource exposes an MCP endpoint.
///
diff --git a/src/Aspire/Cli/Commands/LogsCommand.cs b/src/Aspire/Cli/Commands/LogsCommand.cs
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/Aspire.Cli.Tests/Commands/DescribeCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/DescribeCommandTests.cs
index 58c0a7eb120..8217851017b 100644
--- a/tests/Aspire.Cli.Tests/Commands/DescribeCommandTests.cs
+++ b/tests/Aspire.Cli.Tests/Commands/DescribeCommandTests.cs
@@ -390,6 +390,139 @@ public async Task DescribeCommand_Follow_JsonFormat_StripsLoginPathFromDashboard
Assert.Equal("http://localhost:18888/?resource=redis", resource.DashboardUrl);
}
+ [Fact]
+ public async Task DescribeCommand_HiddenResources_AreExcludedByDefault()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var outputWriter = new TestOutputTextWriter(outputHelper);
+ var provider = CreateDescribeTestServices(workspace, outputWriter, [
+ new ResourceSnapshot { Name = "redis", DisplayName = "redis", ResourceType = "Container", State = "Running" },
+ new ResourceSnapshot { Name = "aspire-dashboard", DisplayName = "aspire-dashboard", ResourceType = "Executable", State = "Hidden" },
+ new ResourceSnapshot { Name = "hidden-svc", DisplayName = "hidden-svc", ResourceType = "Project", 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("redis", deserialized.Resources[0].Name);
+ }
+
+ [Fact]
+ public async Task DescribeCommand_IncludeHidden_ShowsHiddenResources()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var outputWriter = new TestOutputTextWriter(outputHelper);
+ var provider = CreateDescribeTestServices(workspace, outputWriter, [
+ new ResourceSnapshot { Name = "redis", DisplayName = "redis", ResourceType = "Container", State = "Running" },
+ new ResourceSnapshot { Name = "aspire-dashboard", DisplayName = "aspire-dashboard", ResourceType = "Executable", State = "Hidden" },
+ new ResourceSnapshot { Name = "hidden-svc", DisplayName = "hidden-svc", ResourceType = "Project", 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(3, deserialized.Resources.Length);
+ Assert.Contains(deserialized.Resources, r => r.Name == "redis");
+ Assert.Contains(deserialized.Resources, r => r.Name == "aspire-dashboard");
+ Assert.Contains(deserialized.Resources, r => r.Name == "hidden-svc");
+ }
+
+ [Fact]
+ public async Task DescribeCommand_SpecificResource_IncludesHiddenWithoutFlag()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var outputWriter = new TestOutputTextWriter(outputHelper);
+ var provider = CreateDescribeTestServices(workspace, outputWriter, [
+ new ResourceSnapshot { Name = "redis", DisplayName = "redis", ResourceType = "Container", State = "Running" },
+ new ResourceSnapshot { Name = "aspire-dashboard", DisplayName = "aspire-dashboard", ResourceType = "Executable", State = "Hidden" },
+ ]);
+
+ var command = provider.GetRequiredService();
+ var result = command.Parse("describe aspire-dashboard --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("aspire-dashboard", deserialized.Resources[0].Name);
+ }
+
+ [Fact]
+ public async Task DescribeCommand_Follow_HiddenResources_AreExcludedByDefault()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var outputWriter = new TestOutputTextWriter(outputHelper);
+ var provider = CreateDescribeTestServices(workspace, outputWriter, [
+ new ResourceSnapshot { Name = "redis", DisplayName = "redis", ResourceType = "Container", State = "Running" },
+ new ResourceSnapshot { Name = "aspire-dashboard", DisplayName = "aspire-dashboard", ResourceType = "Executable", State = "Hidden" },
+ ]);
+
+ var command = provider.GetRequiredService();
+ var result = command.Parse("describe --follow --format json");
+
+ var exitCode = await result.InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(ExitCodeConstants.Success, exitCode);
+
+ var jsonLines = outputWriter.Logs
+ .Where(l => l.TrimStart().StartsWith("{", StringComparison.Ordinal))
+ .ToList();
+
+ Assert.Single(jsonLines);
+
+ var resource = JsonSerializer.Deserialize(jsonLines[0], ResourcesCommandJsonContext.Ndjson.ResourceJson);
+ Assert.NotNull(resource);
+ Assert.Equal("redis", resource.Name);
+ }
+
+ [Fact]
+ public async Task DescribeCommand_Follow_IncludeHidden_ShowsHiddenResources()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var outputWriter = new TestOutputTextWriter(outputHelper);
+ var provider = CreateDescribeTestServices(workspace, outputWriter, [
+ new ResourceSnapshot { Name = "redis", DisplayName = "redis", ResourceType = "Container", State = "Running" },
+ new ResourceSnapshot { Name = "aspire-dashboard", DisplayName = "aspire-dashboard", ResourceType = "Executable", State = "Hidden" },
+ ]);
+
+ var command = provider.GetRequiredService();
+ var result = command.Parse("describe --follow --format json --include-hidden");
+
+ var exitCode = await result.InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(ExitCodeConstants.Success, exitCode);
+
+ var jsonLines = outputWriter.Logs
+ .Where(l => l.TrimStart().StartsWith("{", StringComparison.Ordinal))
+ .ToList();
+
+ Assert.Equal(2, jsonLines.Count);
+ Assert.Contains(jsonLines, l => l.Contains("redis"));
+ Assert.Contains(jsonLines, l => l.Contains("aspire-dashboard"));
+ }
+
private ServiceProvider CreateDescribeTestServices(
TemporaryWorkspace workspace,
TestOutputTextWriter outputWriter,
diff --git a/tests/Aspire.Cli.Tests/Commands/ExportCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/ExportCommandTests.cs
index 31a28616245..4e257638a9f 100644
--- a/tests/Aspire.Cli.Tests/Commands/ExportCommandTests.cs
+++ b/tests/Aspire.Cli.Tests/Commands/ExportCommandTests.cs
@@ -597,6 +597,136 @@ public async Task ExportCommand_ResourceFilter_ReplicasByDisplayName_ExportsAllR
entry => Assert.Equal("traces/apiservice-def.json", entry));
}
+ [Fact]
+ public async Task ExportCommand_HiddenResources_AreExcludedByDefault()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var outputPath = Path.Combine(workspace.WorkspaceRoot.FullName, "export.zip");
+
+ var provider = CreateExportTestServices(workspace,
+ resources: [],
+ telemetryEndpoints: new Dictionary
+ {
+ ["/api/telemetry/logs"] = BuildLogsJson(),
+ ["/api/telemetry/traces"] = BuildTracesJson(),
+ },
+ resourceSnapshots:
+ [
+ new ResourceSnapshot { Name = "redis", DisplayName = "redis", ResourceType = "Container", State = "Running" },
+ new ResourceSnapshot { Name = "aspire-dashboard", DisplayName = "aspire-dashboard", ResourceType = "Executable", State = "Hidden" },
+ new ResourceSnapshot { Name = "hidden-svc", DisplayName = "hidden-svc", ResourceType = "Project", State = "Running", IsHidden = true },
+ ],
+ logLines:
+ [
+ new ResourceLogLine { ResourceName = "redis", LineNumber = 1, Content = "Redis ready" },
+ new ResourceLogLine { ResourceName = "aspire-dashboard", LineNumber = 1, Content = "Dashboard started" },
+ new ResourceLogLine { ResourceName = "hidden-svc", LineNumber = 1, Content = "Hidden service log" },
+ ],
+ dashboardAvailable: false);
+
+ var command = provider.GetRequiredService();
+ var result = command.Parse($"export --output {outputPath}");
+
+ var exitCode = await result.InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(ExitCodeConstants.Success, exitCode);
+ Assert.True(File.Exists(outputPath));
+
+ using var archive = ZipFile.OpenRead(outputPath);
+ var entryNames = archive.Entries.Select(e => e.FullName).OrderBy(n => n).ToList();
+
+ // Only redis should be present; hidden resources should be excluded
+ Assert.Collection(entryNames,
+ entry => Assert.Equal("consolelogs/redis.txt", entry),
+ entry => Assert.Equal("resources/redis.json", entry));
+ }
+
+ [Fact]
+ public async Task ExportCommand_IncludeHidden_ShowsHiddenResources()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var outputPath = Path.Combine(workspace.WorkspaceRoot.FullName, "export.zip");
+
+ var provider = CreateExportTestServices(workspace,
+ resources: [],
+ telemetryEndpoints: new Dictionary
+ {
+ ["/api/telemetry/logs"] = BuildLogsJson(),
+ ["/api/telemetry/traces"] = BuildTracesJson(),
+ },
+ resourceSnapshots:
+ [
+ new ResourceSnapshot { Name = "redis", DisplayName = "redis", ResourceType = "Container", State = "Running" },
+ new ResourceSnapshot { Name = "aspire-dashboard", DisplayName = "aspire-dashboard", ResourceType = "Executable", State = "Hidden" },
+ ],
+ logLines:
+ [
+ new ResourceLogLine { ResourceName = "redis", LineNumber = 1, Content = "Redis ready" },
+ new ResourceLogLine { ResourceName = "aspire-dashboard", LineNumber = 1, Content = "Dashboard started" },
+ ],
+ dashboardAvailable: false);
+
+ var command = provider.GetRequiredService();
+ var result = command.Parse($"export --include-hidden --output {outputPath}");
+
+ var exitCode = await result.InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(ExitCodeConstants.Success, exitCode);
+ Assert.True(File.Exists(outputPath));
+
+ using var archive = ZipFile.OpenRead(outputPath);
+ var entryNames = archive.Entries.Select(e => e.FullName).OrderBy(n => n).ToList();
+
+ // Both resources should be present
+ Assert.Collection(entryNames,
+ entry => Assert.Equal("consolelogs/aspire-dashboard.txt", entry),
+ entry => Assert.Equal("consolelogs/redis.txt", entry),
+ entry => Assert.Equal("resources/aspire-dashboard.json", entry),
+ entry => Assert.Equal("resources/redis.json", entry));
+ }
+
+ [Fact]
+ public async Task ExportCommand_SpecificHiddenResource_WorksWithoutFlag()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var outputPath = Path.Combine(workspace.WorkspaceRoot.FullName, "export.zip");
+
+ var provider = CreateExportTestServices(workspace,
+ resources: [],
+ telemetryEndpoints: new Dictionary
+ {
+ ["/api/telemetry/logs"] = BuildLogsJson(),
+ ["/api/telemetry/traces"] = BuildTracesJson(),
+ },
+ resourceSnapshots:
+ [
+ new ResourceSnapshot { Name = "redis", DisplayName = "redis", ResourceType = "Container", State = "Running" },
+ new ResourceSnapshot { Name = "aspire-dashboard", DisplayName = "aspire-dashboard", ResourceType = "Executable", State = "Hidden" },
+ ],
+ logLines:
+ [
+ new ResourceLogLine { ResourceName = "redis", LineNumber = 1, Content = "Redis ready" },
+ new ResourceLogLine { ResourceName = "aspire-dashboard", LineNumber = 1, Content = "Dashboard started" },
+ ],
+ dashboardAvailable: false);
+
+ var command = provider.GetRequiredService();
+ var result = command.Parse($"export aspire-dashboard --output {outputPath}");
+
+ var exitCode = await result.InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(ExitCodeConstants.Success, exitCode);
+ Assert.True(File.Exists(outputPath));
+
+ using var archive = ZipFile.OpenRead(outputPath);
+ var entryNames = archive.Entries.Select(e => e.FullName).OrderBy(n => n).ToList();
+
+ // Only the specified hidden resource should be present
+ Assert.Collection(entryNames,
+ entry => Assert.Equal("consolelogs/aspire-dashboard.txt", entry),
+ entry => Assert.Equal("resources/aspire-dashboard.json", entry));
+ }
+
[Fact]
public async Task ExportCommand_ResourceFilter_NonExistentResource_ReturnsError()
{
diff --git a/tests/Aspire.Cli.Tests/Commands/LogsCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/LogsCommandTests.cs
index 3fb0fcfb59a..1fd3694e2ba 100644
--- a/tests/Aspire.Cli.Tests/Commands/LogsCommandTests.cs
+++ b/tests/Aspire.Cli.Tests/Commands/LogsCommandTests.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Runtime.CompilerServices;
using System.Text.Json;
using Aspire.Cli.Backchannel;
using Aspire.Cli.Commands;
@@ -568,6 +569,328 @@ public async Task LogsCommand_TextOutput_WithoutTimestamps_NoTimestampPrefix()
Assert.Equal("[apiservice-def456] Hello from replica 2", logLines[2]);
}
+ [Fact]
+ public async Task LogsCommand_HiddenResources_AreExcludedByDefault()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var outputWriter = new TestOutputTextWriter(outputHelper);
+ var provider = CreateLogsTestServicesWithHidden(workspace, outputWriter);
+
+ var command = provider.GetRequiredService();
+ var result = command.Parse("logs --format json");
+
+ var exitCode = await result.InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(ExitCodeConstants.Success, exitCode);
+
+ var jsonOutput = outputWriter.Logs.FirstOrDefault(l => l.Contains("\"logs\""));
+ Assert.NotNull(jsonOutput);
+
+ var logsOutput = JsonSerializer.Deserialize(jsonOutput, LogsCommandJsonContext.Snapshot.LogsOutput);
+ Assert.NotNull(logsOutput);
+
+ // Only visible resource logs should be present
+ Assert.All(logsOutput.Logs, l => Assert.Equal("redis", l.ResourceName));
+ Assert.DoesNotContain(logsOutput.Logs, l => l.ResourceName == "aspire-dashboard");
+ }
+
+ [Fact]
+ public async Task LogsCommand_IncludeHidden_ShowsHiddenResourceLogs()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var outputWriter = new TestOutputTextWriter(outputHelper);
+ var provider = CreateLogsTestServicesWithHidden(workspace, outputWriter);
+
+ var command = provider.GetRequiredService();
+ var result = command.Parse("logs --format json --include-hidden");
+
+ var exitCode = await result.InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(ExitCodeConstants.Success, exitCode);
+
+ var jsonOutput = outputWriter.Logs.FirstOrDefault(l => l.Contains("\"logs\""));
+ Assert.NotNull(jsonOutput);
+
+ var logsOutput = JsonSerializer.Deserialize(jsonOutput, LogsCommandJsonContext.Snapshot.LogsOutput);
+ Assert.NotNull(logsOutput);
+
+ // Both visible and hidden resource logs should be present
+ Assert.Contains(logsOutput.Logs, l => l.ResourceName == "redis");
+ Assert.Contains(logsOutput.Logs, l => l.ResourceName == "aspire-dashboard");
+ }
+
+ [Fact]
+ public async Task LogsCommand_SpecificHiddenResource_WorksWithoutFlag()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var outputWriter = new TestOutputTextWriter(outputHelper);
+ var provider = CreateLogsTestServicesWithHidden(workspace, outputWriter);
+
+ var command = provider.GetRequiredService();
+ var result = command.Parse("logs aspire-dashboard --format json");
+
+ var exitCode = await result.InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(ExitCodeConstants.Success, exitCode);
+
+ var jsonOutput = outputWriter.Logs.FirstOrDefault(l => l.Contains("\"logs\""));
+ Assert.NotNull(jsonOutput);
+
+ var logsOutput = JsonSerializer.Deserialize(jsonOutput, LogsCommandJsonContext.Snapshot.LogsOutput);
+ Assert.NotNull(logsOutput);
+
+ Assert.All(logsOutput.Logs, l => Assert.Equal("aspire-dashboard", l.ResourceName));
+ }
+
+ [Fact]
+ public async Task LogsCommand_NewResourceAfterInitialSnapshot_LogsAreIncluded()
+ {
+ // Verifies that logs from a resource not present in the initial snapshot
+ // (e.g. a resource that came online after streaming started) are still shown.
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var outputWriter = new TestOutputTextWriter(outputHelper);
+
+ var monitor = new TestAuxiliaryBackchannelMonitor();
+ var connection = new TestAppHostAuxiliaryBackchannel
+ {
+ IsInScope = true,
+ AppHostInfo = new AppHostInformation
+ {
+ AppHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "TestAppHost", "TestAppHost.csproj"),
+ ProcessId = 1234
+ },
+ ResourceSnapshots =
+ [
+ new ResourceSnapshot
+ {
+ Name = "redis",
+ DisplayName = "redis",
+ ResourceType = "Container",
+ State = "Running"
+ },
+ new ResourceSnapshot
+ {
+ Name = "aspire-dashboard",
+ DisplayName = "aspire-dashboard",
+ ResourceType = "Executable",
+ State = "Hidden"
+ }
+ ],
+ LogLines =
+ [
+ new ResourceLogLine
+ {
+ ResourceName = "redis",
+ LineNumber = 1,
+ Content = "2025-01-15T10:30:00Z Ready to accept connections",
+ IsError = false
+ },
+ new ResourceLogLine
+ {
+ ResourceName = "aspire-dashboard",
+ LineNumber = 1,
+ Content = "2025-01-15T10:30:01Z Dashboard started",
+ IsError = false
+ },
+ new ResourceLogLine
+ {
+ ResourceName = "webapi",
+ LineNumber = 1,
+ Content = "2025-01-15T10:30:02Z Webapi started",
+ IsError = false
+ }
+ ]
+ };
+ monitor.AddConnection("hash1", "socket.hash1", connection);
+
+ var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
+ {
+ options.AuxiliaryBackchannelMonitorFactory = _ => monitor;
+ options.OutputTextWriter = outputWriter;
+ options.DisableAnsi = false;
+ });
+
+ var provider = services.BuildServiceProvider();
+ var command = provider.GetRequiredService();
+ var result = command.Parse("logs --format json");
+
+ var exitCode = await result.InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(ExitCodeConstants.Success, exitCode);
+
+ var jsonOutput = outputWriter.Logs.FirstOrDefault(l => l.Contains("\"logs\""));
+ Assert.NotNull(jsonOutput);
+
+ var logsOutput = JsonSerializer.Deserialize(jsonOutput, LogsCommandJsonContext.Snapshot.LogsOutput);
+ Assert.NotNull(logsOutput);
+
+ // redis logs should be present (visible resource in initial snapshot)
+ Assert.Contains(logsOutput.Logs, l => l.ResourceName == "redis");
+ // webapi logs should be present even though it was not in the initial snapshot
+ Assert.Contains(logsOutput.Logs, l => l.ResourceName == "webapi");
+ // aspire-dashboard logs should still be excluded (hidden)
+ Assert.DoesNotContain(logsOutput.Logs, l => l.ResourceName == "aspire-dashboard");
+ }
+
+ [Fact]
+ public async Task LogsCommand_HiddenResourceAfterInitialSnapshot_IsExcludedInFollowMode()
+ {
+ using var workspace = TemporaryWorkspace.Create(outputHelper);
+ var outputWriter = new TestOutputTextWriter(outputHelper);
+
+ var monitor = new TestAuxiliaryBackchannelMonitor();
+ var snapshotsCallCount = 0;
+
+ var connection = new TestAppHostAuxiliaryBackchannel
+ {
+ IsInScope = true,
+ AppHostInfo = new AppHostInformation
+ {
+ AppHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "TestAppHost", "TestAppHost.csproj"),
+ ProcessId = 1234
+ },
+ ResourceSnapshots =
+ [
+ new ResourceSnapshot
+ {
+ Name = "redis",
+ DisplayName = "redis",
+ ResourceType = "Container",
+ State = "Running"
+ }
+ ],
+ LogLines =
+ [
+ new ResourceLogLine
+ {
+ ResourceName = "redis",
+ LineNumber = 1,
+ Content = "2025-01-15T10:30:00Z Ready to accept connections",
+ IsError = false
+ },
+ new ResourceLogLine
+ {
+ ResourceName = "late-hidden",
+ LineNumber = 1,
+ Content = "2025-01-15T10:30:01Z Hidden resource log",
+ IsError = false
+ }
+ ],
+ GetResourceSnapshotsHandler = _ =>
+ {
+ snapshotsCallCount++;
+ return Task.FromResult(snapshotsCallCount == 1
+ ? new List
+ {
+ new()
+ {
+ Name = "redis",
+ DisplayName = "redis",
+ ResourceType = "Container",
+ State = "Running"
+ }
+ }
+ : new List
+ {
+ new()
+ {
+ Name = "redis",
+ DisplayName = "redis",
+ ResourceType = "Container",
+ State = "Running"
+ },
+ new()
+ {
+ Name = "late-hidden",
+ DisplayName = "late-hidden",
+ ResourceType = "Executable",
+ State = "Hidden"
+ }
+ });
+ },
+ WatchResourceSnapshotsHandler = (_, cancellationToken) => WatchWithLateHidden(cancellationToken)
+ };
+ monitor.AddConnection("hash1", "socket.hash1", connection);
+
+ var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
+ {
+ options.AuxiliaryBackchannelMonitorFactory = _ => monitor;
+ options.OutputTextWriter = outputWriter;
+ options.DisableAnsi = true;
+ });
+
+ var provider = services.BuildServiceProvider();
+ var command = provider.GetRequiredService();
+ var result = command.Parse("logs --follow");
+
+ var exitCode = await result.InvokeAsync().DefaultTimeout();
+
+ Assert.Equal(ExitCodeConstants.Success, exitCode);
+ Assert.Contains(outputWriter.Logs, l => l.Contains("[redis] Ready to accept connections", StringComparison.Ordinal));
+ Assert.DoesNotContain(outputWriter.Logs, l => l.Contains("late-hidden", StringComparison.Ordinal));
+ }
+
+ private ServiceProvider CreateLogsTestServicesWithHidden(
+ TemporaryWorkspace workspace,
+ TestOutputTextWriter outputWriter,
+ bool disableAnsi = false)
+ {
+ var monitor = new TestAuxiliaryBackchannelMonitor();
+ var connection = new TestAppHostAuxiliaryBackchannel
+ {
+ IsInScope = true,
+ AppHostInfo = new AppHostInformation
+ {
+ AppHostPath = Path.Combine(workspace.WorkspaceRoot.FullName, "TestAppHost", "TestAppHost.csproj"),
+ ProcessId = 1234
+ },
+ ResourceSnapshots =
+ [
+ new ResourceSnapshot
+ {
+ Name = "redis",
+ DisplayName = "redis",
+ ResourceType = "Container",
+ State = "Running"
+ },
+ new ResourceSnapshot
+ {
+ Name = "aspire-dashboard",
+ DisplayName = "aspire-dashboard",
+ ResourceType = "Executable",
+ State = "Hidden"
+ }
+ ],
+ LogLines =
+ [
+ new ResourceLogLine
+ {
+ ResourceName = "redis",
+ LineNumber = 1,
+ Content = "2025-01-15T10:30:00Z Ready to accept connections",
+ IsError = false
+ },
+ new ResourceLogLine
+ {
+ ResourceName = "aspire-dashboard",
+ LineNumber = 1,
+ Content = "2025-01-15T10:30:01Z Dashboard started",
+ IsError = false
+ }
+ ]
+ };
+ monitor.AddConnection("hash1", "socket.hash1", connection);
+
+ var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options =>
+ {
+ options.AuxiliaryBackchannelMonitorFactory = _ => monitor;
+ options.OutputTextWriter = outputWriter;
+ options.DisableAnsi = disableAnsi;
+ });
+
+ return services.BuildServiceProvider();
+ }
+
private ServiceProvider CreateLogsTestServices(
TemporaryWorkspace workspace,
TestOutputTextWriter outputWriter,
@@ -651,4 +974,22 @@ private ServiceProvider CreateLogsTestServices(
return services.BuildServiceProvider();
}
+
+ private static async IAsyncEnumerable WatchWithLateHidden([EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ yield return new ResourceSnapshot
+ {
+ Name = "late-hidden",
+ DisplayName = "late-hidden",
+ ResourceType = "Executable",
+ State = "Hidden"
+ };
+
+ // Keep the enumerable alive until cancelled so the watcher stays running.
+ var tcs = new TaskCompletionSource();
+ await using (cancellationToken.Register(() => tcs.TrySetResult()))
+ {
+ await tcs.Task.ConfigureAwait(false);
+ }
+ }
}
diff --git a/tests/Aspire.Cli.Tests/TestServices/TestAppHostAuxiliaryBackchannel.cs b/tests/Aspire.Cli.Tests/TestServices/TestAppHostAuxiliaryBackchannel.cs
index 16d1eee74eb..58982867b2a 100644
--- a/tests/Aspire.Cli.Tests/TestServices/TestAppHostAuxiliaryBackchannel.cs
+++ b/tests/Aspire.Cli.Tests/TestServices/TestAppHostAuxiliaryBackchannel.cs
@@ -51,25 +51,48 @@ internal sealed class TestAppHostAuxiliaryBackchannel : IAppHostAuxiliaryBackcha
///
public Func>>? GetResourceSnapshotsHandler { get; set; }
+ ///
+ /// Gets or sets the function to call when WatchResourceSnapshotsAsync is invoked.
+ /// If null, yields the ResourceSnapshots list.
+ ///
+ public Func>? WatchResourceSnapshotsHandler { get; set; }
+
public Task GetDashboardUrlsAsync(CancellationToken cancellationToken = default)
{
return Task.FromResult(DashboardUrlsState);
}
- public Task> GetResourceSnapshotsAsync(CancellationToken cancellationToken = default)
+ public Task> GetResourceSnapshotsAsync(bool includeHidden, CancellationToken cancellationToken = default)
{
if (GetResourceSnapshotsHandler is not null)
{
return GetResourceSnapshotsHandler(cancellationToken);
}
- return Task.FromResult(ResourceSnapshots);
+ var snapshots = includeHidden
+ ? ResourceSnapshots
+ : ResourceSnapshots.Where(s => !ResourceSnapshotMapper.IsHiddenResource(s)).ToList();
+ return Task.FromResult(snapshots);
}
- public async IAsyncEnumerable WatchResourceSnapshotsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
+ public async IAsyncEnumerable WatchResourceSnapshotsAsync(bool includeHidden, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
+ if (WatchResourceSnapshotsHandler is not null)
+ {
+ await foreach (var snapshot in WatchResourceSnapshotsHandler(includeHidden, cancellationToken).WithCancellation(cancellationToken).ConfigureAwait(false))
+ {
+ yield return snapshot;
+ }
+ yield break;
+ }
+
foreach (var snapshot in ResourceSnapshots)
{
+ if (!includeHidden && ResourceSnapshotMapper.IsHiddenResource(snapshot))
+ {
+ continue;
+ }
+
yield return snapshot;
}
await Task.CompletedTask;
diff --git a/tests/Aspire.Hosting.Tests/Backchannel/AuxiliaryBackchannelRpcTargetTests.cs b/tests/Aspire.Hosting.Tests/Backchannel/AuxiliaryBackchannelRpcTargetTests.cs
index 749ad8bfc20..32a9e114b23 100644
--- a/tests/Aspire.Hosting.Tests/Backchannel/AuxiliaryBackchannelRpcTargetTests.cs
+++ b/tests/Aspire.Hosting.Tests/Backchannel/AuxiliaryBackchannelRpcTargetTests.cs
@@ -62,8 +62,8 @@ await notificationService.PublishUpdateAsync(resourceWithReplicas.Resource, "myr
var result = await target.GetResourceSnapshotsAsync();
- // Dashboard resource should be skipped
- Assert.DoesNotContain(result, r => r.Name == KnownResourceNames.AspireDashboard);
+ // Dashboard resource should now be included
+ Assert.Contains(result, r => r.Name == KnownResourceNames.AspireDashboard);
// Parameter resource (no replicas) should be returned with matching Name/DisplayName
var paramSnapshot = Assert.Single(result, r => r.Name == "myparam");
@@ -312,8 +312,6 @@ public async Task GetResourceLogsAsync_ReturnsLogsFromAllResources_WhenNoResourc
var log2 = Assert.Single(logs, l => l.ResourceName == "resource2");
Assert.Equal($"{TestTimestamp} Log from resource2", log2.Content);
- Assert.DoesNotContain(logs, l => l.ResourceName == KnownResourceNames.AspireDashboard);
-
await app.StopAsync();
}