Share via


驗證與服務層 (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 () 動作中使用存放庫層。 請注意,此控制器不包含任何資料庫邏輯。 建立存放庫層可讓您維持清楚的考慮區隔。 控制器負責應用流程式控制制邏輯,而存放庫則負責資料存取邏輯。

清單 2 - 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 中更新為使用服務層,而不是存放庫層。 控制器層會與服務層交談。 服務層會與存放庫層交談。 每個圖層都有個別的責任。

清單 4 - 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。

清單 8 - 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 類別,將服務層與控制器層隔離。