microsoft/aspire · error · InvalidOperationException
Resource snapshot updates support only one consumer for the…
Error message
Resource snapshot updates support only one consumer for the lifetime of this watcher.
What it means
WatchResourceSnapshotBatchesAsync supports exactly one consumer for the watcher's lifetime; the method claims the consumer slot with Interlocked.Exchange and throws InvalidOperationException if it was already claimed. Re-enumerating the stream (including after the first enumeration completes or throws) is not supported.
Solutions
- Enumerate WatchResourceSnapshotBatchesAsync exactly once per watcher; fan out updates to additional consumers yourself (e.g. Channel<T>/BroadcastBlock).
- Create a new ResourceSnapshotWatcher instance for each independent update stream.
- Restructure retry logic so retries reuse the original enumeration instead of starting a new one on the same watcher.
Example fix
// before
await foreach (var b in watcher.WatchResourceSnapshotBatchesAsync(0, ct)) await consumer1(b);
await foreach (var b in watcher.WatchResourceSnapshotBatchesAsync(0, ct)) await consumer2(b); // throws
// after
await foreach (var b in watcher.WatchResourceSnapshotBatchesAsync(0, ct))
{
await consumer1(b);
await consumer2(b);
} Defensive patterns
Strategy: type-guard
Validate before calling
// claim the stream once and reuse the enumerator
if (watcher.UpdateStreamAlreadyClaimed) throw new InvalidOperationException("This watcher's update stream is already being consumed."); Type guard
static bool CanEnumerate(ResourceSnapshotWatcher w) => !w.IsUpdateConsumerClaimed;
Try / catch
try { await foreach (var b in watcher.WatchResourceSnapshotBatchesAsync(0, ct)) { } }
catch (InvalidOperationException ex) when (ex.Message.Contains("one consumer")) { /* use an existing consumer or a new watcher */ } Prevention
- Enumerate WatchResourceSnapshotBatchesAsync exactly once per watcher
- Fan out to multiple consumers via a broadcast channel instead of re-enumerating
- Never retry by re-calling WatchResourceSnapshotBatchesAsync on the same instance
When it happens
Trigger: Calling WatchResourceSnapshotBatchesAsync a second time on the same watcher instance, or enumerating the returned IAsyncEnumerable from two places concurrently (e.g. passing it to multiple consumers).
Common situations: Subscribing twice after a retry following an earlier enumeration; fanning the same async-iterable out to several UI/dashboard consumers; re-running a watch command in-process against a cached watcher.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- Resource update buffering was not enabled for this watcher.
- Initial resource snapshot load has not completed. Call…
- Already connected to AppHost backchannel.
- Already connected to
- AppHost is incompatible with the CLI. The AppHost must be…
AI-assisted analysis of microsoft/aspire@25830f84bd (2026-09-16).
Data as JSON: /api/errors/734580e3227b655f.
Report an issue: GitHub.
Appendix: source
Thrown at src/Aspire.Cli/Backchannel/ResourceSnapshotWatcher.cs:217
_updateSignal?.Writer.TryWrite(true);
}
}
/// <summary>
/// Streams updates from the same subscription that maintains the current resource collection.
/// Callers should first capture the initial state with <see cref="CaptureAllResources"/>.
/// The update stream can be enumerated only once over the lifetime of the watcher.
/// </summary>
public async IAsyncEnumerable<ResourceSnapshotUpdateBatch> WatchResourceSnapshotBatchesAsync(
long afterSequence,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
EnsureInitialLoadComplete();
var updateSignal = _updateSignal ?? throw new InvalidOperationException("Resource update buffering was not enabled for this watcher.");
if (Interlocked.Exchange(ref _updateConsumerClaimed, 1) != 0)
{
throw new InvalidOperationException("Resource snapshot updates support only one consumer for the lifetime of this watcher.");
}
await foreach (var _ in updateSignal.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
{
ResourceSnapshotUpdate[] updates;
bool isResync;
lock (_resourcesLock)
{
if (_resyncPending)
{
updates = _resources.Values
.Select(snapshot => new ResourceSnapshotUpdate(_updateSequence, snapshot))
.ToArray();
_resyncPending = false;
isResync = true;
}
else
{View on GitHub (pinned to 25830f84bd)