2012年11月7日 星期三

ASP.NET MVC 專案分層架構 Part.3 - 個別 Repository 的資料存取操作

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

離上一篇「ASP.NET MVC 專案分層架構 Part.2 抽出 Repository 裡相同的部份」的發佈也隔了一段時間,我們繼續這一個主題,上一篇的內容是把原本分成個別 Repository 中的相同部分給抽出來,並且應用泛行的特性而另外建立一個 GenericRepository 來處理這些基本的資料存取操作,但非每一個類別的資料操作都是相同的,不是建立一個 GenericRepository 就可以滿足所有的需求,當各個類別有不同的資料存取需求時,應該怎麼做呢?

 


先回顧在上一篇的修改之後,專案的 Models 這一部分的狀態,

image

原本有建立的 ICategoryRepository, IProductCategoryRepository 以及分別實作介面的 CategoryRepository, ProductRepository 都已經被移除了,因為上一篇說過要抽出共用的部份(重複很多次,大家也都聽煩了),但是現在會有以下這些情況的出現:

    • 想要依據不同分類的產品資料
    • 想以明確的主鍵值來取得資料(直接用 ID 而不要用 Linq Expression)

以第一項來說,原本的 GenericRepository 就已經無法滿足,或許有些人會說,GenericRepository 的 GetAll() 方法不是傳回 IQueryable<T> 的結果嗎?那在 Controller 內就可以再以這個 IQueryable<T> 去做不同分類的篩選不就好了嗎?

這樣講也沒錯,但如果說篩選的條件很多或是複雜,這樣資料篩選的操作放在應當是控制流程的 Controller 之中會恰當嗎?

所以我們還是會把 ICategoryRepository, IProductRepository 再建立起來並且繼承 IRepository<T>,現在各類別的介面都重新建立了,那麼相關類別就必須要再一次實作,但這一次我們就不需要去實作 CRUD 以及其他基本的操作了,因為這些都已經在之前所建立的 GenericRepository 之中,而 CategoryRepository, ProductRepository 除了繼承實作各自的方法,因為介面有繼承 IRepository<T>,所以繼承 GenericRepository,這樣在各類別的Repository 就一樣有 CRUD 與其他基本操作的功能,。

 

ICategoryRepository

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace Mvc_Repository.Models.Interface
{
    public interface ICategoryRepository : IRepository<Categories>
    {
        Categories GetByID(int categoryID);
    }
}

CategoryRepository

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Mvc_Repository.Models.Interface;
 
namespace Mvc_Repository.Models.Repository
{
    public class CategoryRepository : GenericRepository<Categories>, ICategoryRepository
    {
        /// <summary>
        /// Gets the by ID.
        /// </summary>
        /// <param name="categoryID">The category ID.</param>
        /// <returns></returns>
        public Categories GetByID(int categoryID)
        {
            return this.Get(x => x.CategoryID == categoryID);
        }
    }
}

 

IProductRepository

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace Mvc_Repository.Models.Interface
{
    public interface IProductRepository : IRepository<Products>
    {
        Products GetByID(int productID);
 
        IEnumerable<Products> GetByCateogy(int categoryID);
    }
}

ProductRepository

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Mvc_Repository.Models.Interface;
 
namespace Mvc_Repository.Models.Repository
{
    public class ProductRepository : GenericRepository<Products>, IProductRepository
    {
        /// <summary>
        /// Gets the by ID.
        /// </summary>
        /// <param name="productID">The product ID.</param>
        /// <returns></returns>
        /// <exception cref="System.NotImplementedException"></exception>
        public Products GetByID(int productID)
        {
            return this.Get(x => x.ProductID == productID);
        }
 
        /// <summary>
        /// Gets the by cateogy.
        /// </summary>
        /// <param name="categoryID">The category ID.</param>
        /// <returns></returns>
        /// <exception cref="System.NotImplementedException"></exception>
        public IEnumerable<Products> GetByCateogy(int categoryID)
        {
            return this.GetAll().Where(x => x.CategoryID == categoryID);
        }
    }
}

 

這時候再來看看現在的 Models 的現況,

image

接下來就是把 Controller 中使用 GenericRepository<T> 的地方修改為使用各類別的 Repository,

 

CategoryController

using System.Data;
using System.Linq;
using System.Web.Mvc;
using Mvc_Repository.Models;
using Mvc_Repository.Models.Interface;
using Mvc_Repository.Models.Repository;
 
