Проверка с помощью уровня службы (VB)
Стивен Уолтер (Stephen Walther)
Узнайте, как переместить логику проверки из действий контроллера в отдельный уровень служб. В этом руководстве Стивен Уолтер объясняет, как обеспечить четкое разделение проблем, изолируя уровень службы от уровня контроллера.
Цель этого руководства — описать один из методов проверки в ASP.NET приложении MVC. В этом руководстве описано, как переместить логику проверки из контроллеров в отдельный уровень служб.
Разделение проблем
При создании приложения ASP.NET MVC не следует помещать логику базы данных в действия контроллера. Сочетание логики базы данных и контроллера усложняет обслуживание приложения с течением времени. Рекомендуется поместить всю логику базы данных в отдельный уровень репозитория.
Например, в листинге 1 содержится простой репозиторий с именем ProductRepository. Репозиторий продукта содержит весь код доступа к данным для приложения. В списке также содержится интерфейс IProductRepository, который реализует репозиторий продуктов.
Листинг 1. Models\ProductRepository.vb
Public Class ProductRepository
Implements IProductRepository
Private _entities As New ProductDBEntities()
Public Function ListProducts() As IEnumerable(Of Product) Implements IProductRepository.ListProducts
Return _entities.ProductSet.ToList()
End Function
Public Function CreateProduct(ByVal productToCreate As Product) As Boolean Implements IProductRepository.CreateProduct
Try
_entities.AddToProductSet(productToCreate)
_entities.SaveChanges()
Return True
Catch
Return False
End Try
End Function
End Class
Public Interface IProductRepository
Function CreateProduct(ByVal productToCreate As Product) As Boolean
Function ListProducts() As IEnumerable(Of Product)
End Interface
Контроллер, приведенный в листинге 2, использует уровень репозитория в действиях Index() и Create(). Обратите внимание, что этот контроллер не содержит логику базы данных. Создание уровня репозитория позволяет поддерживать четкое разделение задач. Контроллеры отвечают за логику управления потоком приложений, а репозиторий отвечает за логику доступа к данным.
Листинг 2. Controllers\ProductController.vb
Public Class ProductController
Inherits Controller
Private _repository As IProductRepository
Public Sub New()
Me.New(New ProductRepository())
End Sub
Public Sub New(ByVal repository As IProductRepository)
_repository = repository
End Sub
Public Function Index() As ActionResult
Return View(_repository.ListProducts())
End Function
'
' GET: /Product/Create
Public Function Create() As ActionResult
Return View()
End Function
'
' POST: /Product/Create
<AcceptVerbs(HttpVerbs.Post)> _
Public Function Create(<Bind(Exclude:="Id")> ByVal productToCreate As Product) As ActionResult
_repository.CreateProduct(productToCreate)
Return RedirectToAction("Index")
End Function
End Class
Создание уровня служб
Таким образом, логика управления потоком приложения принадлежит контроллеру, а логика доступа к данным — в репозитории. В этом случае, куда поместить логику проверки? Одним из вариантов является размещение логики проверки на уровне служб.
Уровень служб — это дополнительный уровень в приложении MVC ASP.NET, который является посредником в обмене данными между контроллером и уровнем репозитория. Уровень служб содержит бизнес-логику. В частности, он содержит логику проверки.
Например, уровень службы продукта в листинге 3 имеет метод CreateProduct(). Метод CreateProduct() вызывает метод ValidateProduct() для проверки нового продукта перед передачей продукта в репозиторий продуктов.
Листинг 3. Models\ProductService.vb
Public Class ProductService
Implements IProductService
Private _modelState As ModelStateDictionary
Private _repository As IProductRepository
Public Sub New(ByVal modelState As ModelStateDictionary, ByVal repository As IProductRepository)
_modelState = modelState
_repository = repository
End Sub
Protected Function ValidateProduct(ByVal productToValidate As Product) As Boolean
If productToValidate.Name.Trim().Length = 0 Then
_modelState.AddModelError("Name", "Name is required.")
End If
If productToValidate.Description.Trim().Length = 0 Then
_modelState.AddModelError("Description", "Description is required.")
End If
If productToValidate.UnitsInStock
Контроллер продукта был обновлен в листинге 4, чтобы использовать уровень служб вместо уровня репозитория. Уровень контроллера взаимодействует со службой. Уровень службы взаимодействует со слоем репозитория. Каждый слой несет отдельную ответственность.
Листинг 4. Controllers\ProductController.vb
Public Class ProductController
Inherits Controller
Private _service As IProductService
Public Sub New()
_service = New ProductService(Me.ModelState, New ProductRepository())
End Sub
Public Sub New(ByVal service As IProductService)
_service = service
End Sub
Public Function Index() As ActionResult
Return View(_service.ListProducts())
End Function
'
' GET: /Product/Create
Public Function Create() As ActionResult
Return View()
End Function
'
' POST: /Product/Create
<AcceptVerbs(HttpVerbs.Post)> _
Public Function Create(<Bind(Exclude := "Id")> ByVal productToCreate As Product) As ActionResult
If Not _service.CreateProduct(productToCreate) Then
Return View()
End If
Return RedirectToAction("Index")
End Function
End Class
Обратите внимание, что служба продукта создается в конструкторе контроллера продукта. При создании службы продукта в службу передается словарь состояния модели. Служба продукта использует состояние модели для передачи сообщений об ошибках проверки обратно в контроллер.
Разделение уровня служб
Мы не смогли изолировать уровни контроллера и службы в одном отношении. Уровни контроллера и службы взаимодействуют через состояние модели. Другими словами, уровень служб зависит от определенной функции платформы ASP.NET MVC.
Мы хотим максимально изолировать уровень служб от уровня контроллера. Теоретически мы должны иметь возможность использовать уровень служб с любым типом приложения, а не только с приложением ASP.NET MVC. Например, в будущем может потребоваться создать интерфейс WPF для нашего приложения. Мы должны найти способ удалить зависимость от ASP.NET состояния модели MVC с уровня служб.
В листинге 5 уровень служб обновлен, чтобы он больше не использовал состояние модели. Вместо этого используется любой класс, реализующий интерфейс IValidationDictionary.
Листинг 5. Models\ProductService.vb (разделенный)
Public Class ProductService
Implements IProductService
Private _validatonDictionary As IValidationDictionary
Private _repository As IProductRepository
Public Sub New(ByVal validationDictionary As IValidationDictionary, ByVal repository As IProductRepository)
_validatonDictionary = validationDictionary
_repository = repository
End Sub
Protected Function ValidateProduct(ByVal productToValidate As Product) As Boolean
If productToValidate.Name.Trim().Length = 0 Then
_validatonDictionary.AddError("Name", "Name is required.")
End If
If productToValidate.Description.Trim().Length = 0 Then
_validatonDictionary.AddError("Description", "Description is required.")
End If
If productToValidate.UnitsInStock
Интерфейс IValidationDictionary определен в листинге 6. Этот простой интерфейс имеет один метод и одно свойство.
Листинг 6. Models\IValidationDictionary.cs
Public Interface IValidationDictionary
Sub AddError(ByVal key As String, ByVal errorMessage As String)
ReadOnly Property IsValid() As Boolean
End Interface
Класс в листинге 7 с именем класса ModelStateWrapper реализует интерфейс IValidationDictionary. Вы можете создать экземпляр класса ModelStateWrapper, передав в конструктор словарь состояния модели.
Листинг 7. Models\ModelStateWrapper.vb
Public Class ModelStateWrapper
Implements IValidationDictionary
Private _modelState As ModelStateDictionary
Public Sub New(ByVal modelState As ModelStateDictionary)
_modelState = modelState
End Sub
#Region "IValidationDictionary Members"
Public Sub AddError(ByVal key As String, ByVal errorMessage As String) Implements IValidationDictionary.AddError
_modelState.AddModelError(key, errorMessage)
End Sub
Public ReadOnly Property IsValid() As Boolean Implements IValidationDictionary.IsValid
Get
Return _modelState.IsValid
End Get
End Property
#End Region
End Class
Наконец, обновленный контроллер в листинге 8 использует ModelStateWrapper при создании уровня службы в конструкторе.
Листинг 8. Controllers\ProductController.vb
Public Class ProductController
Inherits Controller
Private _service As IProductService
Public Sub New()
_service = New ProductService(New ModelStateWrapper(Me.ModelState), New ProductRepository())
End Sub
Public Sub New(ByVal service As IProductService)
_service = service
End Sub
Public Function Index() As ActionResult
Return View(_service.ListProducts())
End Function
'
' GET: /Product/Create
Public Function Create() As ActionResult
Return View()
End Function
'
' POST: /Product/Create
<AcceptVerbs(HttpVerbs.Post)> _
Public Function Create(<Bind(Exclude := "Id")> ByVal productToCreate As Product) As ActionResult
If Not _service.CreateProduct(productToCreate) Then
Return View()
End If
Return RedirectToAction("Index")
End Function
End Class
Использование интерфейса IValidationDictionary и класса ModelStateWrapper позволяет полностью изолировать уровень служб от уровня контроллера. Уровень служб больше не зависит от состояния модели. Вы можете передать любой класс, реализующий интерфейс IValidationDictionary, на уровень службы. Например, приложение WPF может реализовать интерфейс IValidationDictionary с простым классом коллекции.
Итоги
Цель этого руководства состояла в том, чтобы обсудить один из подходов к выполнению проверки в приложении ASP.NET MVC. В этом руководстве вы узнали, как переместить всю логику проверки из контроллеров в отдельный уровень служб. Вы также узнали, как изолировать уровень службы от уровня контроллера, создав класс ModelStateWrapper.