2012年12月24日 星期一

ASP.NET MVC 專案分層架構 Part.5 - 建立 Service 層

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

這一篇距離上一篇「ASP.NET MVC 專案分層架構 Part.4 - 抽出 Model 層並建立為類別庫專案」相隔了好一段時間,除了說工作比較忙碌而沒有時間外,其實主要是為了這個系列的章節順序傷腦筋,因為想要先說其他的部份,但是如果不先說 Service 層的話,後續的章節也不見得會比較好寫,但寫了 Service 層會把之前的一些東西給推翻掉,怕會引起太多的反彈,內容就這樣反反覆覆,文章也寫得斷斷續續,最後還是決定先說明 Service 層。

看這一篇之前一定要先看過「分層架構」的 Part.1 ~ Part.4 這四篇文章。

 


什麼是 Service 層?

在 Repository 層裡,主要是用來操作資料的存取,例如應該要怎麼取得資料、取得多少的資料等,不牽涉商業邏輯的操作,而取得資料後的一些操作,例如訂單資料要去判斷訂單明細中貨品的存量是否可以出貨,像這樣的判斷處理並不適合放在 Repository 層之中,而 Repository 層為提供給 Service 層來使用。

Controller 其主要的工作為系統流程的控制,依據傳入的需求來決定要存取哪些資料,並且決定要選擇 View 以回應到需求端,資料的存取已經交給 Repository 做處理中,而商業邏輯的處理也不適合放在 Controller 當中,過多的商業邏輯處理反而會讓 Controller 的流程控制與商業邏輯混在一起,使得程式會越來越複雜且難以維護,此外就是不利於測試。

服務層,主要是把系統的商業邏輯給封裝起來,Service 層使用 Repository 層所提供的服務來存取資料, Controller 則是透過 Service 層來做資料的處理,而不直接使用 Repository。

 

Repository 還是 Service?

每個開發者的見解都不同,我的作法是,Repositiry 主要負責資料的 CRUD,而 Service 則是處理商業邏輯的處理。

 

建立 Service 層

我們在方案中建立一個 Service 的類別專案,

image

在 Service 專案裡我還是一樣會建立 Interface,所以建立 Interface 目錄來放置各個 Service 的 Interface,而 Service 專案會加入 Models 專案的參考,這樣才知道要存取什麼資料,

image

接著建立 interface,分別建立 ICategoryService.cs 與 IProductService.cs,至於介面的內容有些什麼呢?
基本上 Service 的 Interface 的定義會包含對物件的 CRUD,另外會依照各個 Model 的操作需要而建立其他的方法,如下:

ICategoryService.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Mvc_Repository.Models;
using Mvc_Repository.Service.Misc;
 
namespace Mvc_Repository.Service.Interface
{
    public interface ICategoryService
    {
        IResult Create(Categories instance);
 
        IResult Update(Categories instance);
 
        IResult Delete(int categoryID);
 
        bool IsExists(int categoryID);
 
        Categories GetByID(int categoryID);
 
        IEnumerable<Categories> GetAll();
 
    }
}


IProductService.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Mvc_Repository.Models;
using Mvc_Repository.Service.Misc;
 
namespace Mvc_Repository.Service.Interface
{
    public interface IProductService
    {
        IResult Create(Products instance);
 
        IResult Update(Products instance);
 
        IResult Delete(int productID);
 
        bool IsExists(int productID);
 
        Products GetByID(int productID);
 
        IEnumerable<Products> GetAll();
 
        IEnumerable<Products> GetByCategory(int categoryID);
 
    }
}

有看過「分層架構」前面四篇的人看了這個介面的定義後一定會有所懷疑甚至是不解,為什麼這兩個 Interface 的內容會跟 IRepository, ICategoryRepository, IProductRepository 的內容如此雷同,也可以說混合在一起了,為什麼要這麼做呢?

之前的作法是 Solution 內並沒有區分 Service 層,而對於資料的存取操作雖然是由 Repository 負責,但呼叫使用的卻是 Controller,我們是在 Controller 做了 Service 要做的事情,而一開始也有提到,Repository 是用來服務 Service 的,Repository 主要的工作是做資料存取,加入了 Service 層之後,原先在 ICategoryRepository 與 IProductRepository 內所定義的方法就不再需要了,連帶 CategoryRepository 與 ProductRepository 也不需要建立了,將這些工作交給 Service 來處理,Repository 就只保留 GenericRepository。

在兩個介面中的 Create, Update, Delete 所回傳的型別是 IResult,這是我自己建立的型別,用在執行後的結果訊息傳遞,

IResult.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace Mvc_Repository.Service.Misc
{
    public interface IResult
    {
        Guid ID
        {
            get;
        }
 
        bool Success
        {
            get;
            set;
        }
 
        string Message
        {
            get;
            set;
        }
 
        Exception Exception
        {
            get;
            set;
        }
 
        List<IResult> InnerResults
        {
            get;
        }
 
    }
}

Result.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
 
namespace Mvc_Repository.Service.Misc
{
    public class Result : IResult
    {
        public Guid ID
        {
            get;
            private set;
        }
 
        public bool Success
        {
            get;
            set;
        }
 
        public string Message
        {
            get;
            set;
        }
 
        public Exception Exception
        {
            get;
            set;
        }
 
        public List<IResult> InnerResults
        {
            get;
            protected set;
        }
 
 
        public Result()
            : this(false)
        {
        }
 
        public Result(bool success)
        {
            ID = Guid.NewGuid();
            Success = success;
            InnerResults = new List<IResult>();
        }
 
    }
}

 

實作介面

CategoryService.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Mvc_Repository.Models;
using Mvc_Repository.Models.Interface;
using Mvc_Repository.Models.Repository;
using Mvc_Repository.Service.Interface;
using Mvc_Repository.Service.Misc;
 
namespace Mvc_Repository.Service
{
    public class CategoryService : ICategoryService
    {
        private IRepository<Categories> repository = new GenericRepository<Categories>();
 
 
        public IResult Create(Categories instance)
        {
            if (instance == null)
            {
                throw new ArgumentNullException();
            }
 
            IResult result = new Result(false);
            try
            {
                this.repository.Create(instance);
                result.Success = true;
            }
            catch (Exception ex)
            {
                result.Exception = ex;
            }
            return result;
        }
 
        public IResult Update(Categories instance)
        {
            if (instance == null)
            {
                throw new ArgumentNullException();
            }
 
            IResult result = new Result(false);
            try
            {
                this.repository.Update(instance);
                result.Success = true;
            }
            catch (Exception ex)
            {
                result.Exception = ex;
            }
            return result;
        }
 
        public IResult Delete(int categoryID)
        {
            IResult result = new Result(false);
 
            if (!this.IsExists(categoryID))
            {
                result.Message = "找不到資料";
            }
 
            try
            {
                var instance = this.GetByID(categoryID);
                this.repository.Delete(instance);
                result.Success = true;
            }
            catch (Exception ex)
            {
                result.Exception = ex;
            }
            return result;
        }
 
        public bool IsExists(int categoryID)
        {
            return this.repository.GetAll().Any(x => x.CategoryID == categoryID);
        }
 
        public Categories GetByID(int categoryID)
        {
            return this.repository.Get(x => x.CategoryID == categoryID);
        }
 
        public IEnumerable<Categories> GetAll()
        {
            return this.repository.GetAll();
        }
    }
}

ProductService.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Mvc_Repository.Models;
using Mvc_Repository.Models.Interface;
using Mvc_Repository.Models.Repository;
using Mvc_Repository.Service.Interface;
using Mvc_Repository.Service.Misc;
 
namespace Mvc_Repository.Service
{
    public class ProductService : IProductService
    {
        private IRepository<Products> repository = new GenericRepository<Products>();
 
 
        public Misc.IResult Create(Products instance)
        {
            if (instance == null)
            {
                throw new ArgumentNullException();
            }
 
            IResult result = new Result(false);
            try
            {
                this.repository.Create(instance);
                result.Success = true;
            }
            catch (Exception ex)
            {
                result.Exception = ex;
            }
            return result;
        }
 
        public IResult Update(Products instance)
        {
            if (instance == null)
            {
                throw new ArgumentNullException();
            }
 
            IResult result = new Result(false);
            try
            {
                this.repository.Update(instance);
                result.Success = true;
            }
            catch (Exception ex)
            {
                result.Exception = ex;
            }
            return result;
        }
 
