TransactionScope未提交/回滚及重复调用失效问题咨询
1. What happens if you create a new TransactionScope without committing or rolling back the existing one?
Let me break this down based on how TransactionScope works under the hood:
- First, if you leave a TransactionScope hanging (no
Complete(), noDispose()), that transaction stays active and ties up the associated database connection—connections won't go back to the pool until the transaction finishes. This can lead to connection pool exhaustion over time if you keep doing this. - When you create a new TransactionScope, its behavior depends on the
TransactionScopeOptionyou specify:- Default (Required): The new scope will enlist in the existing ambient transaction. All operations in the new scope will be part of that same uncompleted transaction. Eventually, when the original scope gets disposed (either explicitly or via GC), since
Complete()was never called, the entire transaction (including all work from the new scope) will roll back. - RequiresNew: A brand new independent transaction will be created, separate from the hanging one. But the original transaction still holds resources until it's disposed.
- Suppress: The new scope will run without any ambient transaction, so its operations aren't tied to the hanging one at all.
- Default (Required): The new scope will enlist in the existing ambient transaction. All operations in the new scope will be part of that same uncompleted transaction. Eventually, when the original scope gets disposed (either explicitly or via GC), since
Worst case, if you never dispose the original scope, the database might eventually time out the transaction, but that's not reliable—you'll likely run into connection issues or unexpected rollbacks before that.
2. Why isn't TransactionScope working on the second call to callFunc()?
From your description, this almost certainly boils down to not properly disposing the first TransactionScope. Here's the likely scenario:
Your first call to callFunc() creates a TransactionScope but doesn't wrap it in a using block, and you return without calling Dispose() or Complete(). That leaves the ambient transaction active and the scope's resources unmanaged.
When you call callFunc() the second time, the new TransactionScope (using the default Required option) automatically joins that existing ambient transaction. But since the original scope was never completed, whenever it finally gets disposed (either when GC kicks in or the app exits), the entire transaction—including the second call's inserts—will roll back. That makes it look like the second TransactionScope didn't work, but it actually did—it just got caught up in a doomed transaction.
Fixes you should implement right away:
- Always wrap TransactionScope in a
usingblock: This ensuresDispose()is called automatically, even if an exception is thrown or you return early. Example:void callFunc() { using (var scope = new TransactionScope()) { // Insert into table A scope.Complete(); // Only call this if you want to commit the transaction } // Dispose is called here automatically } - Explicitly call
Complete()only when you're sure the work should be committed: If you don't call this,Dispose()will roll back the transaction by default. - Check for ambient transactions before creating a new scope if needed: If you want to avoid joining an existing transaction in some cases, use
TransactionScopeOption.RequiresNeworSuppressintentionally.
Also, the reason you don't see the inserted data while paused at a breakpoint is because of transaction isolation (default is Read Committed), which prevents uncommitted changes from being visible to other sessions until the transaction is completed. That's expected behavior!
内容的提问来源于stack exchange,提问作者Peru

