2012年10月22日 星期一

ASP.NET MVC 專案分層架構 Part.1 初學者的起手式

2014-12-02 補充說明:
雖然標題包含「初學者」但不表示這是給連 ASP.NET MVC 初學者甚至是程式初學者所看的,對象為已經具有一定程式開發基礎但是對於簡單的分層架構不太瞭解以及沒有概念的開發人員。

上星期(10/13 六)是 twMVC 第五場研討「活動宣傳 - 2012-10-13 twMVC 第五次研討會 … 雲端、專案系統架構」,上半場的主題是由 Bibby 所帶來的「ASP.NET MVC 之實戰架構探討」,有關於專案架構的資訊是每一次 twMVC 研討會後問卷上一定會有人提到想要聽的主題,所以這一次就由 Bibby 來說明,Bibby 在這一場的內容包含了專案架構相關的內容,而會後的問卷結果看來,大家的接受度都相當地高,不過也有些朋友反應說要看一點實作面的內容。

分層開發架構並不是 ASP.NET MVC 獨有的,任何的專案開發不管使用什麼樣的技術都應該要做好專案架構的分層,不管是同一個專案中的分層處理或是不同職責方法的獨立專案分層架構,專案分層架構其目的就是為了要職責確立、關注點分離,讓不同的方法或類別去做該做的事情而且只專注於這些方法、類別的職責上。

接下來的幾篇文章將會以初學者對象,將大雜匯式的專案開發內容逐步的改變,讓專案改為分層架構,所以對於進階開發人員,或是已經會分層架構的朋友就可以跳過這幾篇文章。

而我這邊所講述的分層架構方法也不是絕對或標準、唯一的方法,每個人、每個團隊對於程式的寫法、架構的區分都個有不同,對於架構的見解與實作的方式也有不同,所以我所說明的內容也只是將我的部份見解給寫出來,就如同標題一樣,對象是「初學者」。


前面已經說過了,在這邊再次強調,如果你是程式開發的初學者或是 ASP.NET MVC 初學者,甚至是開發經驗少於兩年的開發人員,請馬上離開此篇文章。

 

使用開發環境:

    • Visual Studio 2012
    • .NET Framework 4.5
    • ASP.NET MVC 4.0
    • MS SQL Server 2012 Express

其實專案分層架構不管環境是不是跟我一樣,還是可以做。

 

專案準備:

這邊我建立了一個 ASP.NET MVC 4.0 的網站專案,並且使用 ADO.NET Entity Framework,
以下就是專案的內容與樣貌,

image


Model
image


Views
image

首頁:
image


Category:
image


Product:
image


Controller 內容:

CategoryController.cs(手動建置)

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Mvc_Repository.Models;
 
namespace Mvc_Repository.Controllers
{
    public class CategoryController : Controller
    {
        public ActionResult Index()
        {
            using (TestDBEntities db = new TestDBEntities())
            {
                var query = db.Categories.OrderBy(x => x.CategoryID);
                ViewData.Model = query.ToList();
                return View();
            }
        }
 
        //=========================================================================================
 
        public ActionResult Details(int? id)
        {
            if (!id.HasValue)
            {
                return RedirectToAction("index");
            }
            else
            {
                using (TestDBEntities db = new TestDBEntities())
                {
                    var model = db.Categories.FirstOrDefault(x => x.CategoryID == id.Value);
                    ViewData.Model = model;
                    return View();
                }
            }
        }
 
        //=========================================================================================
 
        public ActionResult Create()
        {
            return View();
        }
 
        [HttpPost]
        public ActionResult Create(Categories category)
        {
            if (category != null && ModelState.IsValid)
            {
                using (TestDBEntities db = new TestDBEntities())
                {
                    db.Categories.Add(category);
                    db.SaveChanges();
                }
                return RedirectToAction("index");
            }
            else
            {
                return View(category);
            }
        }
 
        //=========================================================================================
 
        public ActionResult Edit(int? id)
        {
            if (!id.HasValue)
            {
                return RedirectToAction("index");
            }
            else
            {
                using (TestDBEntities db = new TestDBEntities())
                {
                    var model = db.Categories.FirstOrDefault(x => x.CategoryID == id.Value);
                    ViewData.Model = model;
                    return View();
                }
            }
        }
 
        [HttpPost]
        public ActionResult Edit(Categories category)
        {
            if (category != null && ModelState.IsValid)
            {
                using (TestDBEntities db = new TestDBEntities())
                {
                    db.Entry(category).State = EntityState.Modified;
                    db.SaveChanges();
                    return View(category);
                }
            }
            else
            {
                return RedirectToAction("index");
            }
        }
 
        //=========================================================================================
 
        public ActionResult Delete(int? id)
        {
            if (!id.HasValue)
            {
                return RedirectToAction("index");
            }
            else
            {
                using (TestDBEntities db = new TestDBEntities())
                {
                    var model = db.Categories.FirstOrDefault(x => x.CategoryID == id.Value);
                    ViewData.Model = model;
                    return View();
                }
            }
        }
 
        [HttpPost, ActionName("Delete")]
        public ActionResult DeleteConfirmed(int id)
        {
            try
            {
                using (TestDBEntities db = new TestDBEntities())
                {
                    var target = db.Categories.FirstOrDefault(x => x.CategoryID == id);
                    db.Categories.Remove(target);
                    db.SaveChanges();
                }
            }
            catch (DataException)
            {
                return RedirectToAction("Delete", new { id = id });
            }
            return RedirectToAction("index");
        }
 
    }
}

