Entity Framework DbContext 物件的生命週期

在 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 的方式。

延伸閱讀

10 則留言:

  1. 您好,感謝您熱心的文章,讓我理解到了Entity Framework DbContext 一些之前沒注意到的這個地方,這些觀念很有幫助,更加了解了生命週期的控管~

    回覆刪除
  2. 請問...用One context per request方式
    如果頁面有30個圖檔...他不就會產生30次 DbContext ??

    這樣效態比較好嗎???

    回覆刪除
  3. 預設情況下,像圖檔這種靜態資源是不會進入 ASP.NET routing 流程的,亦即不會建立 controller 實體。但如果你選擇在 Application_BeginRequest() 裡面建立 context,這可能就得自行判斷目前的 request 所要求的資源是否為靜態資源,以便決定是否要建立 context。判斷是否為靜態資源的寫法可參考這篇 QA: http://stackoverflow.com/questions/6677214/check-for-a-static-file-during-application-beginrequest

    回覆刪除
  4. 請問一下,如果我的專案分成repostiory和service,然後用web api來呼叫service的話,那適合把Dispose放在web api嗎??

    回覆刪除
  5. 就您的描述來看,是 web api 呼叫 service 呼叫 repository 這樣的順序吧。看你的 DbContext 是在哪邊建立的,基本上就由那一層來做 Dispose。例如我的 web api 也是呼叫 service layer,於是我把建立和釋放 DbContext 的工作都放在 service 物件裡;service 物件釋放時,DbContext 也隨之釋放。

    回覆刪除
    回覆
    1. 因為我把所有LINQ的操作都寫在repository,然後想在service做savechange,所以才會問此問題,有在參考unit of work的模式,但因為我的架構跟別人的架構有不同,所以還在觀看怎麼把觀念實作帶來我這邊......原以為在API放掉資源能達到我在service層能管理交易repository..........順便一提,很期待您的新書,必買

      刪除
  6. 讀完第一章了,雖然深知不要過度設計,我原本也想說較複雜的查詢才包在repository層,但為了單元測試,便不得已要把所有LINQ的指令都寫在repository,以方便針對service層做測試,所以才會遇到要在service做交易的困難點,但如果要做到單元測試,必須得做介面,不做介面則便要使用shim的方式來模擬,而且只能針對function才能做單元測試.........

    我目前的專案因為DB又要依SESSION儲存的IP動態切換,又因為使用了autofac,造成我要使用unit of work很有難度.......一度想放棄乾脆別做單元測試算了.......

    回覆刪除
  7. 聽起來你一個人就要寫不少東西....實際上每個人碰到的狀況都不太一樣,工作負荷、時程壓力、公司環境的支持....等,很多因素。加油! (小聲地說....我很少寫單元測試)

    回覆刪除

技術提供:Blogger.