namespace Mvc_Repository.Controllers
{
    public class CategoryController : Controller
    {
        private ICategoryRepository categoryRepository;
 
        public CategoryController()
        {
            this.categoryRepository = new CategoryRepository();
        }
 
 
        public ActionResult Index()
        {
            var categories = this.categoryRepository.GetAll()
                .OrderByDescending(x => x.CategoryID)
                .ToList();
 
            return View(categories);
        }
 
        //=========================================================================================
 
        public ActionResult Details(int? id)
        {
            if (!id.HasValue)
            {
                return RedirectToAction("index");
            }
            else
            {
                var category = this.categoryRepository.GetByID(id.Value);
                return View(category);
 
            }
        }
 
        //=========================================================================================
 
        public ActionResult Create()
        {
            return View();
        }
 
        [HttpPost]
        public ActionResult Create(Categories category)
        {
            if (category != null && ModelState.IsValid)
            {
                this.categoryRepository.Create(category);
                return RedirectToAction("index");
            }
            else
            {
                return View(category);
            }
        }
 
        //=========================================================================================
 
        public ActionResult Edit(int? id)
        {
            if (!id.HasValue)
            {
                return RedirectToAction("index");
            }
            else
            {
                var category = this.categoryRepository.GetByID(id.Value);
                return View(category);
            }
        }
 
        [HttpPost]
        public ActionResult Edit(Categories category)
        {
            if (category != null && ModelState.IsValid)
            {
                this.categoryRepository.Update(category);
                return View(category);
            }
            else
            {
                return RedirectToAction("index");
            }
        }
 
        //=========================================================================================
 
        public ActionResult Delete(int? id)
        {
            if (!id.HasValue)
            {
                return RedirectToAction("index");
            }
            else
            {
                var category = this.categoryRepository.GetByID(id.Value);
                return View(category);
            }
        }
 
        [HttpPost, ActionName("Delete")]
        public ActionResult DeleteConfirmed(int id)
        {
            try
            {
                var category = this.categoryRepository.GetByID(id);
                this.categoryRepository.Delete(category);
            }
            catch (DataException)
            {
                return RedirectToAction("Delete", new { id = id });
            }
            return RedirectToAction("index");
        }
 
    }
}

ProductController

using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using Mvc_Repository.Models;
using Mvc_Repository.Models.Interface;
using Mvc_Repository.Models.Repository;
 
namespace Mvc_Repository.Controllers
{
    public class ProductController : Controller
    {
        private IProductRepository productRepository;
        private ICategoryRepository categoryRepository;
 
        public IEnumerable<Categories> Categories
        {
            get
            {
                return categoryRepository.GetAll();
            }
        }
 
        public ProductController()
        {
            this.productRepository = new ProductRepository();
            this.categoryRepository = new CategoryRepository();
        }
 
        public ActionResult Index()
        {
            var products = productRepository.GetAll()
                .OrderByDescending(x => x.ProductID)
                .ToList();
 
            return View(products);
        }
 
        //=========================================================================================
 
        public ActionResult Details(int id = 0)
        {
            Products product = productRepository.GetByID(id);
            if (product == null)
            {
                return HttpNotFound();
            }
            return View(product);
        }
 
        //=========================================================================================
 
        public ActionResult Create()
        {
            ViewBag.CategoryID = new SelectList(this.Categories, "CategoryID", "CategoryName");
            return View();
        }
 
        [HttpPost]
        public ActionResult Create(Products products)
        {
            if (ModelState.IsValid)
            {
                this.productRepository.Create(products);
                return RedirectToAction("Index");
            }
 
            ViewBag.CategoryID = new SelectList(this.Categories, "CategoryID", "CategoryName", products.CategoryID);
            return View(products);
        }
 
        //=========================================================================================
 
        public ActionResult Edit(int id = 0)
        {
            Products product = this.productRepository.GetByID(id);
            if (product == null)
            {
                return HttpNotFound();
            }
            ViewBag.CategoryID = new SelectList(this.Categories, "CategoryID", "CategoryName", product.CategoryID);
            return View(product);
        }
 
        [HttpPost]
        public ActionResult Edit(Products products)
        {
            if (ModelState.IsValid)
            {
                this.productRepository.Update(products);
                return RedirectToAction("Index");
            }
            ViewBag.CategoryID = new SelectList(this.Categories, "CategoryID", "CategoryName", products.CategoryID);
            return View(products);
        }
 