CategoryController 的程式都是手工刻出來的,而這樣的程式內容也是初學者最常寫的一種,在每一個 Action 方法中只要有操作到資料的部份,都會去 new 一個 TestDBEntities 的 instance 出來,這樣的操作看起來沒有什麼問題,只不過一旦每個 Controller 的 Action 都要這樣處理時,這樣的方式就會覺得很累人,而且在控制流程的地方去直接對資料庫進行處理也不是那麼妥當。

ProductController.cs(使用 Wizard 建立 Controller 與 Views)

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Mvc_Repository.Models;
 
namespace Mvc_Repository.Controllers
{
    public class ProductController : Controller
    {
        private TestDBEntities db = new TestDBEntities();
 
        public ActionResult Index()
        {
            var products = db.Products.Include(p => p.Categories).OrderByDescending(x => x.ProductID);
            return View(products.ToList());
        }
 
        //=========================================================================================
 
        public ActionResult Details(int id = 0)
        {
            Products products = db.Products.Find(id);
            if (products == null)
            {
                return HttpNotFound();
            }
            return View(products);
        }
 
        //=========================================================================================
 
        public ActionResult Create()
        {
            ViewBag.CategoryID = new SelectList(db.Categories, "CategoryID", "CategoryName");
            return View();
        }
 
        [HttpPost]
        public ActionResult Create(Products products)
        {
            if (ModelState.IsValid)
            {
                db.Products.Add(products);
                db.SaveChanges();
                return RedirectToAction("Index");
            }
 
            ViewBag.CategoryID = new SelectList(db.Categories, "CategoryID", "CategoryName", products.CategoryID);
            return View(products);
        }
 
        //=========================================================================================
 
        public ActionResult Edit(int id = 0)
        {
            Products products = db.Products.Find(id);
            if (products == null)
            {
                return HttpNotFound();
            }
            ViewBag.CategoryID = new SelectList(db.Categories, "CategoryID", "CategoryName", products.CategoryID);
            return View(products);
        }
 
        [HttpPost]
        public ActionResult Edit(Products products)
        {
            if (ModelState.IsValid)
            {
                db.Entry(products).State = EntityState.Modified;
                db.SaveChanges();
                return RedirectToAction("Index");
            }
            ViewBag.CategoryID = new SelectList(db.Categories, "CategoryID", "CategoryName", products.CategoryID);
            return View(products);
        }
 
        //=========================================================================================
 
        public ActionResult Delete(int id = 0)
        {
            Products products = db.Products.Find(id);
            if (products == null)
            {
                return HttpNotFound();
            }
            return View(products);
        }
 
        [HttpPost, ActionName("Delete")]
        public ActionResult DeleteConfirmed(int id)
        {
            Products products = db.Products.Find(id);
            db.Products.Remove(products);
            db.SaveChanges();
            return RedirectToAction("Index");
        }
 
        //=========================================================================================
 
        protected override void Dispose(bool disposing)
        {
            db.Dispose();
            base.Dispose(disposing);
        }
    }
}

這個 ProductController.cs 的內容與 Views/Product 目錄下的 View Page 檔案都是使用 Wizard 所快速建立出來的,

image

接著出現「加入控制器」的視窗,依序填入控制器名稱以及選擇「模型類別」「資料內容類別」
注意,資料內容類別這裡要選擇的是對應 Models 下的 Entities 的名稱

SNAGHTML7997dbe

因為也要同時建立 Product 相關的 View 檔案,所以在「加入控制器」的視窗中點選「進階選項」

SNAGHTML79c53c2

點選之後會出現以下的視窗,如果需要另外指定主版頁面的話,就需要在這裡去指定主版頁面檔案,

SNAGHTML79d1617

一般來說,如果主版頁面是選擇預設的 _Layout.cshtml 就不用進入「進階選項」視窗做其他的設定
而 Razor 的 _viewstart 檔案是指以下的檔案,

image
image

其實 CategoryController 與 ProductController 這兩個 Controller 所做的事情以及資料處理的流程與內容都是差不多的,只不過 ProductController 使用 TestDBEntities 時並不是在每一個 Action 方法中去新建 TestDBEntities instance,而是在一開始的時候就宣告一個類別為 TestDBEntities 的私有欄位「db」,

image

如此一來在 ProductController 中的每個 Action 方法都可以不用再自行建立 TestDBEntities 的 instance 出來,而最後使用完 TestDBEntities instance 的釋放動作則是在 ProductController 的最後有 override Dispose 方法,這樣在每次的資料操作處理完畢後就可以釋放資源,

image

但無論是 CategoryController 或是 ProductController 都一樣是會在 Controller 中直接使用到 TestDBEntities,所以我們要把這個地方去做個改變,不要直接在 Controller 中去直接對 TestDBEntities 進行資料操作,這邊要把記住一個觀念就是「關注點分離(Separation of concerns,SOC)」,什麼是關注點呢?

資料操作就是一個關注點,資料驗證流程控制都是關注點,當這些關注點都放在一起的時候,只有一種結果,那就是「混亂」。

假定一個更新資料 的 Action 方法,在方法中要處理輸入的資料是否正確相對應資料是否存在於資料庫更新資料到資料庫中更新後的流程處理等等,這些方法都參雜在一起時,要顧慮的地方就會很多,所以我們可以把對資料庫操作的部份給抽離出來,更新資料的 Action 方法只需要去關注資料的驗證與流程控制就可以。

