Walidacja z użyciem warstwy usług (VB)
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.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
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.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
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.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
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.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
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.vb (oddzielone)
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
Interfejs IValidationDictionary jest zdefiniowany w liście 6. Ten prosty interfejs ma jedną metodę i jedną właściwość.
Lista 6 — Models\IValidationDictionary.cs
Public Interface IValidationDictionary
Sub AddError(ByVal key As String, ByVal errorMessage As String)
ReadOnly Property IsValid() As Boolean
End Interface
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.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
Na koniec zaktualizowany kontroler na liście 8 używa klasy ModelStateWrapper podczas tworzenia warstwy usługi w konstruktorze.
Lista 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
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.