        //=========================================================================================
 
        public ActionResult Delete(int id = 0)
        {
            Products product = this.productRepository.GetByID(id);
            if (product == null)
            {
                return HttpNotFound();
            }
            return View(product);
        }
 
        [HttpPost, ActionName("Delete")]
        public ActionResult DeleteConfirmed(int id)
        {
            Products product = this.productRepository.GetByID(id);
            this.productRepository.Delete(product);
            return RedirectToAction("Index");
        }
 
    }
}

 

原本的 Product 列表都是把產品給全部列出來,一般的方式都會依照分類來顯露不同分類下的產品列表,我們在 IProductRepository 新增加了一個 GetByCategory() 的方法定義,所以 ProductController 這邊也就做了修改,

修改後的 ProductController

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using Mvc_Repository.Models;
using Mvc_Repository.Models.Interface;
using Mvc_Repository.Models.Repository;
 
namespace Mvc_Repository.Controllers
{
    public class ProductController : Controller
    {
        private IProductRepository productRepository;
        private ICategoryRepository categoryRepository;
 
        public IEnumerable<Categories> Categories
        {
            get
            {
                return categoryRepository.GetAll();
            }
        }
 
        public ProductController()
        {
            this.productRepository = new ProductRepository();
            this.categoryRepository = new CategoryRepository();
        }
 
        public ActionResult Index(string category = "all")
        {
            int categoryID = 1;
 
            ViewBag.CategorySelectList = int.TryParse(category, out categoryID)
                ? this.CategorySelectList(categoryID.ToString())
                : this.CategorySelectList("all");
 
            var result = category.Equals("all", StringComparison.OrdinalIgnoreCase)
                ? productRepository.GetAll()
                : productRepository.GetByCateogy(categoryID);
 
            var products = result.OrderByDescending(x => x.ProductID).ToList();
 
            ViewBag.Category = category;
 
            return View(products);
        }
 
        [HttpPost]
        public ActionResult ProductsOfCategory(string category)
        {
            return RedirectToAction("Index", new { category = category });
        } 
 
        /// <summary>
        /// CategorySelectList
        /// </summary>
        /// <param name="selectedValue">The selected value.</param>
        /// <returns></returns>
        public List<SelectListItem> CategorySelectList(string selectedValue = "all")
        {
            List<SelectListItem> items = new List<SelectListItem>();
            items.Add(new SelectListItem()
            {
                Text = "All Category",
                Value = "all",
                Selected = selectedValue.Equals("all", StringComparison.OrdinalIgnoreCase)
            });
 
            var categories = categoryRepository.GetAll().OrderBy(x => x.CategoryID);
 
            foreach (var c in categories)
            {
                items.Add(new SelectListItem()
                {
                    Text = c.CategoryName,
                    Value = c.CategoryID.ToString(),
                    Selected = selectedValue.Equals(c.CategoryID.ToString())
                });
            }
            return items;
        }
 
        //=========================================================================================
 
        public ActionResult Details(int? id, string category)
        {
            if (!id.HasValue) return RedirectToAction("index");
 
            Products product = productRepository.GetByID(id.Value);
            if (product == null)
            {
                return HttpNotFound();
            }
 
            ViewBag.Category = string.IsNullOrWhiteSpace(category) ? "all" : category;
 
            return View(product);
        }
 
        //=========================================================================================
 
        public ActionResult Create(string category)
        {
            ViewBag.CategoryID = new SelectList(this.Categories, "CategoryID", "CategoryName");
            ViewBag.Category = string.IsNullOrWhiteSpace(category) ? "all" : category;
 
            return View();
        }
 
        [HttpPost]
        public ActionResult Create(Products products, string category)
        {
            if (ModelState.IsValid)
            {
                this.productRepository.Create(products);
                return RedirectToAction("Index", new { category = category });
            }
 
            ViewBag.CategoryID = new SelectList(this.Categories, "CategoryID", "CategoryName", products.CategoryID);
 
            return View(products);
        }
 
        //=========================================================================================
 
        public ActionResult Edit(int? id, string category)
        {
            if (!id.HasValue) return RedirectToAction("index");
 
            Products product = this.productRepository.GetByID(id.Value);
            if (product == null)
            {
                return HttpNotFound();
            }
            
            ViewBag.CategoryID = new SelectList(this.Categories, "CategoryID", "CategoryName", product.CategoryID);
            ViewBag.Category = string.IsNullOrWhiteSpace(category) ? "all" : category;
 
            return View(product);
        }
 
