InvalidOperationException in C#: Fix Bad Object State
Check the object's state before you act: don't modify collections mid-loop, don't call Single on 0 or 2 rows, don't use a disposed context.
20+ years shipping production .NET services in enterprise systems. Written from production experience, not tutorials.
- ✓Basic C# and LINQ queries
- ✓How foreach and IDisposable work
- ✓Running dotnet test locally
- InvalidOperationException means the object exists but its current state rejects your call — right object, wrong moment
- Never modify a collection inside foreach over it; loop over a .ToList() snapshot or collect changes and apply them after
- Single() demands exactly one match — use FirstOrDefault with a null check when zero rows are normal, and OrderBy plus First when many match
- A disposed DbContext or closed connection throws on next use — keep one context per scope and never share it across threads
- Think in state machines: check CanExecute-style state (IsOpen, Count, HasValue) before acting instead of catching the exception
Think of a vending machine. It's a perfectly good machine, but if you press the button while the door is open for restocking, it refuses — not because you're wrong, but because the timing is wrong. That's InvalidOperationException. Your object is alive, yet it's in a state where your call makes no sense: editing a list while reading it, asking for the single match when two exist, or using a connection you closed.
InvalidOperationException is C#'s way of saying the call was legal in general but illegal right now. New developers often confuse it with NullReferenceException or ArgumentException, then waste an hour guarding the wrong thing. The object isn't null and the argument isn't malformed — the sequence is wrong. You enumerated while modifying, queried before opening, or reused something already torn down.
These bugs share a shape: hidden state machines. Collections track a version counter that invalidates live enumerators. LINQ operators like Single encode count expectations that real data violates. DbContext tracks its own disposal and connection state. Once you see each API as a small state machine with allowed and forbidden transitions, the exceptions stop feeling random and start reading like instructions.
This guide covers the three heavy hitters — enumerator invalidation, Single versus First, and disposed contexts — plus a debugging workflow that reads the exception message literally. .NET's messages here are unusually honest: they tell you exactly which state rule you broke. You'll learn to trust them, reproduce with minimal tests, and restructure code so the illegal state can't arise.
Read the Message: .NET Tells You the State Rule
InvalidOperationException messages are unusually specific, and trusting them literally saves hours. Collection was modified means exactly that — a version counter moved under a live enumerator. Sequence contains no elements means your Single or First found zero rows. The operation cannot be completed because the DbContext has been disposed names both the object and its state. These aren't riddles; they're the state machine telling you which transition you attempted.
Build the habit of mapping each message to its rule. Enumerator messages point at mutation during iteration, including sneaky paths like lazy LINQ chains that execute during the loop. Query messages point at count assumptions — Single wants exactly one, First wants at least one, and your data disagrees. Connection and context messages point at lifetime bugs where setup, use, and teardown ran in the wrong order.
The workflow is always the same: copy the exact message, find the named object in the trace line, and ask what state it was in when the call landed. Then reproduce that state in a test with the smallest possible setup — two list items, zero query rows, a disposed context. Small reproductions turn vague dread into a concrete transition you can see and fix.
Enumerators: Why foreach Hates Mid-Loop Edits
Every List<T> carries an internal version number bumped by Add, Remove, Clear, and even sorting. When foreach starts, the enumerator snapshots that version. Each MoveNext compares the snapshot to the live version, and any mismatch throws InvalidOperationException immediately. This fail-fast design protects you from skipping items or reading garbage — a silent wrong result would be far worse than a loud exception.
The trap is that modification hides behind innocent-looking calls. Passing the collection to a helper that removes items, triggering lazy-loading that appends, or awaiting inside the loop while another thread edits — all bump the version. Deferred LINQ makes it worse: orders.Where(...) doesn't execute until enumerated, so a filter chained before the loop can execute inside it and observe mid-loop changes.
Three safe patterns cover nearly every case. Snapshot with .ToList() when removals are the point of the loop. Collect-then-apply when you need the live list intact during iteration — gather victims in a second list, remove after. Or use RemoveAll with a predicate and skip the loop entirely. Pick one per situation and the whole bug class disappears from your code for good.
ToList() ended the outage — enumeration ran on a snapshot while removals hit the live list, and the weekend's 12,000 retries dropped to zero.Single vs First: Encode Your Count Expectations
LINQ's element operators are count assertions in disguise. Single demands exactly one match and throws on zero or two-plus. SingleOrDefault tolerates zero but still throws on two-plus. First demands at least one and throws on empty. FirstOrDefault tolerates empty and takes the first of many. Picking the wrong one turns normal data into exceptions — the classic is Single on a lookup where duplicates legitimately exist.
The decision tree is short. Truly unique key like a primary key lookup? Single or SingleOrDefault, and let duplicates throw because they signal corruption. Zero-or-one like an optional profile? SingleOrDefault with a null check. Zero-or-more where you want the best match? OrderBy plus FirstOrDefault — the ordering matters because First without OrderBy on a database picks an arbitrary row.
Watch the async and nullable interplay: SingleOrDefaultAsync returns null for missing reference types but default structs silently, so check HasValue or null explicitly. And never call Single on an unbounded query without a filter — you're asserting global uniqueness on data you haven't inspected. The exception message will say sequence contains no elements or more than one element; believe it and change the operator, not the data.
Disposed DbContext: Lifetimes You Must Respect
DbContext is a scoped unit of work — it tracks changes, holds a connection, and must be disposed after each logical operation. Using it after disposal throws because its internal state (connection, change tracker, caches) is torn down. The usual cause is an async continuation that outlives its scope: you await something, the using block exits, then the continuation touches the dead context.
Registration mistakes amplify the damage. A Singleton DbContext shared across requests throws under concurrency and leaks tracked entities until memory balloons. A context captured in a background lambda or a fire-and-forget Task gets disposed when the request scope ends, then throws minutes later in a place the trace barely connects to the cause.
The rules are simple and absolute. Register DbContext as Scoped, resolve a fresh scope per background job with IServiceScopeFactory, and await every query inside the using block — never return an unmaterialized IQueryable past the block boundary. Call ToListAsync before leaving the scope so execution finishes while the context is alive. Lifetime bugs vanish when no context ever outlives its scope, so treat any query crossing that boundary as a defect.
Connections and Readers: Open Late, Close Early
AdoDb connections and data readers are tiny state machines: closed, open, reading, done. ExecuteReader on a closed connection throws; opening an already-open connection throws; reading past the last row throws. The messages name the expected state every time, so the fix is sequencing, not logic.
The safe pattern is ruthlessly boring: create the connection in a using block, open it immediately before the command, read everything you need, and let disposal close it. Don't cache open connections in fields — pooling already handles reuse far better than your field ever will, and a cached connection goes stale the moment the network hiccups or the server kills an idle session.
Multiple active result sets deserve a callout: without MARS enabled, opening a second reader on the same connection while the first is open throws. Either enable MultipleActiveResultSets in the connection string or materialize the first result into a list before running the second query. Async code adds one more rule — don't share a connection across concurrent awaits. One operation per connection at a time keeps the state machine in a legal transition every run. Log the connection state at the throw site once and the ordering bug usually confesses on the spot.
State-Machine Thinking for Everyday APIs
Once you've been bitten, you start seeing state machines everywhere: enumerators (ready, walking, invalidated), tasks (waiting, running, completed), timers (stopped, ticking, disposed), UI controls (created, shown, disposed). InvalidOperationException is what you get for calling a method in the wrong state, and the prevention is checking state before acting rather than catching afterward.
Many APIs offer explicit state checks — IsOpen, Count, HasValue, IsCompleted, CanRead — precisely so you can branch instead of throwing. Nullable<T>.Value throws on null, so check HasValue first. Task.Result blocks or throws on faults, so await instead. These checks read as boring boilerplate, but they're the difference between code that handles reality and code that assumes it.
Where you own the design, make illegal states unrepresentable. Constructors that demand required data, read-only collections instead of mutable ones passed around, builders that validate before Build — each removes a transition your callers could get wrong. And when an operation genuinely can't proceed, throw InvalidOperationException yourself with a message naming the expected state. Future you, reading that message at midnight, will be grateful.
A Friday Deploy Threw 12,000 InvalidOperationExceptions
ToList()) so removals hit the live list while enumeration runs on a snapshot — a one-word fix. The retry policy was tightened from 10 attempts to 3 with exponential backoff so logic bugs can't hammer the system all weekend. A regression test with 50 stale orders was added, plus an analyzer rule flagging collection modification inside foreach during code review.- Never mutate a collection inside foreach over it — snapshot with ToList or gather removals and apply them after the loop.
- Retry policies multiply logic bugs; cap retries and alert after 3 consecutive failures instead of retrying all weekend.
- Test background jobs with realistic volumes — a single-item test can't catch state bugs that need two items to trigger.
ToList() or collect victims in a separate list and remove them after the loop.Count() to see 0 or 2+ matches — e.g. dotnet run a probe or SELECT COUNT(*) with the same predicate. Fix: use FirstOrDefault plus explicit null handling when zero is normal; add OrderBy plus First when many match; keep Single only for truly unique lookups.| File | Command / Code | Purpose |
|---|---|---|
| Messages.cs | var orders = new List | Read the Message |
| EnumeratorFix.cs | var orders = LoadOrders(); | Enumerators |
| SingleVsFirst.cs | int id = 42; | Single vs First |
| ContextLifetime.cs | await using var scope = provider.CreateAsyncScope(); | Disposed DbContext |
| ConnectionPattern.cs | await using var conn = new SqlConnection(connString); | Connections and Readers |
| StateChecks.cs | if (!order.ShippedAt.HasValue) | State-Machine Thinking for Everyday APIs |
Key takeaways
Common mistakes to avoid
5 patternsRemoving items inside foreach over the same list
ToList(), collect victims then remove, or call RemoveAll with a predicateUsing Single where data isn't unique
Returning IQueryable past a using block
Registering DbContext as Singleton
Catching InvalidOperationException as control flow
Interview Questions on This Topic
What does InvalidOperationException mean in one sentence?
Frequently Asked Questions
20+ years shipping production .NET services in enterprise systems. Written from production experience, not tutorials.
That's Exceptions. Mark it forged?
5 min read · try the examples if you haven't