{"record":{"id":"5a1e022b88fea59a","repo":"dotnet/reactive","slug":"disposable-already-assigned","errorCode":null,"errorMessage":"Disposable already assigned.","messagePattern":"Disposable already assigned\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"AsyncRx.NET/System.Reactive.Async/Disposables/SingleAssignmentAsyncDisposable.cs","lineNumber":27,"sourceCode":"{\n    public sealed class SingleAssignmentAsyncDisposable : IAsyncDisposable\n    {\n        private static readonly IAsyncDisposable Disposed = AsyncDisposable.Create(() => default);\n\n        private IAsyncDisposable _disposable;\n\n        public async ValueTask AssignAsync(IAsyncDisposable disposable)\n        {\n            if (disposable == null)\n                throw new ArgumentNullException(nameof(disposable));\n\n            var old = Interlocked.CompareExchange(ref _disposable, disposable, null);\n\n            if (old == null)\n                return;\n\n            if (old != Disposed)\n                throw new InvalidOperationException(\"Disposable already assigned.\");\n\n            await disposable.DisposeAsync().ConfigureAwait(false);\n        }\n\n        public ValueTask DisposeAsync()\n        {\n            return Interlocked.Exchange(ref _disposable, Disposed)?.DisposeAsync() ?? default;\n        }\n    }\n}\n","sourceCodeStart":9,"sourceCodeEnd":38,"githubUrl":"https://github.com/dotnet/reactive/blob/94b5d5ab912789f5abe9a72138a25bbd716fe59c/AsyncRx.NET/System.Reactive.Async/Disposables/SingleAssignmentAsyncDisposable.cs#L9-L38","documentation":"SingleAssignmentAsyncDisposable.AssignAsync throws InvalidOperationException(\"Disposable already assigned.\") when AssignAsync is called a second time while the disposable is neither unset nor disposed. The CompareExchange observes a non-null old value that is not the Disposed sentinel, meaning a second disposable is being attached to a single-assignment slot — a contract violation the library fails fast on.","triggerScenarios":"Calling AssignAsync twice on the same instance; races between concurrent tasks (firstTask/secondTask patterns) that both assign; operator sinks (Append, Buffer, DoWhile) re-subscribing or re-entering the assignment path after the single-assignment disposable was already populated and not yet disposed.","commonSituations":"Re-running a query builder that shares one SingleAssignmentAsyncDisposable across subscriptions; a race where two concurrent async flows both try to register their cleanup; forgetting to create a fresh sink per subscription in a cold observable.","solutions":["Create a new SingleAssignmentAsyncDisposable per subscription/assignment instead of reusing one instance","Serialize the assignment (lock, semaphore, or assign-once flag) so only the first caller assigns","Check whether assignment already happened before calling AssignAsync and skip the second call","If re-assignment is intentional, dispose the old single-assignment disposable first (after disposal, AssignAsync disposes the new one without throwing) or use a different container (e.g. SerialAsyncDisposable) that supports replacement"],"exampleFix":"// before\nawait sink.Disposable.AssignAsync(d1);\nawait sink.Disposable.AssignAsync(d2); // throws: already assigned\n// after\nawait sink.Disposable.AssignAsync(d1);\nif (!assigned)\n{\n    assigned = true;\n    await sink.Disposable.AssignAsync(d2);\n}\n// or, when replacement is intended, use SerialAsyncDisposable:\nawait serial.AssignAsync(d2); // replaces d1 safely","handlingStrategy":"validation","validationCode":"// allow assignment only once per slot, or after disposal\nif (Interlocked.Exchange(ref assignedFlag, 1) == 0)\n{\n    await single.AssignAsync(d);\n}\nelse\n{\n    await d.DisposeAsync(); // dispose the surplus disposable instead of throwing\n}","typeGuard":"bool CanAssignNow() => assignedFlag == 0;","tryCatchPattern":"try\n{\n    await single.AssignAsync(d);\n}\ncatch (InvalidOperationException ex) when (ex.Message == \"Disposable already assigned.\")\n{\n    // double assignment: dispose the redundant disposable and keep the first one\n    await d.DisposeAsync();\n}","preventionTips":["Create a fresh SingleAssignmentAsyncDisposable per subscription instead of sharing instances","Synchronize concurrent assignment paths (task race in firstTask/secondTask patterns) so only one assigns","Use SerialAsyncDisposable when replacement (rather than single assignment) is the intent","Track assignment state with an explicit flag or check the current disposable before assigning"],"tags":["csharp","invalid-operation","async-disposable","concurrency","state-error"],"backgroundTag":"invalid-state-transition","analyzedSha":"94b5d5ab912789f5abe9a72138a25bbd716fe59c","analyzedAt":"2026-09-15T02:26:24.759Z","contentChangedAt":"2026-09-15T02:26:24.759Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}