2013年8月15日 星期四

錯誤更正:有關 ASP.NET MVC 分層架構使用 Unity 的 DbContext 處理

2014-12-02 補充說明:
這一系列的文章並不適合初階及中階的開發人員,如果你是程式開發的初學者或是 ASP.NET MVC 初學者,甚至是開發經驗少於兩年的開發人員,請馬上離開此篇文章。

有網友在「ASP.NET MVC 專案分層架構 Part.6 - DI/IoC 使用 Unity.MVC」這篇文章提出了一個問題:

image

一開始我還以為是我的 DbContext 是不是有做了什麼特殊設定還是忘了做什麼設定才會出現這樣的情況,然後我把範例程式給找出來,按照網友所提出的情境做操作,結果還真的如他所說的,當我在執行的網頁上做新刪修查時都不會有問題,資料也會同步,但如果直接對資料做修改後,網頁上面的資料並不會跟著改變,在仔細看過原本的程式之後才恍然大悟,我犯了一個嚴重的低級錯誤才會導致這種資料不一致的狀況,這一篇就是來說明要如何改正這個問題。

有關這個錯誤的文章如下:

ASP.NET MVC 專案分層架構 Part.6 - DI/IoC 使用 Unity.MVC」「ASP.NET MVC 4 使用 Unity bootstrapper for ASP.NET MVC

可以按照此篇文章的作法來修正錯誤。

 


先看看原本的程式內容:

public static void RegisterTypes(IUnityContainer container)
{
    var dbContext = new TestDBEntities();
 
    // Repository
    container.RegisterType<IRepository<Categories>, GenericRepository<Categories>>(
        new InjectionConstructor(dbContext));
    
    container.RegisterType<IRepository<Products>, GenericRepository<Products>>(
        new InjectionConstructor(dbContext));
 
    // Service
    container.RegisterType<ICategoryService, CategoryService>();
    container.RegisterType<IProductService, ProductService>();
 
}

上面的 RegisterTypes Method 是位在 Bootstrapper.cs 這個檔案裡,先不對程式做修改,先模擬一下問題發生的情境。

先執行網頁,

image

接著直接到 SSMS 裡對 Category 裡去修改資料,

image

在 SSMS 修改玩資料之後回到網頁,重新整理網頁後卻發現到顯示的資料並不是修改後的內容,

image

進入到編輯頁面也一樣是顯示之前未修改的內容,

image

其實問題點就在於 Bootstrapper.cs 內的 RegisterTypes 這個 Method 是 static 的,

image

在靜態方法裡所建立的 DbContext 實例都將會維持同一個,直到該 Application 結束,所以我們既使有在 SSMS 裡去修改資料,程式執行中的 DbContext 並不會去讀取資料庫的資料,而是讀取在記憶體裡 DbContext 的資料,這是個嚴重的低級錯誤,不能也不該有這樣的錯誤產生,所以就必須要改正這個錯誤,怎麼改呢?

我們原本的做法是要把 DbContext 的建立從 Repository 給拿出去,但是用了 IoC 之後必須想辦法把 DbContext 建立並且注入到 Repository 裡,既然無法在靜態的 RegisterTypes 方法去建立 DbContext 實例,那我們可以另外建立一個類別與方法來做這件事情,然後在 RegisterType 裡增加這個類別的注入。

我這邊所建立的類別為 DbContextFactory

image

IDbContextFactory.cs

public interface IDbContextFactory
{
    DbContext GetDbContext();
}

DbContextFactory.cs (此實作會有問題,在下面有修正過的版本與說明)

public class DbContextFactory : IDbContextFactory
{
    private string _ConnectionString = string.Empty;
 
    public DbContextFactory(string connectionString)
    {
        this._ConnectionString = connectionString;
    }
 
    public DbContext GetDbContext()
    {
        return this.CreateDbContextInstance(this._ConnectionString);
    }
 
