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

  1. Enumerate WatchResourceSnapshotBatchesAsync exactly once per watcher; fan out updates to additional consumers yourself (e.g. Channel<T>/BroadcastBlock).
  2. Create a new ResourceSnapshotWatcher instance for each independent update stream.
  3. 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

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


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)