        [HttpPost]
        public ActionResult Edit(Products products, string category)
        {
            if (ModelState.IsValid)
            {
                this.productRepository.Update(products);
                return RedirectToAction("Index", new { category = category });
            }
            
            ViewBag.CategoryID = new SelectList(this.Categories, "CategoryID", "CategoryName", products.CategoryID);
 
            return View(products);
        }
 
        //=========================================================================================
 
        public ActionResult Delete(int? id, string category)
        {
            if (!id.HasValue) return RedirectToAction("index");
 
            Products product = this.productRepository.GetByID(id.Value);
            if (product == null)
            {
                return HttpNotFound();
            }
 
            ViewBag.Category = string.IsNullOrWhiteSpace(category) ? "all" : category;
 
            return View(product);
        }
 
        [HttpPost, ActionName("Delete")]
        public ActionResult DeleteConfirmed(int id, string category)
        {
            Products product = this.productRepository.GetByID(id);
            this.productRepository.Delete(product);
 
            return RedirectToAction("Index", new { category = category });
        }
 
    }
}

 

2012-11-07 補充更新:

KKBruce 建議補上類別圖,所以就用 VS2012 產出一張簡單的類別圖,包含介面與類別,可點選圖片看詳盡大圖。

image

 


最後做個說明,應該很多人看完這一篇是一頭霧水,怎麼這一篇好像是翻掉上一篇的內容……

有些人會覺得我好像耍了大家,明明第一篇就已經把各個類別的資料存取給抽離出來,到了第二篇卻又說要做一個可以通用的資料存取 Repository,所以把原本各類別的 Repository 又給拿掉,但是這一篇卻又……

事出必有因,不是故意要吊各位的胃口,也不是故弄玄虛,只是這一段歷程是要讓大家走一遭的,別忘了,一開始這一系列的文章就是以「初學者」對象,用引導的方式讓大家知道為什麼要這麼做,而不希望跳過這中間的轉變,因為這個轉變如果不做個交待的話,是會讓很多人摸不著頭緒的。

像這樣的各類別資料存取操作,很多人都有不同的作法,有的人會把 GenericRepository 的方法給做得比較完整,所以就不會在另外建立各類別的 Repository,反而會另外建立 Service,將比較複雜的商業邏輯操作放在 Service 當中,Service 裡再使用 IRepository<T> 對 Models 進行資料存取,這樣一來就把專案的架構分為三層:Web 為 UI 層、Service 為商業邏輯層、Repository(Models) 則為資料存取層,不過關於這些的詳細作法就留待日後再來說明。

 

系列文章下一篇:

ASP.NET MVC 專案分層架構 Part.4 - 抽出 Model 層並建立為類別庫專案

以上