    private DbContext CreateDbContextInstance(string connectionString)
    {
        Type t = typeof(DbContext);
        return (DbContext)Activator.CreateInstance(t, connectionString);
    }
 
}

 

2013-09-08 更正

因為在寫「ASP.NET MVC 使用 Autofac」的時候就發現到 DbContextFactory 的 CreateDbContextInstance 方法有些不太對勁,因為每次仍然會建立一個新的 DbContext instance,所以 DbContextFactory 就做了修正,如下:

public class DbContextFactory : IDbContextFactory
{
    private string _ConnectionString = string.Empty;
 
    public DbContextFactory(string connectionString)
    {
        this._ConnectionString = connectionString;
    }
 
    private DbContext _dbContext;
    private DbContext dbContext
    {
        get
        {
            if (this._dbContext == null)
            {
                Type t = typeof(DbContext);
                this._dbContext =
                    (DbContext)Activator.CreateInstance(t, this._ConnectionString);
            }
            return _dbContext;
        }
    }
 
    public DbContext GetDbContext()
    {
        return this.dbContext;
    }
 
}

不過這還需要另外在 Unity 設定時再去調整 DbContextFactory 的 LifetimeManager,以確保 DbContextFactory instance 沒有重複,讓 DbContextFactory 不會建立重複的 DbContance instance。

 

再來就是修改 Bootstrapper.cs 的 RegisterTypes 內容,修改如下:

public static void RegisterTypes(IUnityContainer container)
{
    string connectionString = 
        WebConfigurationManager.ConnectionStrings["TestDBEntities"].ConnectionString;
 
    container.RegisterType<IDbContextFactory, DbContextFactory>(
        new HierarchicalLifetimeManager(),
        new InjectionConstructor(connectionString));
 
    // Repository
    container.RegisterType<IRepository<Categories>, GenericRepository<Categories>>();
    container.RegisterType<IRepository<Products>, GenericRepository<Products>>();
 
    // Service
    container.RegisterType<ICategoryService, CategoryService>();
    container.RegisterType<IProductService, ProductService>();
 
}

Uinty.MVC3 與 Unity.MVC4 為非官方的整合套件,使用 Enterprsie Library Unity 2.1 與 ASP.NET MVC 做整合,在 Unity 2.1 裡並沒有提供 PerHttpRequest 的 LifetimeManager 的支援,而 TransientLifetimeManager 是 Unity 在註冊類別時所使用的預設 LifetimeManager。

TransientLifetimeManager
http://msdn.microsoft.com/en-us/library/microsoft.practices.unity.transientlifetimemanager(v=pandp.20).aspx
「An LifetimeManager implementation that does nothing, thus ensuring that instances are created new every time.」

TransientLifetimeManager,這是 Unity 註冊類別時的預設值,如果沒有特別加註使用 TransientLifetimeManager 或其他 LifetimeManager 時就會預設使用,使用 TransientLifetimeManager 表示每次都會建立一個新的實例。

在註冊 IDbContextFactory 類別時要確保每次的 HttpRequest 只會建立一個 DbContrextFactory,而且建立的 DbContext 也必須在每次的 HttpRequest 不能夠重複,在這樣的前提下是無法使用 TransientLifetimeManager,不然會造成每個 Repository 都是使用個別的 DbContext,所以必須改使用另一個 LifetimeManager「HierarchicalLifetimeManager」。

HierarchicalLifetimeManager
http://msdn.microsoft.com/en-us/library/microsoft.practices.unity.hierarchicallifetimemanager(v=pandp.20).aspx

「A special lifetime manager which works like ContainerControlledLifetimeManager, except that in the presence of child containers, each child gets it's own instance of the object, instead of sharing one in the common parent.」

 

GenericRepository 也需要做修改,因為原本的 GenericRepository 建構式的參數要用到 DbContext,

image

現在要改成使用 IDbContextFactory,然後 GenericRepository 所使用的 DbContext 再由 IDbContextFactory 的 GetDbContext() 取得,修改後的程式如下,

image

原本在註冊各個 GenericRepository 時都會給 DbContext 這個參數到建構式,因為 GenericRepository 建構式改為使用 IDbContextFactory 後,在前面已經有處理 IDbContextFactory 的註冊,所以就可以不用再註冊 GenericRepository 時去給值了,Unity 會幫我們處理注入。

 

經過上面的修改之後就可以進行測試,不會再出現網頁顯示資料內容與資料庫不一致的情況。

 

上面是 Unity.MVC4(Unity.MVC3)的修改方式,而如果是使用 Unity bootstrapper for ASP.NET MVC 的話,因為所使用的 Unity 版本為 Unity Application Block 3,所以在使用上與 Unity.MVC4 or Unity.MVC3 有稍稍的些許不同(Unity.MVC4 or Unity.MVC3  用的版本為 Unity Application Block 2.1)。

 

Unity bootstrapper for ASP.NET MVC

前面的建立 IDbContextFactory 與 DbContextFactory、修改 GenericRepository 都是一樣的步驟與內容,不同的地方是在於 ~/App_Start/UnityCoinfig.cs 的 RegisterTypes 方法中,修改後的內容如下:

public static void RegisterTypes(IUnityContainer container)
{
    //資料庫連接字串由執行端環境來給予,而不寫死在 DbContextFactory 當中.
    string connectionString = WebConfigurationManager.ConnectionStrings["TestDBEntities"].ConnectionString;
 
    container.RegisterType<IDbContextFactory, DbContextFactory>(
        new PerRequestLifetimeManager(),
        new InjectionConstructor(connectionString));
 
    // Repository
    container.RegisterType<IRepository<Categories>, GenericRepository<Categories>>();
    container.RegisterType<IRepository<Products>, GenericRepository<Products>>();
 
    // Service
    container.RegisterType<ICategoryService, CategoryService>();
    container.RegisterType<IProductService, ProductService>();
}

在註冊 IDbContextFactory 類別時,LifetimeManage 是使用 PerRequestLifetimeManager 而非 HierarchicalLifetimeManager,這是 Unity Application Block 3 所新增的,這個 PerRequestLifetimeManager 主要是用在 HTTP Request 上,讓每一次的單一 HTTP Request 建立一個新的實例。

 


相關文章:

ASP.NET MVC 專案分層架構 Part.6 - DI/IoC 使用 Unity.MVC

ASP.NET MVC 4 使用 Unity bootstrapper for ASP.NET MVC

 

延伸閱讀:

MSDN - TransientLifetimeManager Class (Microsoft.Practices.Unity 2.1)
MSDN - HierarchicalLifetimeManager Class (Microsoft.Practices.Unity 2.1)

MSDN - PerRequestLifetimeManager Class (Microsoft.Practices.Unity 3.0)

Unity容器中的对象生存期管理 - Dennis Gao - 博客园

 

以上

34 則留言:

  1. 在Bootstrap 的 RegisterType 中
    container
    .RegisterType(
    new TransientLifetimeManager(),
    new InjectionConstructor(connectionString))
    .RegisterType(typeof(IRepository<>), typeof(GenericRepository<>));

    為什麼要再有這一段?? 這樣前面的Register不會被覆蓋掉嗎??
    實測的結果, 這一段加了惠在每一個repository有一個context, 但是這段拿掉就都在同一個 context 裡了....

    回覆刪除
    回覆
    1. 不好意思... 我沒有看到文末的code... 不過文中間與文末的 RegisterType 裡面程式碼不同... :)

      刪除
    2. 抱歉,你所提到的程式,是我誤植,目前已經修正,
      因為多出了那一段,所以會把同樣在前面已經註冊過的相同類別給覆蓋過去,所以才會造成 DbContext 重複建立的狀況,
      正確的做法是應該拿掉,深夜修改文章內容,難免精神注意力會不集中,還請見諒。

      另外你有說到文末的程式與文章中間的程式不同,那是因為用的 Unity 整合套件的不同,我在文章有特別註明喔!
      文末的程式是針對使用 Unity bootstrapper for ASP.NET MVC 的情況,而文中間所使用的 Unity.MVC4.

      Unity bootstrapper for ASP.NET MVC 是官方所製作的整合套件,使用的是 Unity 3.0,
      而 Unity.MVC3 或 unity.MVC4 是非官方的第三方整合套件,所使用的 Unity 版本為 2.1,
      Unity 3.0 與 2.1 是有差異的,尤其是在 LifetimeManager 與 RegisterType 方法上。

      我在文章中是有特別強調「以上是 Unity.MVC4(Unity.MVC3)的修改方式,而如果是使用 Unity bootstrapper for ASP.NET MVC 的話,因為所使用的 Unity 版本為 Unity Application Block 3,所以在使用上與 Unity.MVC4 or Unity.MVC3 有稍稍的些許不同(Unity.MVC4 or Unity.MVC3 用的版本為 Unity Application Block 2.1)。」

      刪除
  2. 您好,

    根據您的做法,我遇到了Additional information: The entity type XXX is not part of the model for the current context. 您有遇過嗎?

    回覆刪除
    回覆
    1. 抱歉,依據你所提供的訊息,我無法做有效的判斷或解讀,我未曾遇到過這樣得狀況。

      刪除
    2. 是使用Code First嗎?
      我照kevin大的程式練習時也出現,不過我是使用Code First發生的問題,我使用簡單工廠解決

      刪除
    3. 前後文看一看,這是有 edmx 的,所以不是 code first
      這篇更正的重點在於,不要亂用 static 修飾詞

      刪除
  3. 作者已經移除這則留言。

    回覆刪除
  4. 您好,照您的文章做到目前,我是用code first的方式去產生db,發生了別的問題,由於建立了資料庫,支援 'DbContext' 內容的模型已經變更。請考慮使用 Code First 移轉來更新資料庫 (http://go.microsoft.com/fwlink/?LinkId=238269)。

    請問您有遇過這種情形嗎??有可以解決的辦法嗎??

    回覆刪除
    回覆
    1. Hello, 你好
      就你所提供的訊息,我還是不清楚你遇到了什麼樣得狀況與錯誤,而且你有說發生了別的問題,這部份你並未說明,所以相當抱歉我無法給予任何幫助與建議。

      刪除
  5. kevin您好

    依您上述已修改過的DbContextFactory,
    我在"一個Service"中注入Products和Categories的Repository,發現個別都會Create DbContext Instance
    private DbContext _dbContext;
    private DbContext dbContext
    {
    get
    {
    if (this._dbContext == null)
    {
    Type t = typeof(DbContext);
    this._dbContext =
    (DbContext)Activator.CreateInstance(t, this._ConnectionString);
    }
    return _dbContext;
    }
    }
    每注入一個Repository時,this._dbContext都會是null,
    不知道這樣是不是正確的??
    (我是剛開始Run程式碼時,設下中斷點,按F11去看每個執行步驟的)

    回覆刪除
    回覆
    1. Hello,
      這個必須要看你的 Inject 設定還有你的 Repository 是如何處理的才能找出問題.

      刪除
    2. Hi Kevin,
      前陣子也遇到同樣的問題
      不過我是參考微軟文件解決(https://msdn.microsoft.com/en-us/data/jj592904.aspx)
      主要是在savachange時擷取DbUpdateConcurrencyException,並Reload Entries.
      catch (DbUpdateConcurrencyException ex)
      {
      ex.Entries.Single().Reload();
      }
      提供參考。

      刪除
  6. Hi Kevin,
    目前遇到的情況是Employee的資料在另一個資料庫,因此需要另一條連線來產生Context
    之前的註冊方式是在RegisterTypes內產生新的Entities,並一一的對應到要使用的class
    var Db = new Entities();
    var EmpDb = new EmpEntities();

    container.RegisterType, Repository>(
    new PerRequestLifetimeManager(), new InjectionConstructor(Db));
    container.RegisterType, Repository>(
    new PerRequestLifetimeManager(), new InjectionConstructor(EmpDb));

    想請問說,如果我需要用到多條連線時,我要怎麼把連線跟class對應呢?

    回覆刪除
    回覆
    1. Hello, 你好
      其實我抓不到你要問的點...
      因為感覺你好像要問多個 DBContext 的問題,但又好像不是問這個問題
      再加上你所提供的程式碼更讓我有好多疑惑(Google+ 留言板會過濾掉特殊 tag 喔)
      所以你是否重新整理你的問題呢?
      或者是你可以再將網頁疑下面一點,有個「提醒」的內容,你可以稍加留意。

      刪除
    2. Hi Kevin,
      抱歉沒有把問題詳細敘述,
      我已整理並截圖,
      並藉由[詢問與建議]與您討論

      刪除
    3. Hi Kevin,
      感謝回覆
      BTW, 用google chrome回覆似乎不太穩定
      我接到您的回信後就立刻回覆了
      但今天來看卻沒有,應該是送出失敗

      刪除
    4. 我應該已經回覆給你了,我有提供參考資料
      參考以下的文章和 Source Code
      Unit Of Work With Multiple DBContexts | Friedtip  
      https://friedtip.wordpress.com/2014/02/02/unit-of-work-with-multiple-dbcontexts/
      https://github.com/raghav-rosberg/UnitOfWorkWithMultipleDBContext

      刪除
    5. 應該不是等我給你一個完整的實做範例吧
      其實你所做的已經是可以,所以回覆給你的訊息最後有提供一些建議
      另外也提供了參考作法,接下來的實做就是你的事情囉

      刪除
  7. 您好,目前在學習關於依賴注入的部分,不曉得您有沒有遇過使用資料庫儲存多國語系文字取代RESX資源檔案?
    目前按照前面將資料存取分割,有一LocaleStringService類別實作ILocaleStringService介面,另有一自訂CustomWebViewPage類別繼承自WebViewPage類別,類別內有一方法:GetResourceString(),使用LocaleStringService取得資料。
    目前的狀況是不知如何在WebViewPage內從外部注入ILocaleStringService,不知道是否有遇過相關案例?謝謝。

    回覆刪除
    回覆
    1. 沒有
      型別註冊的地方應該有方法可以處理吧,看你的建構式注入還是屬性注入或是用 Resolve 的方式,應該是有辦法,
      不過我不知道你是怎麼實做你的這些類別,所以我也只能提供這些沒什麼幫助的建議

      刪除
    2. 好的,我再嘗試看看,謝謝。

      刪除
  8. 您好,想請問是否遇到在任一Service更改/新增資料時,若引發模型驗證失敗(如外部索引不正確,長度不符...)而導致SaveChange()失敗,此時使用一個紀錄日誌的LogService寫入Exception內容,但因SaveChange()失敗而導致LogService內的Create也會失敗(同一個DbContext)而無法寫入,是否該使用DbContext的Detech?謝謝

    回覆刪除
    回覆
    1. 你好,我不知道你的 logService 是怎麼處理 exception 記錄,看起來也好像是寫到資料庫裡,log 處理建議應該另外獨立,而不是使用相同的 dbcontext,所以你的狀況與這篇文章的主旨和內容八干子打不到關係,而且所謂的 service 泛指服務或商業邏輯處理,不應該與資料庫有直接相依

      刪除
    2. 作者已經移除這則留言。

      刪除
    3. 是的,資料庫裡有個Log資料表,而LogService跟其他一樣,都有一個私有的Repository。
      LogService處理Exception只是寫入字串沒有什麼特別..看來給他使用的DbContext必須要獨立了。
      另外會在此發問,因為在之前都是在Service內直接new instance,而在此則是透過外部注入,變成不重複的DbContext,只要SaveChange()失敗,若再次呼叫肯定無法再次寫入。
      謝謝

      刪除
  9. 作者已經移除這則留言。

    回覆刪除
  10. 你好,我參考了你的DbContextFactory,發現第一次運行可以,但是第二次運行就會出現
    Message":"An error has occurred.","ExceptionMessage":"The operation cannot be completed because the DbContext has been disposed."
    查看了你的 http://kevintsengtw.blogspot.hk/2012/10/aspnet-mvc-part2-repository.html
    GenericRepository.cs
    protected virtual void Dispose(bool disposing)
    {
    if (disposing)
    {
    if (this._context != null)
    {
    this._context.Dispose();
    this._context = null;
    }
    }
    }
    以及本文的
    public GenericRepository(IDbContextFactory factory)內的
    {this._context = factory.GetDbContext();}

    GenericRepository.dispose的運行結果會使得,
    GenericRepository._context = null,
    GenericRepository._context dispose,
    但是,factory.context != null, 但是factory.context 已經dispose。

    在factory內無法重新建立
    if (this._dbContext == null)
    {
    Type t = typeof(DbContext);
    this._dbContext =
    (DbContext)Activator.CreateInstance(t, this._ConnectionString);
    }

    所以第一次正常,而第二次出error.

    回覆刪除
  11. 我是將你的分層架構理論是應用在了webapi上而不是mvc上,

    我在WebApiconfig.cs內
    public static class WebApiConfig
    {
    public static void Register(HttpConfiguration config)
    {
    var container = new UnityContainer();
    container.RegisterType(new HierarchicalLifetimeManager(),new InjectionConstructor(connectionString));
    ....
    }
    }

    但是我模擬發送webapi的get時候,無論我get多少次,發現以下建構只會運行一次,就是第一次get的時候。
    public DbContextFactory(string connectionString)
    {
    this._ConnectionString = connectionString;
    }
    我以為會每一次request都重新建立一個dbcontextfactory實體,但是沒有。這樣可能是導致第一次運行可以,但是第二次運行就有問題。

    回覆刪除
  12. 最終解決了,在webapi用HierarchicalLifetimeManager滿足一個request建立一次dbcontext的辦法,就是要寫一個UnityResolver(參考如下),這樣加上HierarchicalLifetimeManager才能搞定!
    因為是一個request建立一次dbcontext,這樣就不會出現第一次運行可以,但是第二次運行就有問題。

    參考自https://docs.microsoft.com/en-us/aspnet/web-api/overview/advanced/dependency-injection
    using Microsoft.Practices.Unity;
    using System;
    using System.Collections.Generic;
    using System.Web.Http.Dependencies;

    public class UnityResolver : IDependencyResolver
    {
    ......

    public IDependencyScope BeginScope()
    {
    var child = container.CreateChildContainer();
    return new UnityResolver(child);
    }

    public void Dispose()
    {
    Dispose(true);
    }

    protected virtual void Dispose(bool disposing)
    {
    container.Dispose();
    }
    }

    回覆刪除
    回覆
    1. 剛剛再看了一次Nuget unity下的自動建立的UnityWebApiActivator file,發現了以下註解
      // Use UnityHierarchicalDependencyResolver if you want to use a new child container for each IHttpController resolution.
      //var resolver = new UnityHierarchicalDependencyResolver(UnityConfig.GetConfiguredContainer());
      var resolver = new UnityDependencyResolver(UnityConfig.GetConfiguredContainer());

      我再試驗一下,用它的UnityHierarchicalDependencyResolver,也可以做到每一個request只建立一次dbcontext。

      該Nuget版本號 unity.AspNet.WebApi 4.0.1, Unitiy 4.0.1。

      刪除

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。