Alan Tsai 的學習筆記


學而不思則罔,思而不學則殆,不思不學則“網貸” 為現任微軟最有價值專家 (MVP)、微軟認證講師 (MCT) 、Blogger、Youtuber:記錄軟體開發的點點滴滴 著重於微軟技術、C#、ASP .NET、Azure、DevOps、Docker、AI、Chatbot、Data Science

[iThome 第七屆鐵人賽 09] 用Repository Pattern抽離對Entity Framework的依賴

在上一篇介紹完了如何讓ViewModel和Entity之間的轉換透過AutoMapper變的更簡單,然後透過框架讓設定ViewModel和Entity之間的對應關係變的容易。

在這一篇,將會看Data Access Layer (DAL)的部份,也就是儲存資料層的部份。

Data Acce Layer (DAL)

不管任何大小的軟體,通常都會需要儲存資料。而這個儲存資料最常見的就是儲存到資料庫裡面。以Asp .Net Mvc來說,最常見的就是透過Entity Framework這個ORM的技術來儲存到實體的資料庫,例如MS Sql,Oracle等。

而如果以Entity Framework來講,它所在的角色就屬於DAL層。

以一般比較常見的三層是架構,大概就會如下圖:

3 Layer Architecture

一般的三層式架構 - 裡面白色的表示Mvc裡面比和3層式架構較相近的對應

用Entity Framework做DAL的問題

其實這個問題不只有使用Entity Framework會,任何的DAL實作都有這個問題。

舉個例子來說,假設今天我們用的是Entity Framework作為DAL層,如果開發到一半,對方突然要求不要使用Entity Framework,而是要改成傳統的ADO .Net作為DAL怎麼辦?

或者說如果要做單元測試(Unit Testing),肯定不希望在跑的時候還是連資料庫,而是希望連一些假資料,這時候怎麼辦?

解決方法其實很簡單,就是把實際的DAL在抽一層出來,就有了所謂的Repository Pattern。

Repository Pattern

一般來說,在寫Mvc裡面最長看到的Pattern就是Repository Pattern。

這個Pattern概念非常簡單,Repository其實有儲存庫的意思,所以這個Pattern的意思是,把實際的DAL層透過所謂的Repository封裝之後,從外面的角度來說是和Repository 溝通來取得資料,至於Repository的資料來源是那裡,就不管了。

文字敘述可能有些抽象,我們來看一張圖:

image

Repository的好處:圖片來源: Implementing the Repository and Unit of Work Patterns in an ASP.NET MVC Application (9 of 10)

可以看到,左邊的圖是一般直接用Entity Framework,而右邊使用了Repository Pattern + Unit of Work(下一篇介紹),因此可以再真實的環境用Entity Framework連資料庫,然後在單元測試的時候,連假資料。

這樣就可以把實際的資料來源抽象化,提供更大的彈性。

定義Repository的interface

Repository有很多種實作方法,共通的來說,1個Repository代表一個DB 裡面的 Table。通常的做法有兩種,一種是每一個Table就一個interface,另外一種是以Generic的方式,寫一種通用型的Repository。

我這邊會介紹的是通用型的Repository。

首先,一般的DB 動作有所謂的CRUD,因此我們的interface就至少會包含這幾種動作。同時,我們還會有一個SaveChanges的方法,代表把目前有記錄的動作執行。

/// <summary>
/// 代表一個Repository的interface。
/// </summary>
/// <typeparam name="T">任意model的class</typeparam>
public interface IRepository<T>
{
    /// <summary>
    /// 新增一筆資料。
    /// </summary>
    /// <param name="entity">要新增到的Entity</param>
    void Create(T entity);

    /// <summary>
    /// 取得第一筆符合條件的內容。如果符合條件有多筆,也只取得第一筆。
    /// </summary>
    /// <param name="predicate">要取得的Where條件。</param>
    /// <returns>取得第一筆符合條件的內容。</returns>
    T Read(Expression<Func<T, bool>> predicate);
    
    /// <summary>
    /// 取得Entity全部筆數的IQueryable。
    /// </summary>
    /// <returns>Entity全部筆數的IQueryable。</returns>
    IQueryable<T> Reads();
    
    /// <summary>
    /// 更新一筆資料的內容。
    /// </summary>
    /// <param name="entity">要更新的內容</param>
    void Update(T entity);

    /// <summary>
    /// 刪除一筆資料內容。
    /// </summary>
    /// <param name="entity">要被刪除的Entity。</param>
    void Delete(T entity);

    /// <summary>
    /// 儲存異動。
    /// </summary>
    void SaveChanges();
}

定義好了之後,我們就來看實作。


Entity Framework的Repository interface實作


再來我們就定義一個EF版本的Repository實作:

