Condividi tramite


Convalida con un livello di servizio (VB)

di Stephen Walther

Informazioni su come spostare la logica di convalida dalle azioni del controller e in un livello di servizio separato. In questa esercitazione, Stephen Walther spiega come mantenere una netta separazione delle problematiche isolando il livello di servizio dal livello controller.

L'obiettivo di questa esercitazione è descrivere un metodo per eseguire la convalida in un'applicazione MVC ASP.NET. In questa esercitazione si apprenderà come spostare la logica di convalida dai controller e in un livello di servizio separato.

Separazione dei problemi

Quando si compila un'applicazione MVC ASP.NET, non è consigliabile inserire la logica del database all'interno delle azioni del controller. La combinazione della logica del database e del controller rende l'applicazione più difficile da gestire nel tempo. È consigliabile inserire tutta la logica del database in un livello di repository separato.

Ad esempio, l'elenco 1 contiene un repository semplice denominato ProductRepository. Il repository del prodotto contiene tutto il codice di accesso ai dati per l'applicazione. L'elenco include anche l'interfaccia IProductRepository implementata dal repository del prodotto.

Elenco 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

Il controller nell'elenco 2 usa il livello del repository nelle azioni Index() e Create(). Si noti che questo controller non contiene alcuna logica di database. La creazione di un livello del repository consente di mantenere una separazione pulita delle problematiche. I controller sono responsabili della logica di controllo del flusso dell'applicazione e il repository è responsabile della logica di accesso ai dati.

Elenco 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

Creazione di un livello di servizio

Pertanto, la logica di controllo del flusso dell'applicazione appartiene a un controller e la logica di accesso ai dati appartiene a un repository. In tal caso, dove si inserisce la logica di convalida? Un'opzione consiste nell'inserire la logica di convalida in un livello di servizio.

Un livello di servizio è un livello aggiuntivo in un'applicazione MVC ASP.NET che media la comunicazione tra un controller e un livello di repository. Il livello del servizio contiene la logica di business. In particolare, contiene la logica di convalida.

Ad esempio, il livello di servizio del prodotto in Listing 3 ha un metodo CreateProduct(). Il metodo CreateProduct() chiama il metodo ValidateProduct() per convalidare un nuovo prodotto prima di passare il prodotto al repository del prodotto.

Elenco 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

Il controller del prodotto è stato aggiornato nell'elenco 4 per usare il livello di servizio anziché il livello del repository. Il livello controller comunica con il livello del servizio. Il livello del servizio comunica con il livello del repository. Ogni livello ha una responsabilità separata.

Elenco 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

Si noti che il servizio prodotto viene creato nel costruttore del controller del prodotto. Quando viene creato il servizio di prodotto, il dizionario dello stato del modello viene passato al servizio. Il servizio del prodotto usa lo stato del modello per passare nuovamente i messaggi di errore di convalida al controller.

Disaccoppiamento del livello di servizio

Non è stato possibile isolare il controller e i livelli di servizio in un unico rispetto. I livelli controller e di servizio comunicano tramite lo stato del modello. In altre parole, il livello del servizio ha una dipendenza da una particolare funzionalità del framework MVC ASP.NET.

Si vuole isolare il livello di servizio dal livello controller il più possibile. In teoria, dovremmo essere in grado di usare il livello di servizio con qualsiasi tipo di applicazione e non solo un'applicazione ASP.NET MVC. In futuro, ad esempio, potrebbe essere necessario creare un front-end WPF per l'applicazione. È consigliabile trovare un modo per rimuovere la dipendenza dallo stato del modello MVC ASP.NET dal livello del servizio.

Nell'elenco 5 il livello del servizio è stato aggiornato in modo che non usi più lo stato del modello. Usa invece qualsiasi classe che implementa l'interfaccia IValidationDictionary.

Elenco 5 - Models\ProductService.vb (disaccoppiato)

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

L'interfaccia IValidationDictionary è definita nell'elenco 6. Questa interfaccia semplice ha un singolo metodo e una singola proprietà.

Elenco 6 - Models\IValidationDictionary.cs

Public Interface IValidationDictionary
Sub AddError(ByVal key As String, ByVal errorMessage As String)
ReadOnly Property IsValid() As Boolean
End Interface

La classe in Listato 7, denominata classe ModelStateWrapper, implementa l'interfaccia IValidationDictionary. È possibile creare un'istanza della classe ModelStateWrapper passando un dizionario di stato del modello al costruttore.

Elenco 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

Infine, il controller aggiornato nell'elenco 8 usa ModelStateWrapper durante la creazione del livello di servizio nel relativo costruttore.

Elenco 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

L'uso dell'interfaccia IValidationDictionary e della classe ModelStateWrapper consente di isolare completamente il livello di servizio dal livello controller. Il livello del servizio non dipende più dallo stato del modello. È possibile passare qualsiasi classe che implementa l'interfaccia IValidationDictionary al livello del servizio. Ad esempio, un'applicazione WPF potrebbe implementare l'interfaccia IValidationDictionary con una classe di raccolta semplice.

Riepilogo

L'obiettivo di questa esercitazione era illustrare un approccio all'esecuzione della convalida in un'applicazione MVC ASP.NET. In questa esercitazione si è appreso come spostare tutta la logica di convalida dai controller e in un livello di servizio separato. Si è anche appreso come isolare il livello di servizio dal livello controller creando una classe ModelStateWrapper.