Walidacja z użyciem warstwy usług (C#)

Autor: Stephen Walther

Dowiedz się, jak przenieść logikę weryfikacji z akcji kontrolera i do oddzielnej warstwy usługi. W tym samouczku Stephen Walther wyjaśnia, jak można zachować ostrą separację problemów przez odizolowanie warstwy usługi od warstwy kontrolera.

Celem tego samouczka jest opisanie jednej metody weryfikacji w aplikacji ASP.NET MVC. Z tego samouczka dowiesz się, jak przenieść logikę weryfikacji z kontrolerów i do oddzielnej warstwy usługi.

Oddzielanie obaw

Podczas tworzenia aplikacji ASP.NET MVC nie należy umieszczać logiki bazy danych wewnątrz akcji kontrolera. Mieszanie logiki bazy danych i kontrolera sprawia, że aplikacja jest trudniejsza do utrzymania w czasie. Zaleceniem jest umieszczenie całej logiki bazy danych w oddzielnej warstwie repozytorium.

Na przykład lista 1 zawiera proste repozytorium o nazwie ProductRepository. Repozytorium produktów zawiera cały kod dostępu do danych dla aplikacji. Lista zawiera również interfejs IProductRepository implementujący repozytorium produktów.

Lista 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();
    }

}

Kontroler w liście 2 używa warstwy repozytorium w akcjach Index() i Create(). Zwróć uwagę, że ten kontroler nie zawiera żadnej logiki bazy danych. Tworzenie warstwy repozytorium umożliwia zachowanie czystej separacji problemów. Kontrolery są odpowiedzialne za logikę sterowania przepływem aplikacji, a repozytorium jest odpowiedzialne za logikę dostępu do danych.

Lista 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");
        }

    }
}

Tworzenie warstwy usługi

Dlatego logika sterowania przepływem aplikacji należy do kontrolera, a logika dostępu do danych należy do repozytorium. W takim przypadku, gdzie umieścisz logikę weryfikacji? Jedną z opcji jest umieszczenie logiki walidacji w warstwie usługi.

Warstwa usługi to dodatkowa warstwa w aplikacji ASP.NET MVC, która mediatuje komunikację między kontrolerem a warstwą repozytorium. Warstwa usługi zawiera logikę biznesową. W szczególności zawiera logikę walidacji.

Na przykład warstwa usługi produktu w pozycji Listing 3 ma metodę CreateProduct(). Metoda CreateProduct() wywołuje metodę ValidateProduct(), aby zweryfikować nowy produkt przed przekazaniem produktu do repozytorium produktów.

Lista 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();
    }
}

Kontroler produktu został zaktualizowany na liście 4, aby użyć warstwy usługi zamiast warstwy repozytorium. Warstwa kontrolera komunikuje się z warstwą usługi. Warstwa usługi komunikuje się z warstwą repozytorium. Każda warstwa ma osobną odpowiedzialność.

Lista 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");
        }

    }
}

Zwróć uwagę, że usługa produktu jest tworzona w konstruktorze kontrolera produktu. Po utworzeniu usługi produktu słownik stanu modelu jest przekazywany do usługi. Usługa produktu używa stanu modelu do przekazywania komunikatów o błędach weryfikacji z powrotem do kontrolera.

Oddzielenie warstwy usługi

Nie udało nam się odizolować warstw kontrolera i usługi w jednym zakresie. Warstwy kontrolera i usługi komunikują się za pośrednictwem stanu modelu. Innymi słowy warstwa usługi ma zależność od określonej funkcji platformy ASP.NET MVC.

Chcemy jak najwięcej odizolować warstwę usługi od warstwy kontrolera. Teoretycznie powinniśmy mieć możliwość używania warstwy usługi z dowolnym typem aplikacji, a nie tylko aplikacją ASP.NET MVC. Na przykład w przyszłości możemy chcieć utworzyć fronton WPF dla naszej aplikacji. Powinniśmy znaleźć sposób usunięcia zależności od ASP.NET stanu modelu MVC z naszej warstwy usługi.

Na liście 5 warstwa usługi została zaktualizowana, aby nie używała już stanu modelu. Zamiast tego używa dowolnej klasy, która implementuje interfejs IValidationDictionary.

Lista 5 — Models\ProductService.cs (oddzielone)

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();
    }
}

Interfejs IValidationDictionary jest zdefiniowany w liście 6. Ten prosty interfejs ma jedną metodę i jedną właściwość.

Lista 6 — Models\IValidationDictionary.cs

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

Klasa w liście 7 o nazwie ModelStateWrapper klasy implementuje interfejs IValidationDictionary. Wystąpienie klasy ModelStateWrapper można utworzyć, przekazując słownik stanu modelu do konstruktora.

Lista 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
    }
}

Na koniec zaktualizowany kontroler na liście 8 używa klasy ModelStateWrapper podczas tworzenia warstwy usługi w konstruktorze.

Lista 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");
        }

    }
}

Użycie interfejsu IValidationDictionary i klasy ModelStateWrapper umożliwia całkowite odizolowanie warstwy usługi od warstwy kontrolera. Warstwa usługi nie jest już zależna od stanu modelu. Możesz przekazać dowolną klasę, która implementuje interfejs IValidationDictionary w warstwie usługi. Na przykład aplikacja WPF może zaimplementować interfejs IValidationDictionary z prostą klasą kolekcji.

Podsumowanie

Celem tego samouczka było omówienie jednego podejścia do przeprowadzania walidacji w aplikacji MVC ASP.NET. W tym samouczku przedstawiono sposób przenoszenia całej logiki weryfikacji z kontrolerów i do oddzielnej warstwy usługi. Przedstawiono również sposób izolowania warstwy usługi od warstwy kontrolera przez utworzenie klasy ModelStateWrapper.