共用方式為


驗證與服務層 (C#)

作者:Stephen Walther

了解如何將驗證邏輯從控制器動作移出,並移至個別的服務層。 在此教學課程中,Stephen Walther 說明如何藉由將服務層與控制器層隔離,以保持關注點的明確分離。

此教學課程的目標是描述一種在 ASP.NET MVC應用程式中執行驗證的方法。 在此教學課程中,您會了解如何將驗證邏輯移出控制器,並移入個別的服務層。

分離關注點

當您建置 ASP.NET MVC 應用程式時,您不應將資料庫邏輯放在控制器動作內。 混合資料庫和控制器邏輯可讓您的應用程式在過了一段時間後更難以維護。 建議您將所有資料庫邏輯放在不同的存放庫層中。

例如,清單 1 包含名為 ProductRepository 的簡單存放庫。 產品存放庫包含應用程式的所有資料存取程式碼。 此清單也包含產品存放庫實作的 IProductRepository 介面。

清單 1 -- Models\ProductRepository.cs

using System.Collections.Generic;
using System.Linq;

namespace MvcApplication1.Models
{
    public class ProductRepository : MvcApplication1.Models.IProductRepository
    {
        private ProductDBEntities _entities = new ProductDBEntities();

        public IEnumerable<Product> ListProducts()
        {
            return _entities.ProductSet.ToList();
        }

        public bool CreateProduct(Product productToCreate)
        {
            try
            {
                _entities.AddToProductSet(productToCreate);
                _entities.SaveChanges();
                return true;
            }
            catch
            {
                return false;
            }
        }

    }

    public interface IProductRepository
    {
        bool CreateProduct(Product productToCreate);
        IEnumerable<Product> ListProducts();
    }

}

清單 2 中的控制器會在其 Index() 和 Create() 動作中使用存放庫層。 請注意,此控制器不包含任何資料庫邏輯。 建立存放庫層可讓您維護清楚的關注點分離。 控制器負責應用程式流程控制邏輯,而存放庫則負責資料存取邏輯。

清單1 - Controllers\ProductController.cs

using System.Web.Mvc;
using MvcApplication1.Models;

namespace MvcApplication1.Controllers
{
    public class ProductController : Controller
    {
        private IProductRepository _repository;

        public ProductController():
            this(new ProductRepository()) {}

        public ProductController(IProductRepository repository)
        {
            _repository = repository;
        }

        public ActionResult Index()
        {
            return View(_repository.ListProducts());
        }

        //
        // GET: /Product/Create

        public ActionResult Create()
        {
            return View();
        } 

        //
        // POST: /Product/Create

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create([Bind(Exclude="Id")] Product productToCreate)
        {
            _repository.CreateProduct(productToCreate);
            return RedirectToAction("Index");
        }

    }
}

建立服務層

因此,應用程式流程控制邏輯屬於控制器,而資料存取邏輯則屬於存放庫。 在此情況下,您要將驗證邏輯放在何處? 其中一個選項是將您的驗證邏輯放在 服務層中。

服務層是 ASP.NET MVC 應用程式中的額外層,可協調控制器和存放庫層之間的通訊。 服務層包含商業邏輯。 特別是包含驗證邏輯。

例如,清單 3 中的產品服務層具有 CreateProduct() 方法。 CreateProduct() 方法會先呼叫 ValidateProduct() 方法來驗證新產品,再將產品傳遞至產品存放庫。

清單 3 - Models\ProductService.cs

using System.Collections.Generic;
using System.Web.Mvc;

namespace MvcApplication1.Models
{
    public class ProductService : IProductService
    {

        private ModelStateDictionary _modelState;
        private IProductRepository _repository;

        public ProductService(ModelStateDictionary modelState, IProductRepository repository)
        {
            _modelState = modelState;
            _repository = repository;
        }

        protected bool ValidateProduct(Product productToValidate)
        {
            if (productToValidate.Name.Trim().Length == 0)
                _modelState.AddModelError("Name", "Name is required.");
            if (productToValidate.Description.Trim().Length == 0)
                _modelState.AddModelError("Description", "Description is required.");
            if (productToValidate.UnitsInStock < 0)
                _modelState.AddModelError("UnitsInStock", "Units in stock cannot be less than zero.");
            return _modelState.IsValid;
        }

        public IEnumerable<Product> ListProducts()
        {
            return _repository.ListProducts();
        }

        public bool CreateProduct(Product productToCreate)
        {
            // Validation logic
            if (!ValidateProduct(productToCreate))
                return false;

            // Database logic
            try
            {
                _repository.CreateProduct(productToCreate);
            }
            catch
            {
                return false;
            }
            return true;
        }

    }

    public interface IProductService
    {
        bool CreateProduct(Product productToCreate);
        IEnumerable<Product> ListProducts();
    }
}

產品控制器已在清單 4 中更新,以使用服務層,而不是存放庫層。 控制器層會與服務層交談。 服務層會與存放庫層交談。 每一層有各自不同的責任。

清單1 - Controllers\ProductController.cs

Listing 4 – Controllers\ProductController.cs
using System.Web.Mvc;
using MvcApplication1.Models;

namespace MvcApplication1.Controllers
{
    public class ProductController : Controller
    {
        private IProductService _service;

        public ProductController() 
        {
            _service = new ProductService(this.ModelState, new ProductRepository());
        }

        public ProductController(IProductService service)
        {
            _service = service;
        }

        public ActionResult Index()
        {
            return View(_service.ListProducts());
        }

        //
        // GET: /Product/Create

        public ActionResult Create()
        {
            return View();
        }

        //
        // POST: /Product/Create

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create([Bind(Exclude = "Id")] Product productToCreate)
        {
            if (!_service.CreateProduct(productToCreate))
                return View();
            return RedirectToAction("Index");
        }

    }
}

請注意,產品服務是在產品控制器建構函式中建立的。 建立產品服務時,模型狀態字典會傳遞至服務。 產品服務會使用模型狀態將驗證錯誤訊息傳回控制器。

分離服務層

一方面,我們無法區隔控制器和服務層級。 控制器和服務層會透過模型狀態進行通訊。 換句話說,服務層相依於 ASP.NET MVC 架構的特定功能。

我們想要盡可能將服務層與控制器層區隔。 理論上,我們應該能夠將服務層與任何類型的應用程式搭配使用,而不只是 ASP.NET MVC 應用程式。 例如,未來我們可能會為應用程式建置 WPF 前端。 我們應該找到方法,從服務層中移除 ASP.NET MVC 模型狀態的相依性。

在清單 5 中,服務層因已更新,所以不再使用模型狀態。 相反的,它會使用任何實作 IValidationDictionary 介面的類別。

清單 5 - Models\ProductService.cs (分離)

using System.Collections.Generic;

namespace MvcApplication1.Models
{
    public class ProductService : IProductService
    {

        private IValidationDictionary _validatonDictionary;
        private IProductRepository _repository;

        public ProductService(IValidationDictionary validationDictionary, IProductRepository repository)
        {
            _validatonDictionary = validationDictionary;
            _repository = repository;
        }

        protected bool ValidateProduct(Product productToValidate)
        {
            if (productToValidate.Name.Trim().Length == 0)
                _validatonDictionary.AddError("Name", "Name is required.");
            if (productToValidate.Description.Trim().Length == 0)
                _validatonDictionary.AddError("Description", "Description is required.");
            if (productToValidate.UnitsInStock < 0)
                _validatonDictionary.AddError("UnitsInStock", "Units in stock cannot be less than zero.");
            return _validatonDictionary.IsValid;
        }

        public IEnumerable<Product> ListProducts()
        {
            return _repository.ListProducts();
        }

        public bool CreateProduct(Product productToCreate)
        {
            // Validation logic
            if (!ValidateProduct(productToCreate))
                return false;

            // Database logic
            try
            {
                _repository.CreateProduct(productToCreate);
            }
            catch
            {
                return false;
            }
            return true;
        }

    }

    public interface IProductService
    {
        bool CreateProduct(Product productToCreate);
        IEnumerable<Product> ListProducts();
    }
}

IValidationDictionary 介面會在清單 6 中定義。 這個簡單介面具有單一方法和單一屬性。

清單 6 - Models\IValidationDictionary.cs

namespace MvcApplication1.Models
{
    public interface IValidationDictionary
    {
        void AddError(string key, string errorMessage);
        bool IsValid { get; }
    }
}

清單 7 中的類別,具名 ModelStateWrapper 類別,實作 IValidationDictionary 介面。 您可以將模型狀態字典傳遞至建構函式,具現化 ModelStateWrapper 類別。

清單 7 - Models\ModelStateWrapper.cs

using System.Web.Mvc;

namespace MvcApplication1.Models
{
    public class ModelStateWrapper : IValidationDictionary
    {

        private ModelStateDictionary _modelState;

        public ModelStateWrapper(ModelStateDictionary modelState)
        {
            _modelState = modelState;
        }

        #region IValidationDictionary Members

        public void AddError(string key, string errorMessage)
        {
            _modelState.AddModelError(key, errorMessage);
        }

        public bool IsValid
        {
            get { return _modelState.IsValid; }
        }

        #endregion
    }
}

最後,清單 8 中更新的控制器,會在建構函式中建立服務層時使用 ModelStateWrapper。

清單1 - Controllers\ProductController.cs

using System.Web.Mvc;
using MvcApplication1.Models;

namespace MvcApplication1.Controllers
{
    public class ProductController : Controller
    {
        private IProductService _service;

        public ProductController() 
        {
            _service = new ProductService(new ModelStateWrapper(this.ModelState), new ProductRepository());
        }

        public ProductController(IProductService service)
        {
            _service = service;
        }

        public ActionResult Index()
        {
            return View(_service.ListProducts());
        }

        //
        // GET: /Product/Create

        public ActionResult Create()
        {
            return View();
        }

        //
        // POST: /Product/Create

        [AcceptVerbs(HttpVerbs.Post)]
        public ActionResult Create([Bind(Exclude = "Id")] Product productToCreate)
        {
            if (!_service.CreateProduct(productToCreate))
                return View();
            return RedirectToAction("Index");
        }

    }
}

使用 IValidationDictionary 介面和 ModelStateWrapper 類別,可讓我們完全區隔服務層與控制器層。 服務層不再相依於模型狀態。 您可以將實作 IValidationDictionary 介面的任何類別傳遞至服務層。 例如,WPF 應用程式可能會使用簡單的集合類別實作 IValidationDictionary 介面。

摘要

此教學課程的目標是探討一種在 ASP.NET MVC 應用程式中執行驗證的方法。 在此教學課程中,您已了解如何將所有的驗證邏輯移出控制器,並移入個別的服務層。 您也了解了如何藉由建立 ModelStateWrapper 類別,將服務層與控制器層區隔。