{"record":{"id":"734580e3227b655f","repo":"microsoft/aspire","slug":"resource-snapshot-updates-support-only-one-consumer-for-the","errorCode":null,"errorMessage":"Resource snapshot updates support only one consumer for the lifetime of this watcher.","messagePattern":"Resource snapshot updates support only one consumer for the lifetime of this watcher\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/Aspire.Cli/Backchannel/ResourceSnapshotWatcher.cs","lineNumber":217,"sourceCode":"\n            _updateSignal?.Writer.TryWrite(true);\n        }\n    }\n\n    /// <summary>\n    /// Streams updates from the same subscription that maintains the current resource collection.\n    /// Callers should first capture the initial state with <see cref=\"CaptureAllResources\"/>.\n    /// The update stream can be enumerated only once over the lifetime of the watcher.\n    /// </summary>\n    public async IAsyncEnumerable<ResourceSnapshotUpdateBatch> WatchResourceSnapshotBatchesAsync(\n        long afterSequence,\n        [EnumeratorCancellation] CancellationToken cancellationToken = default)\n    {\n        EnsureInitialLoadComplete();\n        var updateSignal = _updateSignal ?? throw new InvalidOperationException(\"Resource update buffering was not enabled for this watcher.\");\n        if (Interlocked.Exchange(ref _updateConsumerClaimed, 1) != 0)\n        {\n            throw new InvalidOperationException(\"Resource snapshot updates support only one consumer for the lifetime of this watcher.\");\n        }\n\n        await foreach (var _ in updateSignal.Reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))\n        {\n            ResourceSnapshotUpdate[] updates;\n            bool isResync;\n            lock (_resourcesLock)\n            {\n                if (_resyncPending)\n                {\n                    updates = _resources.Values\n                        .Select(snapshot => new ResourceSnapshotUpdate(_updateSequence, snapshot))\n                        .ToArray();\n                    _resyncPending = false;\n                    isResync = true;\n                }\n                else\n                {","sourceCodeStart":199,"sourceCodeEnd":235,"githubUrl":"https://github.com/microsoft/aspire/blob/25830f84bd145686607ad00c057b3f84e2e51d43/src/Aspire.Cli/Backchannel/ResourceSnapshotWatcher.cs#L199-L235","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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."],"exampleFix":"// before\nawait foreach (var b in watcher.WatchResourceSnapshotBatchesAsync(0, ct)) await consumer1(b);\nawait foreach (var b in watcher.WatchResourceSnapshotBatchesAsync(0, ct)) await consumer2(b); // throws\n// after\nawait foreach (var b in watcher.WatchResourceSnapshotBatchesAsync(0, ct))\n{\n    await consumer1(b);\n    await consumer2(b);\n}","handlingStrategy":"type-guard","validationCode":"// claim the stream once and reuse the enumerator\nif (watcher.UpdateStreamAlreadyClaimed) throw new InvalidOperationException(\"This watcher's update stream is already being consumed.\");","typeGuard":"static bool CanEnumerate(ResourceSnapshotWatcher w) => !w.IsUpdateConsumerClaimed;","tryCatchPattern":"try { await foreach (var b in watcher.WatchResourceSnapshotBatchesAsync(0, ct)) { } }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"one consumer\")) { /* use an existing consumer or a new watcher */ }","preventionTips":["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"],"tags":["cli","watcher","single-consumer","streaming"],"backgroundTag":"invalid-state-transition","analyzedSha":"25830f84bd145686607ad00c057b3f84e2e51d43","analyzedAt":"2026-09-16T11:10:06.193Z","contentChangedAt":"2026-09-16T11:10:06.193Z","schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}