        public IResult Delete(int productID)
        {
            IResult result = new Result(false);
 
            if (!this.IsExists(productID))
            {
                result.Message = "找不到資料";
            }
 
            try
            {
                var instance = this.GetByID(productID);
                this.repository.Delete(instance);
                result.Success = true;
            }
            catch (Exception ex)
            {
                result.Exception = ex;
            }
            return result;
        }
 
        public bool IsExists(int productID)
        {
            return this.repository.GetAll().Any(x => x.ProductID == productID);
        }
 
        public Products GetByID(int productID)
        {
            return this.repository.Get(x => x.ProductID == productID);
        }
 
        public IEnumerable<Products> GetAll()
        {
            return this.repository.GetAll();
        }
 
        public IEnumerable<Products> GetByCategory(int categoryID)
        {
            return this.repository.GetAll().Where(x => x.CategoryID == categoryID);
        }
    }
}

2013-05-29 更新說明:之前文章裡在 Repository 裡取得所有資料是使用 GetAll() 方法,而到了此篇文章卻改使用 Fetch(),其實兩種方法的程式都是一樣的,只是名稱不同,所以為了此系列文章的一致性,所以再改回使用 GetAll() 方法,感謝網友「amu607」的告知。

 

調整

完成了 Service 專案的建立與介面和實作之後,接下來就是要調整 Solution 的內容,首先要調整的就是 Repository 專案的內容,我們已經不需要 ICategoryRepository 與 IProductRepository 與其實作,所以就是把這些介面與實作從 Repository 專案中移除,

image

接下來就是調整 Web 專案,首先加入 Service 專案的參考,

image

再來就是修改 CategoryController 與 ProductController 的內容,因為之前是在 Controller 使用各個 model 的 repository,而現在都被移除之後,就改使用 CategoryService 與 ProductService。

 

CategoryController.cs

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

ProductController.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using Mvc_Repository.Models;
using Mvc_Repository.Service;
using Mvc_Repository.Service.Interface;
 
namespace Mvc_Repository.Web.Controllers
{
    public class ProductController : Controller
    {
        private IProductService productService;
        private ICategoryService categoryService;
 
        public IEnumerable<Categories> Categories
        {
            get
            {
                return categoryService.GetAll();
            }
        }
 
        public ProductController()
        {
            this.productService = new ProductService();
            this.categoryService = new CategoryService();
        }
 
        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)
                ? productService.GetAll()
                : productService.GetByCategory(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 = categoryService.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 = productService.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.productService.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.productService.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.productService.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.productService.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)
        {
            this.productService.Delete(id);
            return RedirectToAction("Index", new { category = category });
        }
 
    }
}

 

為什麼要捨棄原本建立的 repository 改使用 service 呢?

假如在 Controller 裡會慢慢地增加很多種方法,而這些方法越來越繁雜時,Controller 就會越來越難維護,Controller 除了原來的 Action 方法外還會有很多用來處理商業邏輯操作的方法存在,Controller 應該關注的是流程的控制、資料輸入與輸出的處理、決定回應需求要使用那一個 View,過多的商業邏輯處理方法同處在 Controller 裡,就破壞了「關注點分離」的原則,而這些商業邏輯的處理大多都會跟資料存取有關,那如果把這些方法都移往 repository 裡去,對於 repository 所扮演的角色與責任又會有所偏移。

repository 應該關注的是資料存取,不應該牽涉到商業邏輯的處理,所以這時候就需要加入 Service 層,這個 Service 層不需要關注資料怎麼處理,只需要知道資料存取是在哪裡,而其本身所關注的就是商業邏輯的處理。

將 Controller 的商業邏輯處理移往 Service 層,而原本依據各個 model 所建立的 repository 也就不需要存在,因為這部份的操作已經由 service 來處理,而各個 model 的 repository 就不需要個別建立,為了避免過多的資料存取方法的發散,在 service 裡是使用 GenricRepository 來處理資料的存取。

 

最後來看看調整後的架構,

image

 

 

2012-12-26 更新

補上 CodeMap 圖,讓希望能夠了解各層明確關連,

Mvc_Repository

Web 與 Models 的關聯在於 Model 資料與定義.

Web 與 Service 的關聯在於 controller 透過 service 來取得或更新 Model 資料.

Service 與 Models 的關聯在於 controller 使用 Models 的 Respository 來對資料進行存取操作.

image

Mvc_Repository.Web - Controller

image

Mvc_Repository.Service

image

Mvc_repository.Models

image

 


其實專案分層架構的建立順序與步驟不會如同分層架構的 Part.1 一直調整到 Part.5,如果真是這樣的話也會太累人了,通常都會先建立好 Model 的內容,然後建立 Repository,接著建立 Service,Model, Repository, Service 這幾個部分不會與表現層(Web)有直接的關係,所以進行網站的 Layout 設計或調整的同時,這幾個部分可以先進行實作,一方面也可以依據需求來做調整與修改,當基礎架構都設計到一個階段後,就可以進行網站雛形架構的實作,不必等到整個網站的 Layout 設計或是切版出來之後才做。

之前有人詢問有關於 ViewModel 在 Service 層之中是要怎麼處理的,因為這個問題不再分層架構的範疇下,有關 ViewModel 的說明,可以先參考不久前的文章「ASP.NET MVC 的 ViewModel - 基礎篇」,先對 ViewModel 有基本的認識後,日後會在另外的篇幅中做這部份的交代。

 

系列文章下一篇:

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

 

延伸閱讀:

Huan-Lin 學習筆記

應用程式的分層設計 (1) - 入門範例

應用程式的分層設計 (2) - 一點改進

應用程式的分層設計 (3) - DDD、六角、與洋蔥架構

Repository,我可能不會用你

Repository,我可能不會用你 (2) - 範例

 

以上提供的文章所談論的架構與我所說明的架構有所不同,但對於專案的架構要如何規畫,每個層面各自做什麼處理,在這些文章中都有深入的說明,另外提醒的就是別忘了看文章後面的回應討論,那也是重點喔!

 

以上