把資料庫操作方法從 Action 方法給抽離出來,我們通常都會使用一種設計模式「Repository Pattern(倉儲模式)」,有關倉儲模式的定義與實作的方式都因為每個人的解釋或是見解而有所有不同,大致上,這個模式就是用來處理資料操作,資料操作不脫離四種操作「C, R, U, D」也就是 Create(建立)Read(讀取)Update(更新)Delete(刪除),所以我們就會去為每個 Model 類別建立專屬的 Repository 類別,每個 Repository 類別都只專注於自己所負責 Model 的資料操作,例如 Category 的 Repository 只需要專注於 Category 資料的操作,不需要干涉其他資料類別的操作,這就是所謂的「單一職責原則(Single Responsibility Principle,SRP)」。

建立 Repository 還有另一個重點就是「重複使用 」,像取得某筆資料、取得全部資料,這類的資料操作是會經常重複用到的,如果每次都要重複建立相同的程式碼,一旦取得資料的邏輯有變動就需要全部做修改,為了避免這樣的情況,我們可以將這些經常使用到的方法建立在 Repository 中,往後要用到時就只需要使用 Repository 中的方法,既使取得資料的邏輯有變動也只要修改 Repository 的方法就可以,這也就是物件導向中所強調的「Do not Repeat Yourself(DRY)」。

 

接下來我們依照 Repository Pattern 來個別建立 Category 與 Product 的 Repository 類別。

 


建立 Repository

先建立 Repository 類別的介面,建立介面是為了要避免直接依賴 Repository 類別,往後在 Controller 中只需要用介面來進行資料操作,而不需直接使用 Repository 類別;

建立 ICategoryRepository 與 IProductRepository

在專案中的 Models 下建立 Interface 目錄,並分別新增兩個介面檔案:ICategoryRepository, IProductRepository

image

ICategoryRepository.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace Mvc_Repository.Models.Interface
{
    public interface ICategoryRepository
    {
        void Create(Categories instance);
 
        void Update(Categories instance);
 
        void Delete(Categories instance);
 
        Categories Get(int categoryID);
 
        IQueryable<Categories> GetAll();
 
        void SaveChanges();
    }
}

IProductRepository.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace Mvc_Repository.Models.Interface
{
    public interface IProductRepository
    {
        void Create(Products instance);
 
        void Update(Products instance);
 
        void Delete(Products instance);
 
        Products Get(int productID);
 
        IQueryable<Products> GetAll();
 
        void SaveChanges();
 
    }
}


建立兩個 Repositiry 類別檔案,並且實作剛才所建立的兩個 Interface,

image

實作 ICategoryRepository 介面

using Namespace

image

實作介面內容

image

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Mvc_Repository.Models.Interface;
 
namespace Mvc_Repository.Models
{
    public class CategoryRepository : ICategoryRepository
    {
        public void Create(Categories instance)
        {
            throw new NotImplementedException();
        }
 
        public void Update(Categories instance)
        {
            throw new NotImplementedException();
        }
 
        public void Delete(Categories instance)
        {
            throw new NotImplementedException();
        }
 
        public Categories Get(int categoryID)
        {
            throw new NotImplementedException();
        }
 
        public IQueryable<Categories> GetAll()
        {
            throw new NotImplementedException();
        }
 
        public void SaveChanges()
        {
            throw new NotImplementedException();
        }
    }
}

一開始所有方法都是空空的內容,就等著我們去實作,

 

實作 CategoryRepository 內容:

這時候就可以把原本在 CategoryController 中的資料存取的程式給搬到 CategoryRepository 中,最後實作了 IDispose 介面,讓資料操作完成後可以釋放資源,

CategoryRepository.cs

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Web;
using Mvc_Repository.Models.Interface;
 
namespace Mvc_Repository.Models
{
    public class CategoryRepository : ICategoryRepository, IDisposable
    {
        protected TestDBEntities db
        {
            get;
            private set;
        }
 
        public CategoryRepository()
        {
            this.db = new TestDBEntities();
        }
 
 
        public void Create(Categories instance)
        {
            if (instance == null)
            {
                throw new ArgumentNullException("instance");
            }
            else
            {
                db.Categories.Add(instance);
                this.SaveChanges();
            }
        }
 
        public void Update(Categories instance)
        {
            if (instance == null)
            {
                throw new ArgumentNullException("instance");
            }
            else
            {
                db.Entry(instance).State = EntityState.Modified;
                this.SaveChanges();
            }
        }
 
        public void Delete(Categories instance)
        {
            if (instance == null)
            {
                throw new ArgumentNullException("instance");
            }
            else
            {
                db.Entry(instance).State = EntityState.Deleted;
                this.SaveChanges();
            }
        }
 
        public Categories Get(int categoryID)
        {
            return db.Categories.FirstOrDefault(x => x.CategoryID == categoryID);
        }
 
        public IQueryable<Categories> GetAll()
        {
            return db.Categories.OrderBy(x => x.CategoryID);
        }
 
 
        public void SaveChanges()
        {
            this.db.SaveChanges();
        }
 
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }
 
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (this.db != null)
                {
                    this.db.Dispose();
                    this.db = null;
                }
            }
        }
 
    }
}

ProductRepository.cs

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Web;
using Mvc_Repository.Models.Interface;
 
