Проверка с помощью уровня службы (C#)

по Стивен Вальтер

Узнайте, как переместить логику проверки из действий контроллера в отдельный слой служб. В этом руководстве Стивен Вальтер содержит сведения о том, как можно обеспечить четкое разделение проблем, изолируя уровень службы на уровне контроллера.

Цель этого руководства — описать один метод выполнения проверки в приложении MVC ASP.NET. В этом руководстве вы узнаете, как переместить логику проверки из контроллеров в отдельный слой служб.

Отделение проблем

При создании приложения ASP.NET MVC не следует размещать логику базы данных в действиях контроллера. Смешивание базы данных и логики контроллера делает приложение более сложным для обслуживания с течением времени. Рекомендуется размещать всю логику базы данных в отдельном слое репозитория.

Например, листинг 1 содержит простой репозиторий с именем Продуктрепоситори. Репозиторий продукта содержит весь код доступа к данным для приложения. В список также входит интерфейс Ипродуктрепоситори, реализованный в репозитории продуктов.

Листинг 1 — Моделс\продуктрепоситори.КС

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. Контроллерс\продуктконтроллер.КС

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 имеет метод Креатепродукт (). Метод Креатепродукт () вызывает метод Валидатепродукт () для проверки нового продукта перед передачей продукта в репозиторий продукта.

Листинг 3. Моделс\продуктсервице.КС

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. Контроллерс\продуктконтроллер.КС

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

    }
}

Обратите внимание, что служба продукта создается в конструкторе контроллера продукта. При создании службы продукта словарь состояния модели передается в службу. Служба продукта использует состояние модели для передачи сообщений об ошибках проверки обратно в контроллер.

Отделение слоя служб

Не удалось изолировать уровни контроллера и службы в одном отношении. Уровни контроллера и службы взаимодействуют через состояние модели. Иными словами, уровень служб зависит от конкретной функции платформы MVC ASP.NET.

Мы хотим, чтобы уровень служб был как можно более изолирован от уровня контроллера. Теоретически, мы должны иметь возможность использовать слой служб с приложением любого типа, а не только ASP.NET приложение MVC. Например, в будущем может потребоваться построить внешний интерфейс WPF для нашего приложения. Мы должны найти способ удаления зависимости от состояния модели MVC ASP.NET из уровня служб.

В листинге 5 слой служб был обновлен так, чтобы он больше не использовал состояние модели. Вместо этого используется любой класс, реализующий интерфейс Ивалидатиондиктионари.

Листинг 5 — Моделс\продуктсервице.КС (отделено)

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

Интерфейс Ивалидатиондиктионари определен в листинге 6. Этот простой интерфейс имеет один метод и одно свойство.

Листинг 6. Моделс\ивалидатиондиктионари.КС

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

Класс в листинге 7 с именем класса Моделстатевраппер реализует интерфейс Ивалидатиондиктионари. Можно создать экземпляр класса Моделстатевраппер, передав в конструктор словарь состояния модели.

Листинг 7 — Моделс\моделстатевраппер.КС

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 использует Моделстатевраппер при создании слоя служб в конструкторе.

Листинг 8. Контроллерс\продуктконтроллер.КС

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

    }
}

Использование интерфейса Ивалидатиондиктионари и класса Моделстатевраппер позволяет полностью изолировать наш уровень служб от нашего уровня контроллера. Уровень службы больше не зависит от состояния модели. Можно передать любой класс, реализующий интерфейс Ивалидатиондиктионари, в слой службы. Например, приложение WPF может реализовать интерфейс Ивалидатиондиктионари с помощью простого класса коллекции.

Сводка

Цель этого учебника — обсудить один из подходов к выполнению проверки в приложении ASP.NET MVC. В этом руководстве вы узнали, как переместить всю логику проверки из контроллеров в отдельный слой служб. Вы также узнали, как изолировать слой служб от своего уровня контроллера, создав класс Моделстатевраппер.