「這裡的風很大,請小心 DbContext 被吹出作用域。」
開發 ASP.NET Core 應用程式時,你一定碰過這段設定
1 services.AddDbContext<MyDbContext>();
這一行註冊你的 DbContext,但你知道它的 生命週期(Lifetime)是什麼嗎?Scoped、Transient 差在哪?又該怎麼選擇?今天我們就來一趟資料庫生命週期的航行。
🌊 EF Core 的預設生命週期:Scoped Entity Framework contexts
By default, Entity Framework contexts are added to the service container using the scoped lifetime because web app database operations are normally scoped to the client request.
簡單來說,EF Core 的 DbContext 預設會註冊成 Scoped,也就是,每個 HTTP Request 會建立一個 DbContext 實例。這個實例會被整個 Request 共用,直到請求結束後才釋放。這種設計既合理又安全,畢竟一個請求裡的資料庫操作應該是一體的,不應該在不同的 DbContext 之間跳來跳去。
🌊 Scoped vs Transient:生命週期的差異 如果你把 DbContext 改成 Transient 呢?意思是:
每次注入都會新建一個 DbContext。即使在同一個 Request 裡的兩個 Repository,也會拿到不同的實例。
🧪 實例範例:A、B Repository 同時操作 DbContext 想像有两個同時操作的 Repository,ARepository 與 BRepository 都正確 DI 同一個 AdventureWorks2022Context:
A 增加 “Elly”
B 增加 “Joanne”
中間動作包在一個 Transaction 中,然後 Rollback
使用 Scoped:兩者 DbContext HashCode 相同,證明分享同一實例。 使用 Transient:兩者是不同的 DbContext,Transaction 無法共用,甚至會出現新增加時 DB Lock 的問題。
ARepository
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class ARepository { private readonly AdventureWorks2022Context _adventureWorks2022DbContext; public AdventureWorks2022Context DbContext => _adventureWorks2022DbContext; public ARepository (AdventureWorks2022Context adventureWorks2022DbContext ) { this ._adventureWorks2022DbContext = adventureWorks2022DbContext; } public async Task AddRDA () { var RD1 = new Rd() { RdName = "Elly" , DeptCode = "PQ3" , }; this ._adventureWorks2022DbContext.Rds.Add(RD1); var RD2 = new Rd() { RdName = "grger" , DeptCode = "ff" , }; this ._adventureWorks2022DbContext.Rds.Add(RD2); await this ._adventureWorks2022DbContext.SaveChangesAsync(); } public string AllData => string .Join("," , _adventureWorks2022DbContext.Rds.Select(rd => rd.RdName).ToArray()); }
BRepository
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class BRepository { private readonly AdventureWorks2022Context _adventureWorks2022DbContext; public AdventureWorks2022Context DbContext => _adventureWorks2022DbContext; public BRepository (AdventureWorks2022Context adventureWorks2022DbContext ) { this ._adventureWorks2022DbContext = adventureWorks2022DbContext; } public async Task AddRDB () { var RD1 = new Rd() { RdName = "Joanne" , DeptCode = "RD05" , }; this ._adventureWorks2022DbContext.Rds.Add(RD1); var RD2 = new Rd() { RdName = "WGEWRGR" , DeptCode = "ff" , }; this ._adventureWorks2022DbContext.Rds.Add(RD2); await this ._adventureWorks2022DbContext.SaveChangesAsync(); } public string AllData => string .Join("," , _adventureWorks2022DbContext.Rds.Select(rd => rd.RdName).ToArray()); }
皆註冊為 Scoped (Http Request 生命週期)
1 2 3 4 builder.Services.AddDbContext<AdventureWorks2022Context>(options => options.UseSqlServer(configurationManager.GetConnectionString("AdventureWorks2022" ))); builder.Services.AddScoped<ARepository>(); builder.Services.AddScoped<BRepository>();
在 Program.cs 仿造部落格文章撰寫 Minimal API 測試(方便),只是改成非同步版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 var app = builder.Build();app.MapGet("/" , async (ARepository a, BRepository b) => { a.DbContext.Database.EnsureCreated(); System.Console.WriteLine($"Dbcontext in ARepository hashcode : {a.DbContext.GetHashCode()} " ); System.Console.WriteLine($"Dbcontext in BRepository hashcode : {b.DbContext.GetHashCode()} " ); System.Console.WriteLine($"Dbcontext in BRepository Before Transaction : {b.DbContext.Database.CurrentTransaction} " ); await using (var Atransaction = await a.DbContext.Database.BeginTransactionAsync()) { System.Console.WriteLine($"Dbcontext in BRepository After Transaction : {b.DbContext.Database.CurrentTransaction} " ); System.Console.WriteLine($"ARepo Transaction HashCode : {a.DbContext.Database.CurrentTransaction?.GetHashCode()} vs BRepo Transaction HashCode : {b.DbContext.Database.CurrentTransaction?.GetHashCode()} " ); await a.AddRDA(); try { await b.AddRDB(); } catch (Exception ex) { System.Console.WriteLine($"Error : {ex.Message} " ); } System.Console.WriteLine($"data before Rollback : A {a.AllData} vs B {b.AllData} " ); await Atransaction.RollbackAsync(); } System.Console.WriteLine($"data after rollback : A {a.AllData} B {b.AllData} " ); });
註冊為 Scoped,ARepository 與 BRepository 取得的 DbContext 是同一個物件,二者的操作也被包成同一個交易
改註冊為 Transient,ARepository、BRepository 的 DbContext 不同,甚至 ARepository 啟用交易後,可能還會引發資料庫鎖定導致 BRepository 無法寫入,兩邊也無法包成交易,不過寫入會不會互卡還關乎 Provider 是誰
🌊 為何 using dbContext() 你可能有看過這種 Code
1 using var context = new ProductContext();
這表示你自己要處理 DbContext 的 Dispose,這在 Console Application 較為常見,因為這種環境沒有 DI 工具來自動管理資源。相對來說,在 ASP.NET Core MVC 或 API 裡,常是用建構子注入 DbContext,對應的 context 由底層管理。如果看到 MVC Controller 裡用 using var context = …,那應是一個不常見的狀況
🌊 EF 會幫我們打理連線嗎? 答案是:看是誰創造這個連線。
您對 EF 說:
1 2 services.AddDbContext<MyDbContext>(options => options.UseSqlServer("Server=.;Database=MyDb;Trusted_Connection=True;" ));
這時是 EF 根據連線字串創造 SqlConnection,EF Core 會自動處理它的開啟、釋放、Dispose 等。
但您自己用 SqlConnection:
1 2 3 var conn = new SqlConnection("Server=.;Database=MyDb;Trusted_Connection=True;" );services.AddDbContext<MyDbContext>(options => options.UseSqlServer(conn));
這時責任就落在你身上了,EF 不會覺得這是它的錯,你就該自己把 conn.Dispose() 打理好。
🌊 結語 在應用程式的航道上,DbContext 就像船艙裡那位操舵的水手——他掌握著資料的流動節奏,也承載著交易的一致性與效能的平衡。選擇 Scoped、Transient,不是隨意撐起風帆,而是根據航線與氣候的判斷;在該共享的時候共享,在該獨立的時候獨立,才能讓應用不偏不倚,穩穩前行。