namespace Mvc_Repository.Models
{
    public class ProductRepository : IProductRepository, IDisposable
    {
        protected TestDBEntities db
        {
            get;
            private set;
        }
 
        public ProductRepository()
        {
            this.db = new TestDBEntities();
        }
 
        
        public void Create(Products instance)
        {
            if (instance == null)
            {
                throw new ArgumentNullException("instance");
            }
            else
            {
                db.Products.Add(instance);
                this.SaveChanges();
            }
        }
 
        public void Update(Products instance)
        {
            if (instance == null)
            {
                throw new ArgumentNullException("instance");
            }
            else
            {
                db.Entry(instance).State = EntityState.Modified;
                this.SaveChanges();
            }
        }
 
        public void Delete(Products instance)
        {
            if (instance == null)
            {
                throw new ArgumentNullException("instance");
            }
            else
            {
                db.Entry(instance).State = EntityState.Deleted;
                this.SaveChanges();
            }
        }
 
 
        public Products Get(int productID)
        {
            return db.Products.FirstOrDefault(x => x.ProductID == productID);
        }
 
        public IQueryable<Products> GetAll()
        {
            return db.Products.Include(p => p.Categories).OrderByDescending(x => x.ProductID);
        }
 
 
        public void SaveChanges()
        {
            this.db.SaveChanges();
        }
 
 
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }
 
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (this.db != null)
                {
                    this.db.Dispose();
                    this.db = null;
                }
            }
        }
    }
}
 

因為兩個 Repositroy 都需要實作 IDispose 介面,所以我們可以在兩個 Repository 的介面去指定繼承 IDispose 介面,如此一來 CategoryRepository 與 ProductRepository 就必須要實作 IDispose 介面的方法了,


ICategoryRepositoey.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace Mvc_Repository.Models.Interface
{
    public interface ICategoryRepository : IDisposable
    {
        void Create(Categories instance);
 
        void Update(Categories instance);
 
        void Delete(Categories instance);
 
        Categories Get(int categoryID);
 
        IQueryable<Categories> GetAll();
 
        void SaveChanges();
    }
}


IProductRepository.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace Mvc_Repository.Models.Interface
{
    public interface IProductRepository : IDisposable
    {
        void Create(Products instance);
 
        void Update(Products instance);
 
        void Delete(Products instance);
 
        Products Get(int productID);
 
        IQueryable<Products> GetAll();
 
        void SaveChanges();
 
    }
}


在 Controller 中使用 Repository

建立好 CategoryRepository 與 ProductRepository 後就是分別在 CategoryController 與 ProductController 中使用,
我們在 controller 中是用 interface 類別 來操作資料,所以在 controller 的一開始會需要增加以下的程式,

image

先建立一個類別為 ICategoryRepository 的私有欄位,然後在建構式中再以 CategoryRepository 類別去建立 instance,
如此一來就可以使用放在私有欄位的 categoryRepository 來進行資料的存取操作,
以下是改使用 ICategroyRepositiry 後的 CategoryController,


CategoryController.cs

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using Mvc_Repository.Models;
using Mvc_Repository.Models.Interface;
 
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().ToList();
            return View(categories);
        }
 
        //=========================================================================================
 
        public ActionResult Details(int? id)
        {
            if (!id.HasValue)
            {
                return RedirectToAction("index");
            }
            else
            {
                var category = this.categoryRepository.Get(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.Get(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.Get(id.Value);
                return View(category);
            }
        }
 
        [HttpPost, ActionName("Delete")]
        public ActionResult DeleteConfirmed(int id)
        {
            try
            {
                var category = this.categoryRepository.Get(id);
                this.categoryRepository.Delete(category);
            }
            catch (DataException)
            {
                return RedirectToAction("Delete", new { id = id });
            }
            return RedirectToAction("index");
        }
 
    }
}


ProductController.cs

在 ProductController.cs 中除了使用 IProductRepository 之外也有用到 ICategoryRepository,這是因為 Product 有關聯到 Category,建立或是更新 Product 時會使用到 Category 下拉選單,這邊就會需要透過 ICategoryRepository 來取得所有產品分類的資料,

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using Mvc_Repository.Models;
using Mvc_Repository.Models.Interface;
 
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().ToList();
            return View(products);
        }
 
        //=========================================================================================
        
        public ActionResult Details(int id = 0)
        {
            Products product = productRepository.Get(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.Get(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.Get(id);
            if (product == null)
            {
                return HttpNotFound();
            }
            return View(product);
        }
 
        [HttpPost, ActionName("Delete")]
        public ActionResult DeleteConfirmed(int id)
        {
            Products product = this.productRepository.Get(id);
            this.productRepository.Delete(product);
            return RedirectToAction("Index");
        }
 
    }
}

到此就完成了 Repository 的實作與使用,兩個 Controller 中的方法都沒有直接對資料庫進行資料操作,而是透過 Repository 類別來進行資料的存取,在 Controller 的方法裡就不再需要考慮到對資料庫的存取操作,只需要考慮流程控制與資料的正確與否。

image


