Skip to content

Fix thread safety of disposableObjects in ServiceContainer.Dispose#611

Merged
seesharper merged 3 commits into
masterfrom
fix-dispose-thread-safety
May 19, 2026
Merged

Fix thread safety of disposableObjects in ServiceContainer.Dispose#611
seesharper merged 3 commits into
masterfrom
fix-dispose-thread-safety

Conversation

@seesharper
Copy link
Copy Markdown
Owner

@seesharper seesharper commented May 19, 2026

Summary

  • TrackInstance and Dispose() both accessed disposableObjects (List<IDisposable>) without any synchronization, creating a data race under concurrent load — most visibly when services are still being first-resolved on background threads while the container is being disposed. Depending on .NET version and timing, this manifests as InvalidOperationException: Collection was modified or ArgumentException: Destination array was not long enough.
  • Dispose() also had no guard against the same instance being disposed more than once (e.g. if two container lifetimes happened to return the same object).

Changes

  • TrackInstance — wraps disposableObjects.Add() in lock(container.lockObject), matching the locking pattern already used by Scope.TrackInstance.
  • ServiceContainer.Dispose() — atomically snapshots-and-clears disposableObjects under lockObject, then disposes the snapshot outside the lock so no lock is held during user Dispose() calls (avoids deadlocks). A reference-equality HashSet skips duplicate entries, consistent with how Scope.Dispose() already works.
  • DisposableObjectComparer — new private nested class using RuntimeHelpers.GetHashCode for stable object-identity hashing.
  • Dispose_ConcurrentWithServiceCreation_DoesNotThrow — stress test with 3 concurrent threads (2 resolving services, 1 disposing) over 1000 iterations as a regression guard.

Test plan

  • All 1529 existing tests continue to pass
  • New concurrent stress test passes under the fix

🤖 Generated with Claude Code

seesharper and others added 3 commits May 19, 2026 13:19
TrackInstance and Dispose both accessed the disposableObjects list without
synchronization, causing "collection was modified" errors under concurrent load.

- Lock disposableObjects.Add in TrackInstance using the existing lockObject
- Atomically snapshot-and-clear disposableObjects under lock in Dispose, then
  dispose the snapshot outside the lock to avoid holding the lock during user code
- Use a reference-equality HashSet to skip duplicates, consistent with Scope.Dispose
- Add DisposableObjectComparer nested class for stable identity-based hashing
- Add concurrent stress test (1000 iterations, 3 threads) as regression guard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cover the duplicate-detection continue branches in Scope.DisposeAsync()
(fast IAsyncDisposable path, IDisposable path, and slow async Await path)
and the DisposableObjectComparer.Equals path in ServiceContainer.Dispose().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@seesharper seesharper merged commit 5e1761e into master May 19, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant