EF Core - TransactionScope vs BEGIN TRAN/COMMIT

「如果用 TransactionScope 包了一段程式,裡面又在 SQL 裡手動開 BEGIN TRAN,那最後誰決定要不要真的 commit?」

只要你在 using 範圍內操作資料庫,EF 或 ADO.NET 會自動參與這個「環境交易 (Ambient Transaction)」。如果沒呼叫 scope.Complete(),在離開 using 時就會自動 rollback
1 | using (var scope = new TransactionScope()) |

這是 T-SQL 層級的交易,寫在 SQL 裡面
1 | BEGIN TRAN |
這是 資料庫端的交易。它可以存在於
- 一個獨立的連線(沒有
Ambient Transaction) - 某個由 .NET 傳進來的環境交易裡面
當你在 C# 中用 TransactionScope 包起來後,你建立的每個資料庫連線,預設會自動加入這個環境交易 (Ambient Transaction),所以 BEGIN TRAN 這個「SQL 裡的交易」其實只是「子交易」或「巢狀交易」,它會參與外部的環境交易 (Ambient Transaction)
1 | TransactionScope (老大) |
當 TransactionScope 最後沒有 .Complete(),整個 Ambient Transaction 會被 Rollback,即使第二段 SQL 在資料庫層看起來「COMMIT 成功」,實際上那只是該連線內的子交易結束,最終整個分散式交易 (MSDTC) 會通知資料庫 rollback
1 | static string cs = "Data Source=...省略...;Application Name={0}"; |
每次建立連線時,都使用不同的 Application Name。因為 SQL Server 的連線池 (Connection Pool) 會根據連線字串內容共用連線,這樣做可以強迫建立不同的連線,確保之後會觸發 MSDTC 分散式交易

清空測試用的 LockLab 表
1 | // 將LockLab資料表清空 |

建立TransactionScope
1 | using (TransactionScope tx = new TransactionScope()) |

以隨機式連線字串建立連線,確保觸發分散式交易
1 | using (SqlConnection cn = new SqlConnection(getDiffCnStr())) |
第一段 sql, Insert
1 | //以隨機式連線字串建立連線,確保觸發分散式交易 |

第二段 SQL 使用COMMIT TRAN
1 | //第二段SQL 使用COMMIT TRAN |

觸發 Rollback
1 | throw new ApplicationException("刻意觸發錯誤!"); |

Overview
1 |
|
| 步驟 | 動作 | 狀態 |
|---|---|---|
| 1 | 第一段 SQL 插入 'TEST' |
在交易中,未提交 |
| 2 | 第二段 SQL 插入 'JEFF' 並 COMMIT TRAN |
子交易 Commit,但仍屬於外層交易 |
| 3 | 丟出 Exception,未呼叫 tx.Complete() |
TransactionScope 結束時 Rollback 整個 MSDTC 交易 |
| 4 | 結果 | LockLab 仍是空表(兩筆資料都被 Rollback) |
即使第二段 SQL 在程式裡明確執行了 COMMIT TRAN,只要它是在 TransactionScope 的範圍內,就會被視為外層環境交易 (Ambient Transaction) 的子交易。當外層 TransactionScope 沒呼叫 .Complete(),整個分散式交易會 Rollback → ✅ 結果是所有 Insert 都不會留下。這就是為什麼這段程式執行完後 LockLab 仍是空的