18 則留言:

  1. 連看三篇,爽。

    不過有個小小建議,因為會有很多介面、類別在那裡繞來繞去,如果可以提供類別圖來說明,應該會更完美。

    也期待接下去三層的文章。

    回覆刪除
    回覆
    1. 說得也是,又是介面又是類別,的確會讓人頭昏,感謝 KKBruce 的建議,下回補上。

      刪除
  2. 修改後的 ProductController 中的 public List CategorySelectList
    如果頁面邏輯複雜一點,會有很多類似的 method

    這個 Controller 的程式碼就會變的很長
    如果把它抽離出來,有沒有什麼建議的架構或命名方式呢
    (ex:ProductMethod Class 之類的)

    或者有沒有可能把它放在 Service 層

    回覆刪除
    回覆
    1. Hello, David

      你看到重點了,目前「分層架構」這個系列還在 Repository 資料存取的部份,所以 ProductController - CategorySelectList() 的這個方法的操作,就如你所說的,通常會再抽出來,讓 Controller 這裡面的程式可以更簡潔,而只需要關注系統流程的操作,我的作法是我還會將這種把資料取出後還要加工的方法給放在一個 Service 類別中,像 CategorySelectList() 這個方法會隸屬於 Category 的 Service 中,就會建立一個 CategoryService 的類別來放置有關 Category 的Business Logical 處理。

      在我的分層裡,Repository 是對資料的 CRUD 處理,而 Service 是透過 Repository 來存取資料,不直接操作資料的 CRUD,而 最後 Presentation 是對上 Service 來處理資料的輸出入。

      以上是簡短的回應,希望你不要見怪,因為「分層架構」的進度還沒走到 Service 這裡,現階段還是先把 Repository 的進度給完成,有關 Service 層的說明就請再等等囉。

      如果對於 Service 層想要有更進一步的了解,可以參考以下的網頁:
      http://stackoverflow.com/questions/5049363/difference-between-repository-and-service-layer

      刪除
    2. 你好, 如果中間需要用上 ViewModels, 請問 service Layer 那裡應該怎麼辦呢....我在參考以下的網頁:
      http://www.asp.net/mvc/tutorials/older-versions/models-(data)/validating-with-a-service-layer-cs

      看得我暈頭轉向 ~_~

      刪除
    3. 依據你所提供的這篇文似乎重點不是在於 ViewModel 而是在 Server Layer 裡面對於 Model 的驗證,
      所以我不清楚你的問題點是在哪裡,
      不過 ViewModel 與 Server Layer 的部份也會是未來會說明的,
      只是我現在忙了一點,所以就請耐心等候。

      刪除
    4. OH ....是我心急了....

      刪除
    5. 別這麼說,應該是我最近更新文章的速度慢了,工作還是比較重要,所以文章就一直沒有進度,
      希望我很快能有時間來繼續寫這個系列的文章。

      刪除
  3. 這樣已經非常清楚囉,謝謝啦

    期待下篇的說明 :)

    回覆刪除
  4. Hi Kevin,

    請問一下您 Service Layer 會在建立 Interface 嗎
    目前我專案開發已經開始分成 Models、Repository、Service、Web

    在 Repository Layer 時,我會先建立 Interface,再建立 Class
    不過在 Service Layer 時,我沒有這麼做,就直接建立 Class
    因為我認為 Service Layer 沒有必要實作 IOC

    以我的觀點來說實作 IOC 是方便於寫 Test Case 和更換資料庫時,自己建立不同的 Repository Layer

    回覆刪除
    回覆
    1. 其實我會建立 Interface,尤其是在大型專案,當然小型專案就不會這樣做,看專案而定。

      刪除
    2. 那我知道了,我在仔細想想,目前還想不到 Server Layer 建立 Interface 的實際應用會在什麼地方 XD

      刪除
  5. 請問關於GenericRepository 及IGenericRepository 在這個範例裡好像功能已經很齊全了,在實務還有什麼東西可能放進這裡面的呢?
    謝謝回答。

    回覆刪除
    回覆
    1. 這篇文章裡的 GenericRepository 及 IGenericRepository 應該沒有很齊全吧... 因為只有定義兩個方法,
      不過如果有看到第五篇「ASP.NET MVC 專案分層架構 Part.5 - 建立 Service 層 (http://kevintsengtw.blogspot.tw/2012/12/aspnet-mvc-part5-service.html)」,該篇文章類別圖裡的 IGenericRepository 已經有定義了幾個基本的方法,其實就是基本的 Create, Read, Update, Delete 等資料操作,至於在實務應用上還有什麼可以放進去,這就要看你所開發的系統會有哪些方法是所有類別都會用到的。
      或是你可以在網路上搜尋看看,或是在一些 open source 的專案裡,看看別人所寫的專案會用到什麼樣得方法。

      刪除
    2. 感謝您的回答.如果拆分在Service裡的話.
      基礎的GenericRepository 好像就挺通用的.
      剩下在Service裡解決.所以沒想到GenericRepository還會需要補足什麼,除非要針對存取方法做變更,目前還沒想到有什麼會特別需要做在GenericRepository的層級。
      請教您還會在Model針對存取資料再建其他的Repository嗎?
      謝謝

      刪除
    3. 基本上是不會另外再去建立 Repository,因為 GenericRepository 可以拿到某個 Model 的 DbSet,就可以做蠻多事情了,
      如果會有比較特殊的需求的話,例如多個 Model 的關聯操作,因為擺在那一個 Model 的 Service 都不太適合,這個時候我就會另外建立 Service 來做多個 Model 的操作。
      其實也是有蠻多數的開發者認為不太需要另外建立 Repository,而是直接讓 Service 對 EntityFramework 做資料存取,讓中間不必再透過 Repository,這也是一種方式。
      開發方式沒有一個正解,只有適合的方式,就看專案與開發團隊是否適合與接受。

      刪除
    4. 非常感謝您的回答。

      刪除
  6. 並且應用泛「行」 => 「型」~

    回覆刪除

提醒

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