Traduction▲
Cet article est la traduction la plus fidèle possible de l'article original : Validating with a Service Layer
Introduction▲
L'objectif de ce tutoriel est de décrire une méthode de validation dans une application ASP.NET MVC. Vous apprendrez à déplacer votre logique de validation de vos contrôleurs vers une couche service distincte.
Séparation de code▲
Quand vous développer une application ASP.NET MVC, vous ne devez pas mettre vos accès logique à une base de données dans les actions de votre contrôleur. Mélanger le code du contrôleur à ceux des accès bases de données rendra votre code plus difficile à maintenir avec le temps. Il est donc recommandé de mettre les accès aux données dans une couche séparée.
Par exemple, le code ci-dessous contient un simple dépôt nommé ProductRepository. Ce dépôt contient tout le code d'accès aux données nécessaire à l'application. Cet exemple de code inclus aussi l'interface IProductRepository que le répertoire de dépot implémente.
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
(
);
}
}
Le contrôleur donné en exemple ci-dessous utilise dans ses action Index() et Create() la couche d'accès aux données créée. Notez le contrôleur ne contient aucune logique base de données. Créer une couche répertoire de dépot vous permet de conserver une séparation nette de votre code. Les contrôleurs sont responsables de la logique de flux de contrôle et le répertoire de dépot assure la logique accès aux données.
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▲
Donc, le flux de contrôle de l'application est géré par le contrôleur, et le répertoire de dépot s'occupe des accès aux données. Dans ce cas, où mettre la logique de validation? Une option consiste à placer la logique de validation dans une couche service.
Une couche service dans une application ASP.NET MVC est une couche supplémentaire qui gére les communications entre le contrôleur and et la couche d'accès aux données. La couche service contient la logique buisness. Et en particulier, elle s'occupe de la logique de validation.
Par example, le couche service ProductService dans le code ci-dessous a une méthode CreateProduct(). La méthode CreateProduct() appelle ValidateProduct() pour valider un nouveau produduit avant de le passer au ProductRepository.
using
System.
Collections.
Generic;
using
System.
Web.
Mvc;
namespace
MvcApplication1.
Models
{
public
class
ProductService :
MvcApplication1.
Models.
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
(
);
}
}
Note contrôleur ProductControlleur a été modifié dans le code ci-dessous exploiter la couche service plutôt que le répertoire de dépot. La couche contrôleur "parle" à la couche service. La couche service "parle" à la couche accès aux données. Chaque couche a donc ses propres responsabilités.
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"
);
}
}
}
Notez que le ProductService est instancié dans le constructeur du contrôleur. Lorsque le ProductService est instancié, le dictionnaire d'états du modéle est passé au service. Le ProductService exploite les états du modéle pour renvoyer au contrôleur les erreurs lors de la validation.
Dissociation de la couche service▲
Nous n'avons donc pas réussi dès le premier coup à séparer le contrôleur de la couche service. Le contrôleur et la couche service communiquent par l'intermédiaire du ModelState. En d'autres mots, la couche service à une dépendance avec une fonctionnalité particulière du Framework ASP.NET MVC.
On veut isoler le plus possible notre couche service de notre contrôleur. En théorie, nous devrions être capable d'utiliser la couche service avec tout type d'application et pas seulement avec les applications ASP.NET MVC. Par exemple, dans le futur, nous voudrons développer une application WPF pour notre application. Nous devons donc trouver un moyen de supprimer la dépendance entre notre couche service et le ModelState d'ASP.NET MVC.
Dans le code ci-dessous, la couche service a été modifiée afin de ne plus utiliser le ModelState. A la place, elle utilise une classe qui implémente l'interface IValidationDictionary.
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);
System.
Collections.
Generic.
IEnumerable<
Product>
ListProducts
(
);
}
}
L'interface IValidationDictionary est définie ci-dessous. Elle contient une seule méthode et une propriété.
namespace
MvcApplication1.
Models
{
public
interface
IValidationDictionary
{
void
AddError
(
string
key,
string
errorMessage);
bool
IsValid {
get
;
}
}
}
La classe suivante, nommée ModelStateWrapper, implémente notre interface IValidationDictionary. Il est possible d'instancier la classe ModelStateWrapper en passant un dictionnaire d'états du modéle au constructeur de la classe.
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
}
}
Enfin, nous devons modifier le contrôleur pour qu'il utilise la classe ModelStateWrapper lors de l'instanciation de la couche service.
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"
);
}
}
}
En exploitant notre interface IValidationDictionary et la classe ModelStateWrapper, nous avons plu complétement isoler notre couche service de notre controleur. Le couche service ne posséde plus de dépendances avec le ModelState. Vous pouvez passer n'importe quelle classe qui implémtente l'interface IValidationDictionary à votre couche service. Par exemple, une application WPF peut implémenter l'interface IValidationDictionary avec une simple classe des Collections.
Conclusion▲
Le but de ce tutoriel était de discuter d'une approche pour créer une logique validation dans une application ASP.NET MVC. Ainsi, vous avez appris comment déplacer toute la logique de validation en dehors de vos contrôleurs et dans une couche service séparée. Vous avez aussi appris comment isoler la couche service de votre contrôleur en développant une classe ModelStateWrapper.