「專案分層架構」這個題目相當難以說明,因為要分幾層、怎麼分層、各層有什麼職責、要做什麼事?這些都可以再細分成很多個主題來說明,但對以往專案沒有做過分層的人或是程式初學者(不管是 WebForm or MVC)而言,怎麼做分層架構就是一個大難題,尤其是現在很多書籍在教程式時,書中的範例往往都是大雜匯地把資料存取、流程控制、使用介面處理都放在一起,久而久之就讓很多的開發人員就只會大雜匯的開發方式,這個情況最容易在 ASP.NET WebForm 的專案中看到,我目前駐點單位的資訊窗口人員就是堅持在 Code Behid 的一個事件中要把所有的處理給做完,所以就可以在一個 Button_Click 的事件中看到有 SQL Statement, ADO.NET, DataSet, DataTable, Server Control 的許許多多處理,這位窗口就極度無法接受所謂的「分層架構開發」,因為她說分層開發不好維護,要找一個錯誤要找很久,不如就在一個事件處理中把所有要做的事情都一次處理完畢,有問題的話就知道要從哪邊找錯誤…… Orz

這一篇分層架構就先到此為止,這是第一個步驟而已,這篇文章中的作法是比較淺顯,先說個大概,不要一次說得太深,這麼做是要讓初學者可以先有個概念,而進階開發者都應該了解這一篇的內容,甚至也知道要如何再進行更進一步的修改,所以下一篇就會講到更進階一些的修改,讓分層架構可以更加完整。

分層架構不一定要分好幾個專案來操作,在同一個網站專案中也是可以做出的分層架構,不論使用哪一種方式,都必須要看專案的實際需求而定,如果只是一個小小的專案就不需要大費周章地硬是要在一個 Solution 中去切分好幾個 Project,但如果是大型的專案,我就真的會建議要在 Solution 中去切分為 Project,如此一來讓專案結構可以更加嚴謹並且有比較好的責任區分。

 


系列文章下一篇:

ASP.NET MVC 專案分層架構 Part.2 抽出 Repository 裡相同的部份

 

延伸閱讀:

MSDN - The Repository Pattern
http://msdn.microsoft.com/en-us/library/ff649690.aspx

Huan-Lin 學習筆記 - 應用程式的分層設計 (1) - 入門範例

http://huan-lin.blogspot.tw/2012/09/designing-layered-data-centric.html

Huan-Lin 學習筆記 - 應用程式的分層設計 (2) - 一點改進
http://huan-lin.blogspot.tw/2012/10/designing-layered-application-2.html

點部落-昏睡領域-Clark的心得筆記 - [Architecture Pattern] Repository

http://www.dotblogs.com.tw/clark/archive/2012/04/29/71883.aspx

Wiki - 關注點分離

http://zh.wikipedia.org/wiki/%E5%88%86%E7%A6%BB%E5%85%B3%E6%B3%A8%E7%82%B9

|Wiki - Don't repeat yourself

http://en.wikipedia.org/wiki/Don't_repeat_yourself

91之ASP.NET由淺入深 不負責講座 Day17 - 單一職責原則

http://ithelp.ithome.com.tw/question/10054102

Code Project - Implementing Repository Pattern With Entity Framework

http://www.codeproject.com/Articles/37155/Implementing-Repository-Pattern-With-Entity-Framew

twMVC - Bibby - ASP.NET MVC 之實戰架構探討(簡報檔)

https://speakerdeck.com/u/twmvc/p/asp-dot-net-mvczhi-shi-zhan-jia-gou-tan-tao


以上

