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

Стивен Уолтер (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");
        }

    }
}

Создание уровня служб

Таким образом, логика управления потоком приложения принадлежит контроллеру, а логика доступа к данным — в репозитории. В этом случае, куда поместить логику проверки? Одним из вариантов является размещение логики проверки на уровне служб.

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

Например, уровень службы продукта в листинге 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.