77 則留言:

  1. Hi Kevin,

    我又來了 XD

    有些疑問想請問您一下,我目前進行的專案 DB 是沒有建立關聯的,所以我會建立一個 ViewModel 是要給 table join,而這一層的程式我會寫在 Repository

    因為寫在 Service 時,如果去建立 n 個 GenericRepository, GenericRepository
    會因為不同的 Entity Framework 實體而發生錯誤

    如果在 GenericRepository 加一個建構式來傳入 Entity Framework 實體,又感覺沒有達到一致性的邏輯

    這種情況來說,有沒有什麼不一樣的寫法呢?

    回覆刪除
    回覆
    1. Hello, David
      我不太清楚你的架構,如果可以的話,在「關於我」裡面有我的 Email,
      你可以將你的問題與狀況以 Email 方式,把細節再描述清楚一些,
      如此我才能知道要如何跟你討論。

      刪除
  2. Hello Kevin:

    看了您分層架構的系列文章後有一些疑問

    很多情況下並不是所有欄位都會讓使用者輸入

    例如修改者帳號和修改時間

    這時修改帳號和時間這個動作應該是在Controller裡還是Services裡?

    基於不想每個使用該Services的Controller都要做同樣的動作

    我覺得應該要在Services裡做

    但是抓取登入資訊的User物件只能在Controller裡取得

    這時是否把User物件也一份傳到Services裡會比在Controller裡取得並填入Entity裡來得好?

    另外想請問在傳遞Entity時是利用edmx裡的Entity加上metadata的方式來判斷資料格式

    還是自行建立viewmodel來傳遞會比較好?

    再麻煩您解惑,謝謝您

    回覆刪除
    回覆
    1. Hello 你好,
      的確不是每個欄位都會讓使用者去輸入或是修改,要修改也是由系統來做,例如你所提到的修改者名稱與修改時間,
      我看了一下這系列到這一篇的文章內容,因為是著重於程式的架構面,所以就沒有顯示太多前端畫面出來,
      在前端的新增與修改頁面,只會輸出要讓使用者輸入的欄位,
      修改時間這個會是在 service 由程式來給,
      而修改者名稱因為是必須在網站的上下文裡得到,不會是在 service 裡拿到,
      所以會是由 controller 這邊把修改者名稱傳到 service。

      至於你所說的傳遞 Entity,是要用 Entity 類別還是使用 ViewModel 類別,
      其實就名稱來看就已經知道答案了,ViewModel 是給 View 所使用的類別,可以看做是功能性的 DTO,
      除了給 View 使用或是 View 與 Controller 的溝供外,實在是不宜另做他用,
      有關 ViewModel 的使用,可以使用「ViewModel」來搜尋這個部落格,我有寫了三篇相關文章來說明。

      物件的傳遞,在小型專案裡,由於 Model 並不會太過於複雜,所以大多直接使用 Entity 類別,
      而當專案規模大的時候,我們會將 Model 給切出去,此時會衍生建立 DTO 類別來做為資料的傳遞使用,
      大型專案裡,前端所使用的物件,大多不是使用 Entity Framework 的原生 Entity 類別,而是會使用 DTO 類別,
      有關於這些內容還在做整理,加上「分層架構」是需要很多心力與腦力,深怕寫出錯誤的內容而誤導了大家的觀念。

      分層架構觀念與作法的養成並非一朝一夕,也不是看了幾本書或是幾篇文章就可以了解,
      可以多看看網路上一些 Open Source 的專案程式,看看不同專案的作法,
      然後再勤於實作與練習,並反覆質疑自己的架構是否合宜、是否有缺陷等等。

      以上

      刪除
    2. 更正
      「溝供」應為「溝通」
      「前端所使用的物件,大多不是使用 Entity Framework 的原生 Entity 類別」應為「前端所使用的物件,有些不是使用 Entity Framework 的原生 Entity 類別」

      刪除
  3. Hello Kevin:
    感謝您的回覆
    網路上找了一些專案,很多都是只有mvc三層而已
    是否有比較推薦可以提供參考學習

    期待您之後精采的文章

    謝謝

    回覆刪除
    回覆
    1. 有關系統架構的資源大多都是英文的,而系統架構並不限定 ASP.NET MVC 的範疇,
      以大角度的觀點來看,ASP.NET MVC 或 ASP.NET 甚至於 Windows Form 都只是表現層的實作,
      這邊推薦你一個在 CodePlex 的專案「Layered Architecture Sample for .NET」,
      http://layersample.codeplex.com/
      這個專案不要在意程式能不能執行,而是要看這個專案的架構,
      另外 CodePlex 有很多 ASP.NET MVC 的專案,其實都可以下載下來研究,我無法跟你說要看哪幾個專案,
      就是要多看看這些專案是怎麼做的。
      .
      另外想要了解架構方面的內容,有一本書可以推薦給你,
      「軟體構築美學:當專案團隊遇上失控程式,最真實的解決方案 (Brownfield Application Development in .Net)」
      http://www.tenlong.com.tw/items/9866348784?item_id=58312
      此書譯者「蔡煥麟」的部落格也是不容錯過,
      Huan-Lin 學習筆記:http://huan-lin.blogspot.tw/
      .

      刪除
    2. 對了... 說明一下「mvc三層」
      M.V.C 這三個不是「層(Layer)」的概念或是形式
      而是構成系統的三個主要部分
      M.V.C 並不能相比於一般的三層式架構(資料存取層、商業邏輯層、表現層)
      兩著是不同的概念

      刪除
  4. kevin大您好,

    這篇看完後,架構都跟您相同,程式碼也都相同
    在執行的時候,我新增了一個Category,新增成功
    當我要先增(Create)一個Product,此時沒有新增成功,也沒有跳出什麼錯誤訊息
    所以我就在ProductController設中斷點看參數是否有ModelBind起來,
    資料都有繫結起來

    所以我就想說在ProductService中設下中斷點,不過好像沒有什麼用,無法看到ProductService
    中的Create裡的product參數的內容

    所以像這種情況我要怎麼去對他偵錯? 或找Bug??

    回覆刪除
    回覆
    1. hello,
      其實看你這樣的描述,我必須說這有如瞎子摸象一般,我完全沒有頭緒,
      這幾天應該都是你留言提問的吧,
      與其這樣在文章留言提問,不如就把你的程式(最好是整個方案)以及你的問題彙整起來,然後 Email 給我,
      這樣我才能真的知道問題,因為你有看到程式都找不倒錯誤了,更何況是我看不到你程式的情況呢?

      刪除
    2. kevin大您好,

      好,我會將問題彙整再寄給您囉
      很感謝您還抽空回答我的問題

      謝謝您

      刪除
    3. 請教一下您的Email是??

      我在關於我沒有看到耶@@

      刪除
  5. 匿名 先生/小姐
    有問題的話
    如果再台北的話 禮拜四跑一趟伯朗比較快...

    回覆刪除
    回覆
    1. 的確,如果對 ASP.NET MVC 開發上有許多疑問的朋友,
      如果是住在台北市或新北市的朋友,歡迎每週四晚上 07:30 ~ 09:00 到伯朗咖啡科大店來參加 twMVC 每週聚會,
      這個聚會是開放的,沒有任何形式、不限主題,歡迎帶著疑問前來與 twMVC 討論,
      或是你有什麼新鮮事、任何想法想要分享,也歡迎各位前來!

      伯朗咖啡科大店 - 台北市 大安區 忠孝東路三段52號1樓 (捷運忠孝新生站三號出口)
      地圖:http://goo.gl/tcnoO

      刪除
  6. 您好,看完這篇文章後,想請教問題,Service 層的 Create、Update 等等方法實作內容是一樣的,您是否會再做處理呢?
    另外想再請教,國外這方面的文章有不少人都會將 Repository 跟 UnitOfWork 設計模式結合在一起,想聽聽您對 UnitOfWork 結合的看法,因每個人作法都不一樣,您的做法也很不錯,想聽聽不同的見解,如有造成困擾還請見諒,謝謝您。

    回覆刪除
    回覆
    1. 你好,
      我不清楚你所謂的再做處理是指哪一方面?因為這一系列的文章僅對一些間單的 CRUD 做說明,可能無法帶出比較複雜的內容,如果是在實際的專案處理上,Service 要處理的事情就會相當地多,都是依據需求做進一步的處理。
      至於 Unit Of Work,這也將會是這個系列文章下一階段將會談到的,就如你所言,每個人的做法都不盡相同,各有各的優缺點存在,如果是依我實際專案上的做法,因為我主要是做網站程式,所以我視每一個進來的 Request 處理為一個工作單元,新刪修的處理在一次的 Request 裡完成 Commit,但這也並不是每個專案適用,還是要依據專案的適用性來選擇適合的做法,不一定一種做法就要全部套用在每個專案上。
      因為 unit Of Work 這種 pattern 在台灣並不是那麼被拿來做討論,尤其是 .NET 的開發人員,如果有遇到 ASP.NET MVC 的開發人員想要了解 Unit of Work 的做法時,我都會先讓他們看 ASP.NET MVC 官網上的教學課程,那篇接學文章的做法也是常被使用的其中一種,相信你也一定看過。

      Implementing the Repository and Unit of Work Patterns in an ASP.NET MVC Application (9 of 10) : The Official Microsoft ASP.NET Site
      http://goo.gl/vJbvq

      歡迎在這裡提供意見或討論甚至於是指教,這樣技術才能相互成長,謝謝。

      刪除
  7. Kevin哥你好:
    我注意到Service實作類別裡的IsExists會去呼叫Repository的Fetch,我在之前的文章沒看到有定義出這個method,請問Fetch跟GetAll有什麼差別呢? 謝謝

    回覆刪除
    回覆
    1. 抱歉,因為這個系列的每篇文章發佈時間的間隔都有點長,所以程式內容在修修改改時就會遺漏掉,
      其實 Fetch() 與 GetAll() 的程式實作都是一樣的,都是使用以下的程式:
      this._context.Set();

      之後因為做整理時把 GetAll() 給移除而使用 Fetch(),Fetch 的意思為「取出」,看個人習慣要用什麼樣的方法名稱,
      其實在我本身的專案中是使用 GetAll(),所以兩種方法都是同一種的操作。

      感謝你的告知,我會再對文章裡的程式做個修正。

      刪除
  8. Kevin大 您好 最近參考您這一系列分層架構的練習手上一些案子,
    碰到了一點疑惑,以您這篇的例子來說 Product跟 Category兩個 contorller
    我今天Product 顯示的東西,可能是一個複合式的資料(JOIN 過 category 或JOIN很多表...),
    這時我的選擇可能會多做一個VieModel 來return給 view顯示,那我ProductServices應該要怎麼實作比較好?
    在Services的method直接return ViewModel? 但是這樣好像就沒有責任分離了,萬一我的ViewModel要多欄位,這樣就要就要修改Services裡面的method,就變成 Services跟Controller 綁的太死
    不知道碰上這種情況時,該如何去設計這個分層結構呢?
    謝謝!

    回覆刪除
    回覆
    1. Hello,你好
      因為這一系列的文章目前是以比較淺顯的情境來做說明,所以像你所提及的較為複雜的狀況就沒有做交代,
      先說明 ViewModel 這個部分,有關 ViewModel 要放在哪一層,有很多種作法,
      而我是認為 ViewModel 是在表現層(Web)這邊才會用到的,所以其他層並不會觸及到 ViewModel,
      所以 Service 層也不會也不應該去使用到 ViewModel,那 Service 所處理的複合資料應該要怎麼處理呢?
      基本上在實務上的作法就是會另外建立 DTO 類別(Data Transfer Object),
      DTO 類別的作用與 ViewModel 很類似,都是單純的物件類別,只有屬性與欄位,不太會去定義行為,
      另外定義 DTO 類別用於複合資料的傳輸就可以解決這個問題,
      畢竟不是所有的資料傳輸只能單純依靠 Domain Model 類別。

      而 View 的 Model 可以直接使用 DTO 類別嗎?也是可以的。
      只要定義清楚,避免事後維護上的雜亂,都可以使用的,因為 ViewModel, DTO 類別, Domain Model 都是物件類別,
      都可以作為 View 所使用的 Model.

      刪除
    2. Kevin大 您好 感謝您的回覆
      看到您所說的 DTO,我還是有一個問題,也是一樣用您這個例子來說
      我今天在 ProductServices 處理一個 Product join Category 的方法
      我可能定義了一個DTO 裡面的欄位有 ProductName CategoryName ProductInsertDate ProductPrice
      這樣return回去 給 ProductController 使用
      但這個DTO欄位其實就跟ViewModel定義的情況一樣,對ProductController 他在多做一次把 DTO塞進去ViewModel好像有點多餘?
      而且這樣的情況下,又回到原問題,如果我又在View要增加欄位 DTO、ProductServices都要動 好像並沒有解決問題?
      謝謝

      PS 抱歉 最近才剛從PHP跳槽過來學習asp.net mvc 問題可能有點蠢,請您諒解

      刪除
    3. 我所以提供的方法並非針對你所提出的情景來解說,而是針對常看到的複雜情況來做說明,
      依照你的情形,其實不必做到 DTO,
      再來就是一個 Controller 也不是只能使用一個 Repository 或 Service,
      MVC 的 Controller 是依據前端傳送過來的需求,然後再去調用適當的方法以取得符合需求的資料,最後再依據需求回傳結果到前端,
      所以你的方法比較簡單,只需要在 Controller 裡去調用 Service 取得資料,然後塞到 ViewModel 裡就可以。

      另外我前面的回覆裡也有說到,View 的 Model 也是可以直接使用 DTO 類別,
      所以你所謂「DTO塞進去ViewModel」這樣的作法就真的是多餘,直接用 DTO 類別就可以。

      最後我要說的是,其實我所給的建議並非標準做法,講明白一點就是,不是 MVC 的作法就是我所說的那樣,這世界上存在著各種作法,不是只限於 MVC 才能去用,或是 MVC 就必須使用某種作法,正確的應該是說,物件導向的程式做法有千千百百種,每種作法有其優點與缺點,並非每種做法都是正確的,只有適合與不適合的做法,所以像你所遇到的情境,你可以去是作多種不同的方式,然後找出適合情境與適合自己的方法,由自己去做驗證。

      刪除
    4. 再補充一下,你有提到一個重點「如果我又在View要增加欄位 DTO、ProductServices都要動」
      前端顯示的資料固然是由後端提供,但是後端 Service 不應當為了某個頁面上的顯示而去特地去增加一個特殊的類別,
      否則當前端顯示的資料有增減的變動時,那就會連帶影響 Service 以及 DTO,
      我所說的 DTO 也可以當做 View 的 Model 是在比較單純情況下的一個偷懶方法,
      我所定義的 DTO 應該只存在於 Service 之間以及 Service 與 Controller 間的溝通。

      所以適當的建立 ViewModel 還是有其必要,Service 不該跟 ViewModel 有所接觸,
      一切都只限於 Controller 的 Action 方法中去做資料裝載的處理,
      Controller Action 方法調用適當的 Service 取得資料後,再依據需求將取得的資料填入 ViewModel 中,
      最後就是 View 顯示資料。

      刪除
  9. 想請問在您的程式碼裡好像沒有看到呼叫Repository的dispose來釋放db的resource, 就小弟了解Dispose是需要有程式去呼叫才會有做事..

    回覆刪除
    回覆
    1. 有呀, 在 GenericRepository 裡面就有 Dispose 這個 Method, 在文章中間有一張類別架構圖片
      http://goo.gl/kFAHzz
      圖片裡面就可以看到在 在 GenericRepository 裡面就有 Dispose。
      在分層架構的第二篇裡就有定義了,只是沒有放在 IRepository 的定義中,
      http://kevintsengtw.blogspot.tw/2012/10/aspnet-mvc-part2-repository.html
      IRepository.cs 這個介面就有繼承 IDispose 介面,
      所以 GenericRepository 因為是繼承實作 IRepository,所以也就會去實作 Dispose 方法。

      刪除
    2. 好像沒有明確回答你的問題,
      因為有繼承實作 IDispose 介面的 Dispose 方法,所以程式執行完畢之後,系統會自動執行 Dispose 將資源給回收,
      至於是什麼時候,這我就不是很確定,這部份的處理與執行都是由底層的 .NET Framework 來做。

      MSDN - IDisposable 介面
      http://msdn.microsoft.com/zh-tw/library/system.idisposable.aspx

      刪除
    3. 嗯,但小弟記得有實作IDipose介面好像不會"自動"釋放,要被呼叫才行
      但在找相關文章發現了一篇 http://blog.jongallant.com/2012/10/do-i-have-to-call-dispose-on-dbcontext.html#.UgmhozsyawI
      說明DbContext是會自動釋放的, 所以有無呼叫Dispose就沒有太多差別了

      刪除
    4. 有關 Entity Framework 生命週期的說明,可以參考以下的文章:
      Huan-Lin 學習筆記: Entity Framework DbContext 物件的生命週期
      http://huan-lin.blogspot.com/2012/12/entity-framework-dbcontext-lifetime-in.html

      刪除
  10. 請問在IResult 中的ID 欄位是會根據不同的table而會有不同的型別, 例如 int
    有沒有辦法使用 Generic type 去定義這個變數呢?

    回覆刪除
    回覆
    1. IResult 只是一個回傳結果的型別,其 ID 欄位是 GUID 型別,於建立 IResult instance 時就會自動給值(建構式有處理),
      所以並不會如你所說的依據不同 table 而有不同型別,
      因為只是一個回傳處理結果的型別,所以不會因為處理資料的不同而有所不同變化,
      在文章中有 IResult 的定義以及實作 IResult 介面的 Result 類別程式內容,
      從 Result.cs 裡就可以了解。

      刪除
  11. Kevin 前輩你好,看了您的文章有個問題想發問。
    如果我觀念沒錯誤的話,定義介面是為了讓多個共同性質的類別能實作同一介面的方法,以保有擴充性以及共用性,但在文章中針對個別的 Service 都先定義了介面,這樣感覺上是不是會失去了介面原本的涵義。不知道 Kevin 前輩是不是有什麼比較好的見解或是這邊為什麼要這樣子去定義,而不直接建立 Server 就好?

    回覆刪除
    回覆
    1. 介面"不只是"你所說的[為了讓多個共同性質的類別能實作同一介面],如果依你所言就覺得失去介面原本的涵義,這就有些狹隘。

      刪除
    2. Kevin 前輩你好。
      這又讓我有點讓我混淆了,最近才比較常在接觸 OO 的概念,所以正確來講介面在物件導向中是扮演著什麼樣的角色。

      刪除
    3. 抱歉,雖然這麼說蠻不負責的,但我會建議你向其他前輩請教或討論,以免我誤導或傳遞了錯誤的內容。

      刪除
    4. OK 還是感謝前輩的回覆 ~ 我再來翻翻書釐清一下概念好了。

      刪除
    5. Hello, 91哥對於介面的使用上提供給我一些說明,徵得 91哥的同意後將內容轉載,如下:
      --
      從實作細節反推,只有因需求跟重構時發生,才有意義。
      如果設計都只是先設計實作細節,再一對一的長一層殼出來,那設計上就會被實作細節綁死。

      因此,interface應該都是訂在 context 端,而細節是獨立存在。
      怎麼把interface跟細節組合起來,就是透過 adapter 來組合。

      這樣就可以保持:context端純抽象,不管任何實作細節。
      實作細節端,不管要滿足哪一個介面,只管把事情做好。
      adapter沒有任何邏輯,只有接黏這兩個東西。
      context跟adapter的組合,再透過 DI framework 來做。

      重構的部份,指得就是擷取介面,而這個動作會發生在,針對某一個public的行為,發現需要抽象,因為出現了多個instance的需求。
      也就是原本的class可能有10個public行為,但這個需求只針對其中兩個行為進行「擷取介面」。

      刪除
  12. 你好,我想請教一下
    比對 CategoryService.cs 和 ProductService.cs 兩個檔案
    其中其實有很多函式重複
    您在實務上是否會將重複的抽出來做成類似 IRepository.cs 檔的方式
    用一個通用的泛型介面來處理這些重複性
    感謝回覆~

    回覆刪除
    回覆
    1. Hello,
      就我的經驗來說,我不會這麼做,Service 跟 Repository 不同是的資料處理的操作,
      Repository 是直接處裡資料的存取,而資料存取不會牽涉到商業邏輯,所以可以抽出一個 IRepository 介面來統一定義方法,
      而 Service 是將取得的資料去使用商業邏輯加以處理,每一個 Service 都會有不同的責任,
      就我的觀點來說,我不會去建立一個統一方法的介面讓各個 Service 去繼承實作,不然會限制 Service 的做法與彈性,
      其實你可以反問自己,介面對你的專案開發上而言,有什麼樣的作用與有什麼樣的意義。

      刪除
    2. 你好
      我想我描述的不夠清楚啦
      我想做的不只是 IRepository.cs 的東西而已
      只是先把 CRUD 之類的抽到 IService.cs 裡面去
      因為從 Repository 來的 CRUD 畢竟每個 Table 都會有
      然後再做一個 ICategoryService.cs 來繼承 IService.cs
      把擴充商業邏輯的部份寫在 ICategoryService.cs 中
      這樣 CRUD 就不用每個 Table 都還要實作一次
      概念上其實蠻像「ASP.NET MVC 專案分層架構 Part.3 - 個別 Repository 的資料存取操作」的結果

      刪除
    3. Hello,
      因為你所問的是:
      「在實務上是否會將重複的抽出來做成類似 IRepository.cs 檔的方式,用一個通用的泛型介面來處理這些重複性」
      所以我就將我本身在專案開發的應用方法告訴你,但看來你是想要知道這樣做可以或不可以。
      這麼說,專案的分層架構沒有一個正確答案或標準做法,只有是否合適的作法與是否適合開發者的方法。
      .
      我不這麼做的原因在於,並非每個 Service 都會有 CRUD,而且每個 Service 的 CRUD 也不一定都是只有針對某一個資料類別,
      有些 Service 並沒有 CRUD,這些 Service 只是去做一些特定的資料處理,
      所以有無必要去建立一個所謂的泛用 Service 介面呢?在我所開發的專案並沒有這麼做。
      另一個原因是因為 IoC Container 的 Configuration 的設定,
      因為我前面說過,專案裡的 Service 有各自不同的實作方法,所以我盡量去單純化,讓 IoC Configuration 設定不要過於複雜。
      .
      其實你有想法就先去實作一次,去實際體會這麼做的開發與運作過程,從而了解並找出合適的實作方法。

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

    回覆刪除
  14. Kevin老師您好 :

    我有些觀念不是很清楚,希望能請教一下,

    目前工作上被要求使用sql指令,所以應用在EF都是透過Database.SqlQuery 方法來下sql,

    如果說我依照本篇方法建立service層,移去本來在repository各自實作的方法,只留下共用repository 的 CRUD,

    那意思不就是說我會在service層下sql指令去實做各自類別特殊的方法?

    我不知道的是在service層下sql是合適的嗎??

    網路上教學多用linq,比較少看到用sql作範例
    ----------------------------------------

    而您延伸閱讀Huan-Lin 學習筆記內對service層定義又有所不同,我有點搞不清楚service層實際的用途

    我目前的理解是repository就像是DAL, 例如實作Product GetByID的方法,

    然後到service 層(是BLL), 將取到的Product 做一些邏輯判斷, 例如排除產品價格小於100圓之類的.

    希望請老師解惑我的盲點,非常感激







    回覆刪除
    回覆
    1. Hello 你好
      依據你一開始所描述的,其實你的專案應該只需要一般的 ADO.NET 執行資料庫的存取操作即可,並不需要使用到 Entity Framework,使用 ASP.NET MVC 開發時,Model 裡對於資料庫存取的這一個部分並不是一定得用 Entity Framework 才可以,這在我之前的一篇文章裡就有詳細的描述與說明,
      「ASP.NET MVC 的 Model 使用 ADO.NET」
      http://kevintsengtw.blogspot.tw/2013/05/aspnet-mvc-model-adonet.html
      .
      另外在去年的 twMVC#10 我就有針對這一個主題做了一次分享「twMVC#10 - Model 的設計與使用」
      http://mvc.tw/Event/2013/7/20
      .
      方法要依據實際的使用情境來決定使用,之所以會有 GenericRepository 這個類別的產生則是因為使用了 ORM 的關係,因為對於資料庫的存取有這麼一層 solution (EF or NHibernate or LINQ to SQL ... etc),我們會使用這些 ORM 所提供的方法去對資料進行存取的操作,在 Service 與 ORM Solution 中間加上這麼一層 Repository 除了定義對於各類別的資料存取之外,也定義了存取方法的實作範圍,以免在系統開發時的無限發散情況的發生,這在於多人開發的專案最容易有這樣的情形產生。
      如果專案預期不會有更換資料庫或是資料存取方法(例如更換 ORM)的狀況時,其實這一層的 Repository 是可以不必去實作,從 Service 裡就可以直接去使用 ORM 操作資料的存取。
      .
      所以你的專案情況更本不必使用 EF,直接使用 ADO.NET,然後定義好你的資料存取層(DAL 或是也可以稱為 Repository),而不必在 Service 裡去寫 SQL,之所以要區分資料存取與商業邏輯層,就是要明確規範各層的職責,如果面臨到必須要在 Service 裡去寫 T-SQL 時,不必多想,很明顯的就是你的實作有很大的問題。
      .
      我覺得我跟蔡煥麟老師所說的 Service 有什麼不一樣的地方,只是文字描述與內容的詳述方式不同而已,Service 不直接牽涉到怎麼向資料庫存取資料,在 Service 裡是透過資料存取層拿到資料後再去做所謂的商業邏輯處理,也就是你所說的,因為你並沒有將你所看到的蔡煥麟老師對於 Service 的定義給提供出來,所以我無法知道的你是在說我哪一個地方與他所說的有何不同,如果可以也請你再提供你所看到蔡煥麟老師對於 Service 定義的文章出處,因為他也是一位文章多產的部落客,雖然我有訂閱他部落格的 RSS,但是我自己也常常忘記我自己所寫的文章內容,所以不見得我就知道你所指的是什麼?
      .
      網路上對於 ASP.NET MVC 的專案教學文章的確是比較多以 EF 來做說明,純粹的 ADO.NET 作為 Model 的相關資源是比較少,但是就如同我前面所提供我之前的文章內容,無論是使用 EF 還是其他的 ORM 或是使用 ADO.NET 都是一樣的觀念,只是在實際的程式實作有所不同而已,說到這裡我就不禁要問你,知道什麼是 ORM 嗎?什麼是 ADO.NET 嗎?知道什麼是資料關係對映嗎?為何有了 ADO.NET 還需要 Entity Framework 呢?而物件導向的程式設計與 ADO.NET、Entity Framework 有什麼樣的關聯與應用呢?
      .
      我認為你要先釐清基本的觀念問題,釐清之後就知道一般的 ADO.NET 與 EF 的操作差異性,然後就會知道在只有使用一般 ADO.NET 做資料存取操作時如何以物件導向程式設計的方式去做系統開發與規劃、設計。

      刪除
    2. 補充說明,「ASP.NET MVC 的 Model 使用 ADO.NET」文章最底下的相關連結都是系列文章內容,都相當重要,尤其是最後一個「ASP.NET MVC 與 ASP.NET WebForm 使用 Simple Injector 切換選擇不同 Repository 原始碼下載」,裡面就有這一系列文章的所有範例程式原始碼,我是直接提供完整且可以正常執行的專案檔,藉由完整的專案內容讓大家可以更加清楚瞭解。

      刪除
    3. 非常感謝老師不因為我是初學,仍非常用心的回答,我會好好思考老師問的問題.


      我對ORM只有粗淺的概念,就是將資料以物件的方式做存取,而不是像以前使用dataset,datareader直接去處理資料,
      這在型態上能有更好的處理,另外就是可以預防SQL injection, 而EF底層也是ado.net做出來的.

      我並不清楚為何有了ado.net還需要EF,因為現處環境的人並不建議我使用它,一是因為學習取線較高,
      二是要考慮身邊的人習慣的開發方式.但我仍抱持著以後會用到心態去瞭解他.

      關於service部分,在應用程式的分層設計 (1) - 入門範例提到:

      NorthwindApp.Service:Class Library 專案。服務層,或應用程式層。撰寫應用程式邏輯的地方。這應該是薄薄的一層,不包含商業邏輯,而只是利用下一層的領域物件來提供展現層所需的服務。
      而Kevin老師是說 Service 是處理商業邏輯的部分,我單就字面上的意思去看,產生一個不含商業邏輯,一個含商業邏輯的矛盾,儘管分的層次不盡相同,應該都是指同一個service層吧? 這部分就是我疑惑的地方.

      ---------------------------------------------------------------------------------------
      我想舉一個簡單的例子來說明我的疑惑,

      像是專案中,我要做一個列表的搜尋與排序,不用EF,我會用組SQL字串的方式去實作他,通通實作在DAL層,
      但我以為搜尋和排序是service層該做的事情,DAL只要單純把資料讀出來就好,但如果遇到很大量的資料,
      先從DAL把所有資料撈出來,再到service用linq 去處理搜尋和排序就很奇怪,還是我一開始對搜尋排序的假設就是不正確的,
      而是要將搜尋與排序的參數傳道DAL去處理??

      但如果用EF去實作的話,因為他的型別是IQuerable,還沒有真正的去執行,所以我可以在service處理搜尋排序的邏輯部分
      不知道這樣的觀念是不是有問題


      一兩個月前,我第一次看這一系列文章,是幾乎看不太懂,最近再複習一次,已經可以知道文章在說什麼,
      但要真的理解與會用大概又要一段時間了,
      我會利用時間去研讀老師補充的系列文章,真的非常感謝老師用心的補充與回覆,

















      刪除
    4. Hello, 你好(下次的回覆,最後面就別空白那麼多行)
      有關 Service 的定義,我是覺得你太強調所謂的「正確答案」,在程式設計裡,一個名詞在不同的使用情境與架構下就會有不同的解釋與定義,例如 ViewModel,如果說是用在 MVVM 模式下,那麼實作的內容就會與我們在 MVC 裡所使用的 ViewModel 定議會有所不同。
      蔡老師在他文章裡所定義的服務層,他的說明是針對他所設計的架構來做解釋,而我的架構所定義的 Service 就與他的有明顯不同,然而能說誰對誰錯,或是誰的解釋與定義就比較好嗎?
      就如同我時常在文章裡所說的,用什麼樣的方式與解法,或是要怎麼設計架構,沒有一套所謂的標準模式,沒有所謂的正確答案,你要去通盤瞭解過整個架構之後,才會知道用何種實作方式與使用什麼樣的技術才會比較適合。
      而 Service 的定義,就蔡老師的做法與定義,你要先瞭解他在那一系列的文章所設計的架構,進而去瞭解為何他做這樣的方層,以及為何各層的定義與實作的內容,才能夠明白他所定義的意涵。
      而我的定義就是以一個相當基本且最常見的做法來去架構與定義,所以與蔡老師所定義的 Service 就會有很大的不同。
      .
      如同我一開始所說的,程式設計與架構沒有一個正確答案或是正確的實作方式,因為每個人所認知與所理解以及所設計的內容都會有所不同,我必須說你還需要多多獲取這方面的資訊,這方面的資訊以英文的內容佔絕大多數,中文的內容相當少,所以要多看外國人所寫的技術文章,至於那邊可以看到這些資訊呢?我想「如何尋找相關資料」也是一門你需要練習的課程,多多練習怎麼使用 Google 來查詢這部分的資料就是第一門課。
      .
      再來我是覺得你說你是一個初學者,我不曉得你是 .NET 的初學者,還是 ASP.NET 或 ASP.NET MVC 的初學者,不管是那一種,在你這個階段並不適合看這方面的文章,我在分層架構的第一篇文章一開始雖然有說過這系列是針對「初學者」,這個初學者所指的是對於分層架構的初學者,而並不是程式設計的初學者,初學者應該要先把基本的操作給學好,基本的觀念建立起來,因為怎麼做分層對於一個初學者來說太進階了,程式設計的基本都還沒有打好基礎就要做分層架構,有如地基沒有挖好就想要蓋摩天大樓。
      .
      多看、多練習、多實作、多討論,這是學習程式的不二法門,我從初學到現在,我也只敢說我只是略懂,我看很多的文章,我做很多的練習,光是在我電腦就有好幾把個練習的 Lab 專案,我以前曾經自大的去自己實作開發 ORM Solution,當然我不可能有能力完成,但是這一個過程讓我學習到很多的觀念與做法,這些經驗就成了我之後程式開發的養分,在每次工作上的實作,我盡可能將我的想法給實現在專案上,在專案上驗證我所練習的技術與觀念,然後當有問題時,我盡可能與工作伙伴、同事做討論,因為每個人的觀念、想法都不同,藉由一次次的討論,修正自己的想法與觀念,同時也 feedback 給一同討論的同事,讓大家在同一個專案或同一個公司裡有一致的做法。
      .
      有疑惑是一件好事,多處提問與討論也是一件好事,但是別追求所謂的正確答案與唯一解法。
      .
      再來就是你所說的 DAL 與 BLL 的問題,有關排序與搜尋,以一般情境來說,並不算是商業邏輯,是必須要在 DAL 去做的,什麼是「商業邏輯」?你必須要先去搞清楚。
      何謂 IQueryable ? 何謂 IEnumerable,何時要用那一個?兩種要如何區別什麼情境要使用哪一種?
      這些必須要先弄清楚之後再去討論接下來的觀念與分層架構的做法會比較有實質意義。
      .
      通常我都會告知初學者,尤其是程式開發與 ASP.NET MVC 或 ASP.NET WebForm 的初學者,我會請他們千萬不要看這一系列的分層架構文章,因為看了只會害了初學者,如果沒有一定的基礎與觀念而依樣畫葫蘆的模仿,一定會碰到很多很多很多的問題,於是我都會請他們先用傳統的方式做了幾個專案,並在這幾個專案裡去找出問題,這些過程都經歷過之後再來看這系列的文章,才會知道我是在說什麼,不然看多了也只是浪費時間、徒增煩惱。
      所以我在這邊也要跟你說,請忘了這系列的文章,請照著你公司之前專案的做法做過一次,去發現問題、釐清問題點、試著找出解決方式,然後多經歷幾次專案,這些經歷過了之後再回頭來看這系列的文章。
      .
      以上

      刪除
    5. 我明白了,
      目前自己經歷mvc專案算是第二個而已,我會先把自己專案弄更清楚再繼續往下走,
      感謝kevin老師的指導,希望自己過一段時間有所成長再來分享這好消息^_^

      刪除
  15. 您好,我想請問一下,若每個Entity都會有一個Service,如果查詢是需要Join多個tables,該如何使用?
    也就是說,Service之間提供的資料要如何join呢?我原先是在GenericRepository設置一個屬性:
    public IQueryable Table
    {
    get
    {
    return this.GetAll();
    }
    }
    需要Join時則像下面:
    using(SystemsService SysSrv = new SystemsService())
    {
    var Roles = from r in Repo.Table
    join s in SysSrv.Table
    on r.SYSTEM_SERIAL_ID equals s.SERIAL_ID
    where s.SYSTEM_NO == SYSTEM_NO
    select r;

    return Roles;
    }
    但我發現這種作法,若Db裡沒資料,撈回來也無法判斷是否為null也無法使用.Any()擴充方法判斷。導致一直出現null reference的錯誤。
    請問老師能否提供建議?謝謝!

    回覆刪除
    回覆
    1. 首先我要說的是,我並沒有說一個 Entity 只能對應一個 Service 或是 一個 Service 只能處理一個 Entity 類別的資料。
      然後為何撈回來沒有資料而無法做判斷呢?總是有辦法處理的呀?
      使用 LINQ 透過 EF 去操作資料,可以先使用 LINQPad 進行嘗試與練習,
      以你的程式來看,我不太懂為何 Service 與 Repository 會交錯混用呢?

      刪除
    2. 抱歉,我問題表達的不清楚,以您的範例 ProductService.cs 來說,
      程式裡面宣告了宣告了 private IRepository repository = new GenericRepository();
      故對 Products 的 CRUD 和一些資料查詢則直接使用 GenericRepository 裡的方法。
      我的問題是,假如今天要提供的資料是需 Product Join OrderDetail ,已經先宣告一個ViewModel 來對應預計撈回來兩個 table 的欄位,
      但真正在撈資料的程式部分該怎麼做比較好?
      1. 在 Service 裡使用 DbContext 直接寫 Entities Join 的 LINQ 而不透過 repository (這樣似乎破壞了repository的本意?)
      2. 若是在Service裡宣告這兩個 table 的 repository 來處理,除了我前一個問題裡用的方法外(在GenericRepository設置一個屬性傳回GetAll()),我不懂還能怎麼做。

      謝謝!

      刪除
    3. 後來想想,有時候也的確會在 Service 裡去取用別的 Service 某個方法所回傳的資料,然後在再使用 Repository 取得資料後再去混用,只是看到你的程式寫法是有讓我有些訝異...

      刪除
    4. 的確 Service 直接對 DbContext 做操作就完全失去做分層操作的意義。
      有個觀念的地方要提醒的是,雖然現在透過 ORM 操作物件資料都會是一個 table 對應一個物件,但是以物件導向的做法上,也是有不同資料儲存體對映到一個 Entity 類別,或是多個 Table 對映到一個 Entity 類別,有有可能是一個 View 一個 Stored Procedure 對映到一個 Entity 類別,所以在程式裡不會再去說 table,而是以物件為單位。
      .
      一個 Service 可以操作多個物件的資料處理,別再我的範例裡面打轉,這是以學習分層的初學者為出發點的系列文章,範例當然都是簡單的狀況,一旦遇到進階的狀況時,當然就必須要舉一隅而三隅反囉。
      .
      因為我不清楚你的程式開發經歷,你也可以作為參考。
      另一種分層架構的做法就是不會有 Repository 這一層,這是在於專案的資料來源都是單一透過 ORM 向資料庫作存取,
      像 EF, NHibernate 這一類所提供的 DbContext 或 Session 都已經是具備了 Repository 的操作處理,
      在這樣的專案架構下,如果再疊一層 Repository 就會多一層重疊,所以也可以考慮。
      .
      http://stackoverflow.com/questions/18727432/sharprepository-join-between-two-repositories
      http://tech.pro/blog/1191/say-no-to-the-repository-pattern-in-your-dal

      刪除
    5. 謝謝您這麼詳細的說明,看到第三段有種豁然開朗的感覺!:)
      因為是第一次嘗試 repository pattern,照著這系列文章 step by step 把專案建起來,不免有些想法就直接被這些 Sample 給框住了~ XD

      刪除
  16. 謝謝Kevin大,看完這系列的文章對分層架構總算有些概念,也開始應用到專案上,遇到一些跟這系列文章不太相關的問題,想請教您。
    實務上會遇到某些欄位不讓使用者修改,例如會員資料裡的email欄位不能修改,我會加入下方的程式碼:
    DbContext.Entry(member).State = EntityState.Modified;
    DbContext.Entry(member).Property(o => o.Email).IsModified= false;
    DbContext.SaveChanges();

    但是現在這部分移到 GenericRepository.Update() 裡面去了,
    想用方法多載另外寫個可以傳排除欄位參數的 GenericRepository.Update(排除的欄位們),
    在 Service 層只要把要排除更新的欄位傳進去就好了,
    可是不曉得要怎麼把 DbContext.Entry(member).Property(o => o.Email).IsModified= false 這裡面的 Lambda 部分 (o => o.Email) 當參數傳遞?

    回覆刪除
    回覆
    1. 建議之後如果有相關討論,請使用在左邊可以看到的「詢問與建議」,因為這類的討論在這個留言板裡並不好描述。
      有關資料存取層的處理,我並不會使用你的做法,因為這已經是商業邏輯去更改了資料存取的規則,
      這一類的處理其實從輸入的前端網頁然後到後端的 Controller,這其實可以使用 ViewModel 或是一些處理程序讓使用者或是資料模型不會包含到不想要被改變的欄位,我想你所建立的專案,應該都還是大部分從頭到尾都是使用 Entity Framework 所產生的模型類別吧.
      .
      把握一個原則,資料存取層的操作應該都要一致性,而且不能夠輕易被更動,因為資料的存取都是一致的,不會因為哪一份資料的不同而去做特別處理,例如產品資料與客戶資料或是訂單資料,不論怎麼寫入資料庫,或是從資料庫讀出來,都是使用相同的方法,不應該為了某一個類別或是某一個欄位而去做改變。
      .
      你所提到的情境,其實在 MVC 網站這一層裡就可以直接解決,可以使用 ViewModel 或是其他處理,例如你不想讓會員的 Email 被修改,那麼在 Controller 的 Action 方法就能夠去做處理,不論是輸入或是輸出。
      如果不想要在 Controller 這邊做更動,那麼 Service 就是處理商業邏輯的地方,MVC 網站是處理資料的輸入、輸入資料的驗證以及資料的輸出,所以一般而言,大部分的商業邏輯處理都是在 Service 這一層去做操作,所以你的情境就是要在 Service 去處理。
      另外不要去想怎麼用 LINQ 去對原生的模型類別的資料存取做處理,成本太高,影響整個系統的處理很大,得不償失,萬一今天你的客戶跟你說他的機器、資料庫系統環境是無法允許使用 Entity Framework 的時候,你就無法再使用同樣的方式去處理了。
      .
      所以不要輕易的去對資料存取層的操作去做任何的客製化改變,除非你能夠完全掌握 EF 的內容與所有流程,否則請盡量在 Service 層與 Controller 裡去做這部分的處理。

      刪除
    2. 我的專案真的幾乎都用 Entity Framework 產生的 model 來傳遞資料,謝謝 Kevin 大提點。
      最後想到的方法是在 Service 層裡,用 GenericRepository.Get() 讀取要更新的那筆資料,把不想被修改的欄位設為讀取出來的值再作更新,這樣符合 Kevin 大說的資料存取層操作一致性以及在 BLL 來作處理,不過缺點是 Update 時會多一個讀取的動作。

      刪除
  17. Kevin你好,有個點想與你請教,我覺得這個架構下web層不應該直接參考model層,否則開發web層的人還是可以不透過service層就對資料動手腳,而這個架構下web層參考model層的目的應該是使用entity class,所以我傾向把entity在拉出去獨立成一個library,不知道你認為如何?

    回覆刪除
    回覆
    1. Hello, 你好
      其實你所看到的是因為這個範例的 Context 比較小,而且的確在實務上,我是讓表現層是完全與 Model 層隔開的,各層之間的資料傳遞都是使用兩層之間所定義的 DTO 類別,也就是說展現層只會跟 Service 層溝通,而不會碰到 Model 層。
      .
      你要瞭解到,這系列的範例並不是很大,是以一個小小的情境描述來做說明,因為說得太大反而讓我不好寫文章,也無法讓閱讀的讀者能夠瞭解,這是屬於入門級的介紹說明,真的要講到系統軟體架構要怎麼界定與使用,我想以短短篇幅的幾篇文章是無法說清楚講明白的。
      .
      至於你說的要把 Entity 類別給獨立出去,其實在大型系統架構裡,這會屬於 Domain Model 的定義,也的確這樣的做法在大型系統裡會更明確的將各個專案之間做清楚的隔離,但是這也會讓提高系統實作與維護的複雜度,大型系統是有必要這麼做,因為要避免混亂與責任不清的情況發生,但是中型專案是不是這樣做,這就要看團隊如何去做規劃,很多時候所開發的系統並不是很大,實在沒有必要做太多層的切分,而在這樣的情況下,就要靠團隊是否有做明確規範以及落實 Code Review。在系統架構與開發時程、開發成本的拿捏就要看一開始架構系統的工程師如何去規劃,以及之後團隊是否有明文規定與默契去遵守了。

      刪除
    2. 再補充,其實在我工作上所開發的專案,大多數的情況下,展現層是不會與 Model 層有任何的接觸,所使用的物件類別與 Entity Class 或 Donmain Model 並不會是相同的。

      刪除
  18. 謝謝你的回覆,一開始會想把entity抽出去是因為不想在model與web層都有相同的class,你的第一個回復有幫我解惑了,謝謝。

    回覆刪除
  19. 您好想請問關於Service層,Service層是否為根據需要的處理方式而去實作方法呢?例如我有一個MerberService,頁面有登入的需求,是否就是在IMemberService加入一個bool TryLogin(string id,string pwd)然後去實做呢?謝謝

    回覆刪除
    回覆
    1. Hello, 你好
      我覺得你所問的應該是基本的物件導向觀念裡 interface 的使用,以下為參考
      MSDN - 介面 (C# 程式設計手冊)
      https://msdn.microsoft.com/zh-tw/library/ms173156.aspx

      建議
      先把物件導向的觀念與基礎給打穩之後再來看這一系列,否則只會害了自己

      刪除
    2. 你好,因被要求使用此分層方式開發(目前是直接使用DbContext)比如要做一個登入的判斷就是:
      DbContext.Members.Where(m=>m.Id==Id && m.Pwd==pwd).FirstOrDefault();
      所以我想請問的是若使用Service層處理登入的判斷是否就是為其增加一個類似bool TryLogin(string id,string pwd)
      然後再Controller:
      if(MemberService.TryLogin(id,pwd))
      這種作法呢?
      初次學習新方式,如有眾多疑問還請解惑,謝謝。

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

      刪除
    4. Hello,
      因為被要求使用分層方式作開發,但前提是你要先把基礎給打好才用吧
      F1賽車很快,但不是每個人坐上賽車後就可以駕馭
      這系列的分層做法是從一個基礎的簡單網站去做重構然後再分出每個層
      但是在實務開發上,應該是先把你的需求都整理好,找出系統的物件,
      再找出圍繞在這些物件所要處理的事情,如此一來就可以抓出要做哪些功能以及會需要用到哪些資料,
      在系統分析階段去找出上面的內容,SA or SD 就可以先定義設計出物件與介面,
      而後在分派到工程師,工程師再去把介面所定義的實作類別給寫出來,
      並不是說你要多做一個功能就再去介面去增加行為的定義,不是這樣的,這麼做就本末倒置了,
      依據你現在的做法,你心中不會有「為什麼要做介面?介面一點意義都沒有?為何要去定義沒有用處的介面」這些想法嗎?

      刪除
    5. 有人要求你用分層做系統,就應該請他告訴你為何要這麼做,
      如果只叫你來看這系列文章或是叫你照著這系列文章去做,而沒有任何說明也沒有考量你的程度,
      我明白講,他是在害你。
      看看這系列每篇文章一開始我用紅色粗體字所寫的提示訊息
      然後再認真的去想想,你目前所開發的系統是否有必要這麼做,
      在沒有基礎的情況下,你還要繼續用分層的方式去開發系統嗎?不用分層的做法是不是可以做得出來?
      不用分層的方式去做是否可以做得好,是否可以達到物件導向的標準與要求並且遵照 SOLID 呢?

      刪除
  20. HI,目前剛接觸所謂的分層式架構,當然有這些疑問,目前也是到處找資料參考,為什麼這麼做?用意是什麼?
    目前也不是很資深,剛投入IT才半年左右,最後謝謝您的回覆!

    回覆刪除
  21. 抱歉再次詢問,目前與同事討論後有初步了解,目前的專案是使用WebForm,於您的程式碼中有看見Repository有Dispose方法但沒有呼叫,是否因為MVC與WEBFORM有所差異?若使用WEBFORM是否需要寫一個PageBase去呼叫呢?另外使用Service層又如何使用Repository的Dispose?謝謝。

    回覆刪除
    回覆
    1. 你好,其實這系列文章的概念與做法,不管是用 WebForm 或是 MVC 還是 Web Api,都是適用的,
      並沒有因為使用那一種而有所差異,商業邏輯與資料存取邏輯是不會影響展現層要怎麼顯示資料。

      至於 Dispose,這系列到這一篇為止的確是還沒有在程式裡去看到有用到這些 Dispose 方法的地方,但不是建立之後就不管,有建立就會有用到,什麼時候用到呢?大概你們看到這一篇就卡住了,這不應該是個會讓人卡住的點,不過後續的文章裡我也似乎沒有再去處理 dispose 的部分,必須要說,在我工作的實務開發上,都是會在各個 Controller 的 Dispose 方法裡去執行有用到的 Service 與相關物件 的 Dispose 方法,而 Service 也有在其 Dispose 方法裡去執行相依物件的 Dispose 方法,如此一環扣一環,這是一系列的,所以要看到最後再來通盤討論,而不是還沒到最後就去下定論。

      https://msdn.microsoft.com/zh-tw/library/dd492966(v=vs.118).aspx

      WebForm 是否建立 PageBase?
      這不一定吧,看你的需求,因為我只能這麼說,因為有的專案小,沒必要去建立,但也不是專案大就應該建立,
      而是要看你要建立這個基礎類別拿來做什麼事情,而不應該來問我,要問自己或是團隊去討論。

      最後我還是希望你們團隊不要參考這系列文章去做你們的專案,感覺到你們並不是很熟悉一些基礎,但又想要將分層給應用到專案裡,對此我還是那句話,如果你們團隊的成員所做的專案都沒有超過 10 個或是初出茅廬,先建立雛形出來吧,不要將架構訂死,保留可以快速修改的彈性,先把功能給做出來,把功能先做出來並且可以正確執行以及符合需求,之後再來考慮重新整理架構的問題,因為東西都不見得可以如期完成的狀況下,講什麼架構的話都只是空談。

      對了,以下的系列你們也應該看看,並不是叫你們去模仿或照著做,而是看看裡面我是怎麼做的,
      是不是會用到 IoC Container,這不是重點,現階段你們不需要用到,或是搞不懂那是什麼的,就不要用,
      http://kevintsengtw.blogspot.tw/2013/05/aspnet-webform-simple-injector.html
      https://github.com/kevintsengtw/ASPNET_MVC_WebForm_Repository_Sample
      https://www.youtube.com/watch?v=cuaa3i-Q9xY
      https://docs.com/is-twMVC/9465/asp-net-mvc-model-twmvc-10

      刪除
    2. Dispose的部分我了解了,PageBase的部分是想說複寫Dispose,就不需重複寫很多的Service,全部放在PageBase,在PageBase的Dispose去執行每個Service的Dispose而Service的Dispose在呼叫Repository的Dispose。
      目前剛學習此種設計方法,謝謝您的教學與意見!

      刪除
  22. 就個人的理解和看法,Repo是較Enterprise級數的才需要,因為配合DI/IoC做Unit Test。
    另外Repo其實某程度上跟Entity Framework輸出的Context是有一定重複性,本身context已經有Abstract layer,有UnitOfWork,再另起一個Repo跟Context溝通,過程上其實有點冗長。

    而且維護Service/Repo都會花上不少時間,個人認為,Design Pattern是重要,但不要盲目追求。
    就一般而言,有Service Layer(或者其他命名),做一個Logic+DAL存取點其實已經很夠。

    回覆刪除
  23. 作者已經移除這則留言。

    回覆刪除
  24. 看到這一篇後開始有幾個疑問
    1.Service是為了針對一個Table Class去做處理,如果系統內有大量的Table不就一堆Service ?
    2.Service的interface都有CRUD,是否再拉一個interface(CRUD)去給IService繼承 ?
    3.承接2.,那IService是否也如同之前的ICategory去定義非CRUD的一些方法 ?
    4.以實際的狀況來看,Web層的controller應該不會只有單純的操作一個Service,可能三四個table做join的情況顯示在View上,那不就又變成我在control上面對這些Service取回的資料作join的情況出現 ? 這樣DAL又回到UI層了。
    5.承4.,那如果是如此,是否乾脆一開始在DAL層就採用SQL Script的方式將資料串連後再丟回UI層,再把資料丟到對應的ViewModel上 ?

    回覆刪除
  25. 承接上面的問題,如果要解決4. 5.的問題,勢必要把一些較複雜多table組成、運算的資料建立在View裡面,在透過Entity使用出來,這樣在實際上DB Side就會產生一堆View出來,畢竟Linq對複雜的SQL本身就可讀寫性較為不佳,這樣我乾脆建立Class去承接SQL回拋的資料結構。

    回覆刪除
    回覆
    1. service 的設計不是依照 Table 而是依據功能與職責
      repository 的設計所看到的大多是依據 table 而建立,但應該是依據資料,有可能一筆資料會由多個 TABLE 整合出來
      我想這應該可以回答你所提出的幾個問題了吧
      實務開發所面臨到的情境比這幾篇文章的範例要複雜得多
      但是三層式的開發要做就要確實,不可以展示層跳過服務層直接向資料存取層做溝通
      一旦打破這原則,專案的破窗就會出現(請查詢破窗效應)
      當第一個破窗出現就會無限發散,專案的問題也就著出現,尤其是多人開發的大型專案

      刪除
    2. maggiore 以下是我的作法 :
      如果你要接 SQL 回拋的資料,個人建議是放在 repo
      針對回拋的資料處理由 service 去拿 repo

      刪除

提醒

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