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

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

Узнайте, как переместить логику проверки из действий контроллера в отдельный слой служб.Learn how to move your validation logic out of your controller actions and into a separate service layer. В этом руководстве Стивен Вальтер содержит сведения о том, как можно обеспечить четкое разделение проблем, изолируя уровень службы на уровне контроллера.In this tutorial, Stephen Walther explains how you can maintain a sharp separation of concerns by isolating your service layer from your controller layer.

Цель этого руководства — описать один метод выполнения проверки в приложении MVC ASP.NET.The goal of this tutorial is to describe one method of performing validation in an ASP.NET MVC application. В этом руководстве вы узнаете, как переместить логику проверки из контроллеров в отдельный слой служб.In this tutorial, you learn how to move your validation logic out of your controllers and into a separate service layer.

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

При создании приложения ASP.NET MVC не следует размещать логику базы данных в действиях контроллера.When you build an ASP.NET MVC application, you should not place your database logic inside your controller actions. Смешивание базы данных и логики контроллера делает приложение более сложным для обслуживания с течением времени.Mixing your database and controller logic makes your application more difficult to maintain over time. Рекомендуется размещать всю логику базы данных в отдельном слое репозитория.The recommendation is that you place all of your database logic in a separate repository layer.

Например, листинг 1 содержит простой репозиторий с именем Продуктрепоситори.For example, Listing 1 contains a simple repository named the ProductRepository. Репозиторий продукта содержит весь код доступа к данным для приложения.The product repository contains all of the data access code for the application. В список также входит интерфейс Ипродуктрепоситори, реализованный в репозитории продуктов.The listing also includes the IProductRepository interface that the product repository implements.

Листинг 1 — Моделс\продуктрепоситори.КСListing 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 ().The controller in Listing 2 uses the repository layer in both its Index() and Create() actions. Обратите внимание, что этот контроллер не содержит логику базы данных.Notice that this controller does not contain any database logic. Создание слоя репозитория позволяет поддерживать четкое разделение проблем.Creating a repository layer enables you to maintain a clean separation of concerns. Контроллеры отвечают за логику управления потоком приложений, и репозиторий отвечает за логику доступа к данным.Controllers are responsible for application flow control logic and the repository is responsible for data access logic.

Листинг 2. Контроллерс\продуктконтроллер.КСListing 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");
        }

    }
}

Создание слоя службCreating a Service Layer

Таким образом, логика управления потоком приложения принадлежит контроллеру, а логика доступа к данным — в репозитории.So, application flow control logic belongs in a controller and data access logic belongs in a repository. В этом случае, где вы помещаете логику проверки?In that case, where do you put your validation logic? Одним из вариантов является размещение логики проверки на уровне служб.One option is to place your validation logic in a service layer.

Слой служб — это дополнительный уровень в приложении ASP.NET MVC, исправляет связь между контроллером и слоем репозитория.A service layer is an additional layer in an ASP.NET MVC application that mediates communication between a controller and repository layer. Слой служб содержит бизнес-логику.The service layer contains business logic. В частности, он содержит логику проверки.In particular, it contains validation logic.

Например, уровень службы продукта в листинге 3 имеет метод Креатепродукт ().For example, the product service layer in Listing 3 has a CreateProduct() method. Метод Креатепродукт () вызывает метод Валидатепродукт () для проверки нового продукта перед передачей продукта в репозиторий продукта.The CreateProduct() method calls the ValidateProduct() method to validate a new product before passing the product to the product repository.

Листинг 3. Моделс\продуктсервице.КСListing 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 для использования слоя служб вместо слоя репозитория.The Product controller has been updated in Listing 4 to use the service layer instead of the repository layer. Уровень контроллера взаимодействует со слоем служб.The controller layer talks to the service layer. Слой служб обращается к слою репозитория.The service layer talks to the repository layer. Каждый уровень имеет отдельную ответственность.Each layer has a separate responsibility.

Листинг 4. Контроллерс\продуктконтроллер.КСListing 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");
        }

    }
}