43 則留言:

  1. 你好,
    本人是MVC 新手, 你這文章幫助了我很多.
    但本人有個問題: 以這個範例為例, 當 CategoryController 執行到 using (TestDBEntities db = new TestDBEntities()) 後, Categories 資料表的資料會全部載入 TestDBEntities.db.Categories 中. 如果Categories 資料表有大量資料就甚為不妥, 請問有甚麼方法可以改為用條件載入 (sql where)?
    我的email: terry.hk@gmail.com
    謝謝.

    回覆刪除
    回覆
    1. ㄟ... 看來你對於 ADO.NET Entity Framework 還有 LINQ 的使用與概念應該也不是很熟,
      執行到 using (TestDBEntities db = new TestDBEntities()),
      在這個 scope 內的 var query = db.Categories; 這時候還不會真的向資料庫拿資料回來,
      這時候的 query 還是 iQueryable,
      一直要等到 query.ToList(); 這段程式執行之後才會真的向資料庫取資料回來,
      如果要來個條件式的取出資料,可以用:
      var query = db.Categories.FirstOrDefault(x => x.CategoryID == id.Value);
      這是取出一筆資料,
      或是
      var query = db.Categories.Where(x => x.CategoryID == id.Value);
      這是取出一個 IQueryable 的資料,
      以上

      建議,如果對於 ADO.NET Entity Framework 與 LINQ 不熟悉,不要看這一系列的文章,
      看了會害了你,先把基礎的觀念與操作搞熟了之後再來看。

      刪除
  2. 你好,
    我將edmx的資料表(Categories,Products)都拉進去之後,要新增一個Category的Controller時
    範本:
    具有讀取/寫入動作和檢視,使用Entity Framework的MVC控制器
    模型類別:
    Categories (MvcStore.Models)
    資料內容類別:
    DBEntities (MvcStore.Models)

    其他部分都沒有動
    當我按下"加入"時,

    它顯示錯誤訊息:無法擷取MvcStore.Models.Categories 的中繼資料。找不到DBModel.Products的CLR型別。

    這是什麼意思呀??

    回覆刪除
    回覆
    1. 你好,這個問題我並沒有遇到過,
      不過查詢國外論壇類似問題時,大多是發生在使用 Code-First 上,
      有個方法可以試試看,好像記得是從保哥的 Blog 那邊看到的,
      「Will - 如何在 .NET 4.5 的 ASP.NET MVC 4 網站使用 Scaffold 範本」http://goo.gl/GkGsD
      可以試著將「資料內容類別」的 DBEntities (MvcStore.Models),把後面括號的部份給刪除,
      至於是什麼原因造成這個問題,有可能是 VSCommands 的問題也有可能是 DataProvider 的問題,
      總之原因不太清楚,可以試著用「ASP.NET MVC Create Controller Unable to retrive metadata Could not find the CLR type」這組關鍵字詞找尋相關問題與解決方式。

      刪除
    2. kevin大您好,

      保哥那篇的方法我試過不行,但我發現我在edmx的圖表中,
      將Categories資料表加上關聯,右鍵->加入新項目->關聯
      按照預設值設定,但我有取消勾選"將外部索引鍵屬性加入加入至Products實體"
      (因為Product中已經有CategoryID所以不需要勾選)

      按下確定後在那條關聯的線點兩下進入 參考性限制
      主體
      Categories
      相依
      Products
      以下
      CategoryID = CategoryID

      好了之後rebuild一次

      接下來新增CategoryControll了,依照上面的設法,就會出錯了
      但我試過不設定關聯就不會出錯@@

      刪除
    3. 你好:
      我想我們所使用的都是 Northwind 資料庫,一般而言都已經預先設定好 Table 之間的關聯,
      在文章裡的 MVC 專案裡加入一個 ADO.NET 實體資料模型,並加入使用資料庫的 Table,
      在 Datebase-First 模式中,我不會在 EDMX 裡去手動加入模型間的關聯,
      所以我並不會有你所描述去取消勾選或是加入關聯的操作,
      之所以不會做這些操作,主要是因為 EDMX 會隨著資料庫的異動而有所改變,
      會連帶地讓手動在 EDMX 加入的設定受到影響,
      如果 Database First 模式下除了原來資料庫裡已經有建立的關聯之外,還要另外加入新的關聯,
      我會建議另外對 Model 建立 partial class,然後以程式的方式做處理。

      要是你這種手動去加入關聯的需要比較多,我會建議你改用 Code First 的模式。

      刪除
    4. 其實我不太明白你為何要手動去新增一個關聯項目呢?

      刪除
    5. kevin大您好,

      因為我是看你的Model圖有關聯,所以增加一個關聯
      如果是直接將兩張表拉進去edmx的話,不會有這條線

      刪除
    6. 就如同我前面所說的,要先在資料庫中有拉好兩張表的關聯後,在 EDMX 中才會有兩個 Model 的關聯,
      所以你要檢查你所使用資料庫的 Table,例如 Categories 與 Products 是否有拉關聯。

      刪除
    7. kevin大您好,

      不好意思,沒有看到你上一個完全正解的解答
      說的真詳細~

      現在已經沒有這個問題囉~~
      謝謝您!!

      刪除
  3. kevin大您好,

    在CategoryController中

    private ICategoryRepository categoryRepository;
    為什麼要用介面的方式去宣告啊??
    不能使用
    private CategoryRepository categoryRepository;嗎??

    或許它有什麼樣特殊的意義,想請教kevin大~
    依我所知它是用在多型的時候會常見到<--不知道正不正確

    回覆刪除
    回覆
    1. 在一個單純的系統當中通常會是直接以型別去建立一個實例(new a instance),
      不過當一個系統規模越來越大而且也開始做分層規劃,
      像這種直接以型別做宣告的方式,通常會影響後續開發與維護的彈性,
      而以介面來做宣告,就可以保持彈性,
      這個在分層架構的後續文章裡會有提到(在第六篇),
      另外程式中使用介面,也有助於進行單元測試的工作,
      先說到這裡,說太多會讓你更加模糊,或是你也可以在網路搜尋相關資料。

      刪除
    2. kevin大您好,

      了解了~~ 感謝您的回覆

      謝謝您!!

      刪除
    3. 補充以下的連結,
      MSDN - interface (C# 參考)
      http://msdn.microsoft.com/zh-tw/library/87d83y5b(v=vs.110).aspx

      MSDN - 介面 (C# 程式設計手冊)
      http://msdn.microsoft.com/zh-tw/library/ms173156(v=vs.110).aspx

      為什麼要針對介面寫程式? - 軟體開發的天空- 點部落
      http://www.dotblogs.com.tw/jameswu/archive/2008/06/26/4384.aspx

      刪除
  4. kevin您好:

    我照您的文章實作測試後,發現Category的Edit表單中,修改後按Save要送出時卻無法動並回到清單頁面。
    我再回上一頁看,資料有修改到。
    不知道是哪裡出問題?
    謝謝!

    回覆刪除
    回覆
    1. Hello, 你好
      因為我沒有實際看到你的程式,有可能是 View 有問題或是 CategoryRepository 處理出現錯誤等,
      你可以把你所做的專案寄給我看看,kevintsengtw@outlook.com

      刪除
    2. To Kevin
      Yen所說的為這一段,當修改成功應該要redirct 回index,原本的code是return view
      以下為修正過的code
      if (advicement != null && ModelState.IsValid)
      {
      this.advicementRepository.Update(advicement);
      return RedirectToAction("Index");
      }
      else
      {
      return View(advicement);
      }

      刪除
  5. 這篇文章幫助我很大, 感謝分享

    回覆刪除
  6. Kevin 大您好,我想問有關dispose的問題,在db.Savechange()之後沒看到您有寫db.dispose

    請問db 連現在什麼時候才做dispose? 小弟新手 看不太懂

    回覆刪除
    回覆
    1. SaveChange 之後可以不必刻意去執行 dispose.

      如果你對 Entity Framework 才剛接觸,甚至於 ASP.NET MVC 也一樣剛接觸不久,我建議你跳過這一系列的文章。
      先以基本的 ASP.NET MVC + EF 方式練習,等到 EF 與 ASP.NET MVC 熟練之後在回來看。

      刪除
  7. HI Kevin大,之後可以不必刻意去執行 dispose,那您這一篇的寫法 dispose的時機點是在什麼時候?

    我有寫隻sample 不過沒有套 interface ,大概是這樣


    public void AddCustomers(Customers C) {
    DAL.AddCustomers(C);
    DAL.Dispose();
    }

    回覆刪除
    回覆
    1. 這是系列文章,還沒寫到那邊...
      我不清楚你的 DAL 的實作內容,Entity Framework 的 dispose 不等同於 ADO.NET 的關閉資料庫連線,
      EF 的 dispose 還有包含釋放相關資源的處理,而這一部分系統的 GC 會去做處理,
      如果還是需要做顯式處理的話,你的作法是可以,
      因為是單純的在一個 Method 裡新增 Customers 資料後再去做 dispose
      但如果同一個 HttpRequest 的處理不單單只有執行 AddCustomers() 這個 method,在這之後還有做其他處理的話,
      那你已經釋放了 DbContext 的資源在先,之後的同樣要對 DbContext 的處理就需要再去啟始資源,
      這樣就好比以前 ADO.NET 重複建立資料庫連線、關閉連線的循環。

      如果你選擇跳過此系列的其他文章,而執著於 dispose 的部份,你可以看看 ASP.NET MVC 官方教學課程的其中一篇
      「Implementing the Repository and Unit of Work Patterns in an ASP.NET MVC Application (9 of 10)」
      http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application

      刪除
    2. 感謝 Kevin大的指導,有比較瞭解了~~

      我把這方式套在webform上面,之前有寫過MVC的案子有遇到類似用 Repository的方式去撰寫

      那時怎麼看都看不懂,也不知道是什麼東西, 看了您的文章終於"解惑"了~~

      刪除
  8. 請問如果想用async的方式實作,要怎麼訂interface及Repositiry的實作。

    回覆刪除
    回覆
    1. 請參閱「初探 Entity Framework 6 - Async/Await Create, Read, Update, Delete with MVC 5」這一篇文章的內容,
      http://kevintsengtw.blogspot.tw/2013/11/entity-framework-6-asyncawait-create.html
      其實方式都是一樣的,只是多了 Async 與 Task.

      刪除
    2. 感謝您的回答,有試成功了。

      刪除
  9. 您好,
    想請教一個問題
    最近公司有個計畫要將ERP系統用asp.net mvc改寫
    這系統概括分成5個model,每個model有不同的功能群組,total大約15 group, 近千個tables, 總合數百支function,7人維護
    這5個model有些相依
    之前都是一個人寫, 第一次帶多人team. 有些想法想請您指教
    1. 不同類型的分層, 各自整合在不同project
    2. 為了減少library size, 因此相同類型的分層 project將依不同的功能群組, 會有不同的project file
    3. 同類型的分層, namespace相同, 無論拆成多少個project.

    這樣一個solution將會有六七十個 project
    這種分法是否合適?
    namespace 相同, 是否會造成執行速度變慢?

    for example.
    ERP solution
    ASPGroup1 (asp.net) (asp.net namespace always "ERP"
    ASPGroup2 (asp.net)
    ASPGroup3 (asp.net)
    ...
    ASPGroup15 (asp.net)
    ControlGroup1 (namespace is "ERP.Control")
    ControlGroup2
    ...
    ControlGroup15
    ModelGroup1 (namespace is "ERP.Model")
    ModelGroup2
    ...
    ModelGroup15

    回覆刪除
    回覆
    1. 這問題不好在這邊回答,因為你所描述的架構需求相當大,而且勢必有新舊資料結構的問題,如果時間地點允許的話,請在週四晚上來參加 twMVC 固定聚會,一同當面討論。
      http://mvc.tw/coding4fun

      刪除
    2. Hello, 我真的不是在推託或是講客套話,而是希望你可以來與我們一同討論系統架構,
      因為現實狀況裡,有太多團隊或是公司都有來詢問過我們,很多人都想要用 ASP.NET MVC 去改寫有的舊系統,
      但是往往都是因為舊系統並不是物件導向設計或是太過於龐雜,而在改用 ASP.NET MVC 開發時就遇到太多問題,
      ASP.NET MVC 開發是注重強型別與物件導向,所以想要將原有資料結構與系統流程硬生生地改寫,一定會出事情,
      最後導致很多人會覺得 ASP.NET MVC 不好或是難以掌握,追根究柢的原因都是在於對於物件導向與舊有結構無法統整的關係,
      一個 ERP 系統,如你所說的有很多個功能模組,包含了上千個 Table,之間的關連與結構一定是相當複雜,
      而且充滿了許多疊床架屋的「故事」,這些都是必須要一一釐清,
      有很多做法可以去解決這些問題,例如分拆多個系統,各個系統再以 Restful API 串連,以 SOA 的模式來形成整個系統,
      做法有很多,但必須要有完整的事前規劃,
      一個 Solution 有六七十個 Project 是否適當?我想如果有必要的話,是可以這麼做,但有必要嗎?
      在多人開發的專案裡,這樣龐雜的專案是否都能夠得到管控而不會無限發散?
      就如同我前面所說的,可以切分成多個子系統,每個子系統會共用的程式與模組,各個子系統各自獨立,
      如此一來就不會散亂而難以釐清各系統的職責,
      再來你有考慮系統的測試呢?如果你有寫測試的話,應該不至於有這樣的情況發生,
      所以最後還是建議你可以找個時間來跟我們 twMVC 聊聊,交換意見。

      刪除
  10. 詢問一下 我想要將兩個table關聯起來 我也下了linq很奇怪的是在list部分join出了問題
    我原本以為是型別的問題@@就一直跑出並未將物件參考設定為物件的執行個體這個錯誤
    https://gist.github.com/jeok70/9b99e1ce6b297805d4dea5c3a2d5b319
    這是我的片段程式碼
    在sd那塊就出了問題
    按照整個常理是沒問題的啊@@

    回覆刪除
    回覆
    1. 似乎是因為Linq to list我的A資料表跟B資料表不對稱的關係應該是這樣吧?

      刪除
    2. 我必須說,我完全不能夠理解你的問題,因為你所提供的程式內容,細節的部分只有你才能知道,
      而執行後的錯誤訊息也只有你看到過,我這邊完全無法就你的程式內容去分析你所遇到的錯誤。

      從你的程式片段來看,有些無關你所提出問題的地方必須要跟你講,
      將 Repository 的 GetAll() 方法的回傳結果直接使用 ToList() 轉換,
      雖然說可能裡面的資料只有少少的幾筆,但是這樣的習慣不好,
      一旦 GetAll 所回傳的資料有上萬或更多的時候,貿然的使用 ToList() 就會碰到麻煩

      你的問題,有試著在 LINQPad 去執行並且觀察過嗎?

      刪除
    3. 沒有...我嘗試看看,我目前知道的是 似乎因為另一個轉成list的時候是空值導致
      select new SchoolData 發生了問題

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

    回覆刪除
  12. 網誌管理員已經移除這則留言。

    回覆刪除
  13. 請問下面的程式,不就與ViewModel 的原則相違背?


    Products product = this.productRepository.Get(id);
    if (product == null)
    {
    return HttpNotFound();
    }
    ViewBag.CategoryID = new SelectList(this.Categories, "CategoryID", "CategoryName", product.CategoryID);
    return View(product);

    回覆刪除
    回覆
    1. Hello,
      這要分幾個方向來看

      如果是以這一篇文章來看,這只是系列文的第一篇,先帶出 Repository 模式與導入到專案裡實做
      所以不會講到 ViewModel,而且「ASP.NET MVC 專案分層架構」是系列文章 ( https://reurl.cc/WzLAe )
      一開始寫 Part.1 ~ 4 的時候還沒有寫「ASP.NET MVC 的 ViewModel - 基礎篇」這篇文章 ( https://reurl.cc/jV5mM )
      而是到了 Part.4 發佈後因為有人問了有關 Model 的問題,所以才會寫了上面那一篇 ViewModel 的文章
      因為文章的連貫性與內容一致性,所以整個系列文章一直到最後以及範例程式 ( https://github.com/twMVC/twMVC-18-1 ) 都沒有使用 ViewModel

      以實務專案開發來看,如果專案是很簡單而且不是那麼複雜,是否為了要區分而建立 ViewModel,這要自行決定
      專案很小、功能很簡單,其實也不用做什麼分層,也更不用說使用 ViewModel
      如果專案很小也簡單,但不是一個人開發,我會建議要使用 ViewModel,避免程式結構與格式的混亂
      而中型或大型專案那就更不用說,一定會用到 ViewModel,甚至為了區分輸出顯示用與輸入資料用,而會再把 ViewModel 去做區別

      其實沒有什麼違背不違背的,程式寫久了之後,有些過往堅持的原則是會因為專案經歷而做出一些調整
      並不是每個專案都一定要符合所謂的準則
      但並不是每個開發人員從入門就可以不去遵守或依循

      最後,回答你的提問內容
      一、如果是針對文章的程式提出質疑,那我在回覆的一開始就已經說明
      二、如果是小型專案的話,要不要用 ViewModel,就看開發者或開發團隊的共識
      三、如果是比較大的專案是這樣寫的話,那就是找死

      刪除
    2. 並不是每個專案都一定要符合所謂的準則,但並不是每個開發人員從入門就可以不去遵守或依循
      這句話也是矛盾,如果不符合所謂的準則,又要開發人員去遵守或依循,結果就是無所適從
      應該說重要的是準則背後上的意義,即使不使用準則,也需要說明其背後的意義為何
      覺得所有案例還是要把該使用的作法用上,才會更了解ViewModel是不是在所有的情況下都能適用
      會不會有整合上的困難

      刪除
    3. 還有如果只做小型專案,就用不著來看這種進階的文章了,理所當然就是要朝更高的目標邁進,
      所以小型專案的想法也不用去考慮了,就是要做大型專案才來學習吧

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

    回覆刪除

提醒

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