EF Core - InvalidOperationException
程式世界裡最令人頭痛的,從來不是錯誤訊息,而是「有時錯,有時對」的那種模糊不明。你是不是也曾遇過:明明昨天的程式還好好的,今天一跑卻爆了 InvalidOperationException?
這不是你寫錯,而是你不小心跨越了 DbContext 的界線。就像在海上行船,一不小心就漂出了領海,進入無人知曉的風暴。
這篇文章將帶你從根源理解 InvalidOperationException 的幾個常見場景,並透過範例與圖解,一次性釐清觀念,避免未來再被神祕的錯誤訊息追殺。
💥 DbContext 實例重複使用|一艘船不能載兩個船長
問題來源
當你在多個請求或執行緒間重複使用同一個 DbContext 實例,就可能觸發 InvalidOperationException。為什麼?因為…
❗ DbContext 不是 Thread-Safe 的物件。
它內部維護著實體狀態(Added, Modified, Deleted…),而這些狀態並不是為多執行緒設計的。你讓多個線程同時操控 DbContext,就像讓兩個船長同時掌舵 —— 翻船。
1 | public class UserService |
✅ 解法
使用依賴注入(DI),確保 DbContext 的生命週期是 Scoped,每次請求都取得全新的實例。
1
services.AddDbContext<AppDbContext>(options => ...); // 預設為 Scoped
若在非同步情境下同時處理多筆資料(如 Task.WhenAll),請為每個 Task 各自建立 DbContext 實例。(但不建議這樣操作)
💥 SaveChanges 誤用|看似原子,其實碎裂
問題來源
你可能以為包在 BeginTransaction() 裡,資料就會「全成功或全失敗」。但事實上:
❗ 每次 SaveChanges() 本身就會觸發 mini-transaction。
若你在一個交易中呼叫多次 SaveChanges(),只要其中一次失敗,先前成功的部份就無法自動 Rollback!
危險寫法:
1 | public void SaveChangesInTransaction() |
✅ 解法
1 | using (var transaction = _dbContext.Database.BeginTransaction()) |
💥 跨 DbContext 協同操作|兩條船不是同一隊
問題來源
當你在同一個邏輯流程中操作了多個 DbContext 實例,而這些實例所連線的資料庫彼此不共享同一個底層連線(DbConnection)或交易(Transaction),就無法讓它們共同參與一個真正的資料庫交易(transaction)。
EF Core 偵測到交易混亂時,會直接拋出 InvalidOperationException,EF Core 設計上,每個 DbContext 都是獨立的狀態管理者與資料庫連線管理者。它內部的行為包含:
- 自己的 ChangeTracker
- 自己的 DbConnection 實例(除非特別共用)
- 自己的 Transaction 管理
當你用兩個 DbContext 嘗試進行同一筆交易時,它們可能無法互相同步狀態也不知道對方的交易存在與否
1 | using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) |
✅ 解法
- 解法一:強制共用底層連線與交易(但不推薦,複雜易錯)
- 解法二:改為使用單一 DbContext 處理整個流程
💥 漏設 TransactionScopeAsyncFlowOption.Enabled|異步交易上下文遺失
問題來源
當你在 C# 中使用 TransactionScope 包裹非同步方法(例如 SaveChangesAsync())時,如果沒有設定 TransactionScopeAsyncFlowOption.Enabled,那麼在 await 之後,交易上下文(Transaction Context)會中斷,導致 EF Core 無法感知目前交易,最後拋出
1 | System.InvalidOperationException: A TransactionScope must be disposed on the same thread that it was created. |
C# 的 async/await 是「狀態機(state machine)」,不是單純的同步堆疊函數呼叫。在 await 的那一行執行完後,程式控制權會「跳出去」,等任務完成再「跳回來」。如果你沒有特別設定「交易流動選項」,這個跳出去與跳回來的過程會讓交易上下文丟失。而 TransactionScope 是靠 Ambient Transaction(環境交易) 的概念在工作的,依賴 ThreadStatic 儲存當前交易。一旦 await 讓你的執行換了執行緒(例如從 ThreadA 換到 ThreadB),原本儲存在 ThreadA 的 ambient transaction context 就不見了。
✅ 解法
在使用 TransactionScope 時,設置 TransactionScopeAsyncFlowOption.Enabled 以支持異步操作中的事務流。
1 | using (var transactionScope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) |
🌊 結語|界線不是限制,是守護
InvalidOperationException 表面看來只是個錯誤訊息,實則是在提醒我們:
「你越界了,請回頭。」
在 EF Core 裡,DbContext 就像一艘船,它有明確的範圍與規則。你若硬是拉它去別的領海跑,它就會給你個錯誤作為警告。
學會掌握交易的邊界、資料的原子性、與上下文的獨立性,不只是技術層面的最佳實踐,更是讓系統穩定與可維護的基石。