Обратите внимание, что служба продукта создается в конструкторе контроллера продукта.Notice that the product service is created in the product controller constructor. При создании службы продукта словарь состояния модели передается в службу.When the product service is created, the model state dictionary is passed to the service. Служба продукта использует состояние модели для передачи сообщений об ошибках проверки обратно в контроллер.The product service uses model state to pass validation error messages back to the controller.

Отделение слоя службDecoupling the Service Layer

Не удалось изолировать уровни контроллера и службы в одном отношении.We have failed to isolate the controller and service layers in one respect. Уровни контроллера и службы взаимодействуют через состояние модели.The controller and service layers communicate through model state. Иными словами, уровень служб зависит от конкретной функции платформы MVC ASP.NET.In other words, the service layer has a dependency on a particular feature of the ASP.NET MVC framework.

Мы хотим, чтобы уровень служб был как можно более изолирован от уровня контроллера.We want to isolate the service layer from our controller layer as much as possible. Теоретически, мы должны иметь возможность использовать слой служб с приложением любого типа, а не только ASP.NET приложение MVC.In theory, we should be able to use the service layer with any type of application and not only an ASP.NET MVC application. Например, в будущем может потребоваться построить внешний интерфейс WPF для нашего приложения.For example, in the future, we might want to build a WPF front-end for our application. Мы должны найти способ удаления зависимости от состояния модели MVC ASP.NET из уровня служб.We should find a way to remove the dependency on ASP.NET MVC model state from our service layer.

В листинге 5 слой служб был обновлен так, чтобы он больше не использовал состояние модели.In Listing 5, the service layer has been updated so that it no longer uses model state. Вместо этого используется любой класс, реализующий интерфейс Ивалидатиондиктионари.Instead, it uses any class that implements the IValidationDictionary interface.

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

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.The IValidationDictionary interface is defined in Listing 6. Этот простой интерфейс имеет один метод и одно свойство.This simple interface has a single method and a single property.

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

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

Класс в листинге 7 с именем класса Моделстатевраппер реализует интерфейс Ивалидатиондиктионари.The class in Listing 7, named the ModelStateWrapper class, implements the IValidationDictionary interface. Можно создать экземпляр класса Моделстатевраппер, передав в конструктор словарь состояния модели.You can instantiate the ModelStateWrapper class by passing a model state dictionary to the constructor.

Листинг 7 — Моделс\моделстатевраппер.КСListing 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 использует Моделстатевраппер при создании слоя служб в конструкторе.Finally, the updated controller in Listing 8 uses the ModelStateWrapper when creating the service layer in its constructor.

Листинг 8. Контроллерс\продуктконтроллер.КСListing 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");
        }

    }
}

Использование интерфейса Ивалидатиондиктионари и класса Моделстатевраппер позволяет полностью изолировать наш уровень служб от нашего уровня контроллера.Using the IValidationDictionary interface and the ModelStateWrapper class enables us to completely isolate our service layer from our controller layer. Уровень службы больше не зависит от состояния модели.The service layer is no longer dependent on model state. Можно передать любой класс, реализующий интерфейс Ивалидатиондиктионари, в слой службы.You can pass any class that implements the IValidationDictionary interface to the service layer. Например, приложение WPF может реализовать интерфейс Ивалидатиондиктионари с помощью простого класса коллекции.For example, a WPF application might implement the IValidationDictionary interface with a simple collection class.

СводкаSummary

Цель этого учебника — обсудить один из подходов к выполнению проверки в приложении ASP.NET MVC.The goal of this tutorial was to discuss one approach to performing validation in an ASP.NET MVC application. В этом руководстве вы узнали, как переместить всю логику проверки из контроллеров в отдельный слой служб.In this tutorial, you learned how to move all of your validation logic out of your controllers and into a separate service layer. Вы также узнали, как изолировать слой служб от своего уровня контроллера, создав класс Моделстатевраппер.You also learned how to isolate your service layer from your controller layer by creating a ModelStateWrapper class.