/// <summary>
/// 實作Entity Framework Generic Repository 的 Class。
/// </summary>
/// <typeparam name="TEntity">EF Model 裡面的Type</typeparam>
public class EFGenericRepository<TEntity> : IRepository<TEntity>
    where TEntity : class
{
    private DbContext Context { get; set; }

    /// <summary>
    /// 建構EF一個Entity的Repository,需傳入此Entity的Context。
    /// </summary>
    /// <param name="inContext">Entity所在的Context</param>
    public EFGenericRepository(DbContext inContext)
    {
        Context = inContext;
    }

    /// <summary>
    /// 新增一筆資料到資料庫。
    /// </summary>
    /// <param name="entity">要新增到資料的庫的Entity</param>
    public void Create(TEntity entity)
    {
        Context.Set<TEntity>().Add(entity);
    }

    /// <summary>
    /// 取得第一筆符合條件的內容。如果符合條件有多筆,也只取得第一筆。
    /// </summary>
    /// <param name="predicate">要取得的Where條件。</param>
    /// <returns>取得第一筆符合條件的內容。</returns>
    public TEntity Read(Expression<Func<TEntity, bool>> predicate)
    {
        return Context.Set<TEntity>().Where(predicate).FirstOrDefault();
    }

    /// <summary>
    /// 取得Entity全部筆數的IQueryable。
    /// </summary>
    /// <returns>Entity全部筆數的IQueryable。</returns>
    public IQueryable<TEntity> Reads()
    {
        return Context.Set<TEntity>().AsQueryable();
    }

    /// <summary>
    /// 更新一筆Entity內容。
    /// </summary>
    /// <param name="entity">要更新的內容</param>
    public void Update(TEntity entity)
    {
        Context.Entry<TEntity>(entity).State = EntityState.Modified;
    }

    /// <summary>
    /// 更新一筆Entity的內容。只更新有指定的Property。
    /// </summary>
    /// <param name="entity">要更新的內容。</param>
    /// <param name="updateProperties">需要更新的欄位。</param>
    public void Update(TEntity entity, Expression<Func<TEntity, object>>[] updateProperties)
    {
        Context.Configuration.ValidateOnSaveEnabled = false;

        Context.Entry<TEntity>(entity).State = EntityState.Unchanged;

        if (updateProperties != null)
        {
            foreach (var property in updateProperties)
            {
                Context.Entry<TEntity>(entity).Property(property).IsModified = true;
            }
        }
    }

    /// <summary>
    /// 刪除一筆資料內容。
    /// </summary>
    /// <param name="entity">要被刪除的Entity。</param>
    public void Delete(TEntity entity)
    {
        Context.Entry<TEntity>(entity).State = EntityState.Deleted;
    }

    /// <summary>
    /// 儲存異動。
    /// </summary>
    public void SaveChanges()
    {
        Context.SaveChanges();

        // 因為Update 單一model需要先關掉validation,因此重新打開
        if (Context.Configuration.ValidateOnSaveEnabled == false)
        {
            Context.Configuration.ValidateOnSaveEnabled = true;
        }
    }
}

使用Entity Framework的Repository


基本上,我們也是一樣可以用Autofac做注入,不過因為最終我們會用到Unit of Work,所以這邊我就先不注入,純粹展示使用方法。

下面範例基本上會用註解顯示之前用Entity Framework Context的用法(db是代表context),可以做一個用Repository和用原生EF Context的比較。其實用起來差不多。

注入的部分


首先看一下我們如何注入Repository進來:

public class PostsController : Controller
{
	// 預設Scaffolding出來的Entity Context
    // private BlogEntities db = new BlogEntities();

    private IRepository<Post> repo;

    public PostsController()
        : this(new EFGenericRepository<Post>(new BlogEntities()))
    {
    }

    public PostsController(IRepository<Post> inRepo)
    {
        repo = inRepo;
    }
	....

Create和Read(Index)用法比對

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(Post post)
{
...    
    // db.Post.Add(post);
    // db.SaveChanges();
    repo.Create(post);
    repo.SaveChanges();
    return RedirectToAction("Index");
....
}

public ActionResult Index()
{
    //return View(db.Post.ToList());
    return View(repo.Reads().ToList());
}

public ActionResult Details(int? id)
{
	...
    // Post post = db.Post.Find(id);
    Post post = repo.Read(x =&gt; x.Id == id);
	...
}

Update和Delete用法比對

...
public ActionResult Edit(EditViewModel post)
{
	...    
	// db.Entry(postEntity).State = EntityState.Modified;
	// db.SaveChanges();
	repo.Update(postEntity);
	repo.SaveChanges();
	...
}

public ActionResult DeleteConfirmed(int id)
{
	...
	
    //db.Post.Remove(post);
    //db.SaveChanges();
    repo.Delete(post);
    repo.SaveChanges();
	
	...  
}

看過這幾個常見的比對,應該發現和之前(直接使用EF的Context)用法差不多,但是對未來的彈性大幅度提到,下面將舉一個簡單的例子。


模擬要做單元測試,把Repository實作抽換


因為我們的Controller會要的是IRepository,因此當要做單元測試的時候,我們可以給另外一個實作,例如:

public class FakeRepository<Post> 
        : IRepository<Post>
        
{
    private List<Post> data;

    
    public FakeRepository()
    {
        data = new List<Post>();

        data.Add(new Post()
        {
            Id = 1,
            CreateDateTime = DateTime.Now,
            LastModifyDateTime = DateTime.Now.AddDays(-10),
            PostContent = "abc",
            Title = "123"
        });
    }

    public IQueryable<Post> Reads()
    {
        return data.AsQueryable();
    }
	
	// ...其他方法

那做單元測試就簡單並且減少時間(不需要和DB溝通),因為資料是我們灌好的假資料,可以掌控內容。


結語


希望透過這一篇,對於爲什麽和如何使用Repository Pattern會有些概念,不過Repository本身還是有些問題。


一個DB肯定不止一個Table,而Repository只代表了一個Table,那如果我同時要用2個Table以上怎麼辦?這個時候Unit of work就進來了。


Unit of work就留到下一篇在講了。


如果文章對您有幫助,就請我喝杯飲料吧
街口支付QR Code
街口支付QR Code
台灣 Pay QR Code
台灣 Pay QR Code
Line Pay 一卡通 QR Code
Line Pay 一卡通 QR Code
街口支付QR Code
支付寶QR Code
街口支付QR Code
微信支付QR Code
comments powered by Disqus