在 ASP.NET MVC 應用程式中使用 Entity Framework 時,DbContext(或 ObjectContext)物件的壽命,一般是建議與 HTTP request 「同生共死」.... Why?

在使用 DbContext 時,許多 C# 範例程式是用 using 來確保資源盡快回收,例如:

using (var db = new NorthwindEntites())
{
    // 操作 db 的 entity 集合
}

如果是 ASP.NET MVC 應用程式,在 Controller (或其他類別)的各個方法中使用上述寫法,好處是可以讓資料庫連線盡快釋放(盡量提高應用程式的延展性),缺點則是 context 物件很短命(就只活在當下那個 method 中),無法享受 DbContext 提供的一些功能,例如快取、跨方法呼叫的變更追蹤、交易管理等等。

因此,一般會建議讓 context 物件與 HTTP request 「同生共死」,也就是 one context per request:在 request 開始時建立 DbContext 物件,並且在 request 即將結束時摧毀 context 物件(精確來說,並非真的摧毀了,這裡指的是呼叫 Dispose 方法)。

一個 Request 配一個 Context

由於 ASP.NET MVC 的  Controller 物件的壽命大約等同一個 request 的壽命,所以一種簡單的作法是在我們的 Controller 類別中建立和摧毀 context 物件。參考底下的程式片段:

public class CustomerController : Controller
{
    private NorthwindEntities db = new NorthwindEntities();

    protected override void Dispose(bool disposing)
    {
        db.Dispose();
        base.Dispose(disposing);
    }
    
    // 其餘 Action 方法略過
}

這個 CustomerController 類別的程式碼,包括建立 context 物件(NorthwindEntites)和 Dispose 方法,全都是 Visual Studio 2012 的 Add Controller 功能幫我產生的(增刪改查的  Action 方法不是重點,所以沒有貼出來)。參考下圖:



如果所有的 Controller 都會用到同一個 DbContext 物件,則可以寫一個 Controller 基礎類別,例如 MyControllerBase,並將 DbContext 變數宣告在 MyControllerBase 類別中,成為其物件成員。

除了讓 context 成為 Controller 物件的成員,還有一種作法是在 Application 物件的 BeginRequest 事件中建立 context 並存入目前的 HttpContext 的 Items 集合,並且在 EndRequest 事件中摧毀 context 物件,例如:

protected void Application_BeginRequest()
{
    var db = new Models.NorthwindEntities();
    HttpContext.Current.Items["Northwind"] = db;
}

protected void Application_EndRequest()
{
    var db = HttpContext.Current.Items["Northwind"] as Models.NorthwindEntities;
    if (db != null)
    {
        db.Dispose();
        db = null;
    }
}

在 Controller 中取出 context 物件時,可以這樣寫:

public ActionResult Index()
{
    var db = this.HttpContext.Items["Northwind"] as NorthwindEntities;
    return View(db.Customers.ToList());
}

如此一來,商業邏輯層中的所有 Business Objects 也可以共用同一個的 DbContext 物件,並享有跨 BOs 交易管理的便利。

唯一要注意的是,如果你的 ASP.NET 應用程式會在一個 request 生命週期中建立多條執行緒來分頭(並行)執行多個資料操作,這就還是會有問題,得額外寫程式碼來處理 synchronization ,或另尋他法。

越久越好?

如果想要讓 context 物件活得更久,也許有人會想到在 Global.asax 的 Application_Start 事件中建立 context 物件,並將它存入 Application 集合中,讓所有 requests 共享--千萬別這麼做!這等於是用一個 entity context 去管理所有線上使用者的資料查詢和異動,很容易造成資料錯亂。就算是純粹 read only、不會異動資料的 ASP.NET 應用程式,最好也還是別這麼做,因為同一個 DbContext 物件由所有使用者、所有 requests 共享,只要某一次 request 修改了 context 物件的某個屬性,就會影響整個應用程式的行為,這樣實在太沒有彈性。

放在 Session 裡面呢?

一樣不好。把 DbContext 物件存入目前的 Session 中,雖然可以隔離不同使用者之間的資料操作,可是卻難保同一個使用者開啟多個頁面或同時進行多項操作時產生的交互作用。而且 context 會快取 entity 物件,這表示一直到使用者登出或 session timeout 之前,這些快取的 entities 都會一直存在,占用伺服器的記憶體空間。

小結

所以,對 ASP.NET 應用程式來說,DbContext(或 ObjectContext)的生命週期控制模式通常就是底下兩種:
  1. One context per method  
  2. One context per request (一般建議採用)

也許你會撰寫另一個抽象層,例如 Repository 或 Service 類別,用來把 DbContext 包在裡面,以免 Controller 直接依賴 Entity Framework。這種情形,上述原則依然適用,只是你的 Repository 或 Service 類別也必須實作 IDisposable,並且在 Dispose 方法中處置 context 物件。

如果是 Windows Forms 或 WPF 應用程式,則可以採用 one context per form/window 的方式。

延伸閱讀