Partager via


Validation avec une couche de service (C#)

par Stephen Walther

Découvrez comment déplacer votre logique de validation en dehors des actions de votre contrôleur et dans une couche de service distincte. Dans ce tutoriel, Stephen Walther explique comment maintenir une séparation nette des problèmes en isolant votre couche de service de votre couche de contrôleur.

L’objectif de ce tutoriel est de décrire une méthode d’exécution de la validation dans une application MVC ASP.NET. Dans ce tutoriel, vous allez apprendre à déplacer votre logique de validation en dehors de vos contrôleurs et dans une couche de service distincte.

Séparation des préoccupations

Lorsque vous générez une application MVC ASP.NET, vous ne devez pas placer votre logique de base de données à l’intérieur de vos actions de contrôleur. La combinaison de votre base de données et de la logique du contrôleur rend votre application plus difficile à gérer au fil du temps. Il est recommandé de placer toute votre logique de base de données dans une couche de dépôt distincte.

Par exemple, listing 1 contient un dépôt simple nommé ProductRepository. Le référentiel de produits contient tout le code d’accès aux données de l’application. La liste inclut également l’interface IProductRepository que le référentiel de produits implémente.

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();
    }

}

Le contrôleur de la liste 2 utilise la couche de dépôt dans ses actions Index() et Create(). Notez que ce contrôleur ne contient aucune logique de base de données. La création d’une couche de dépôt vous permet de maintenir une propre séparation des préoccupations. Les contrôleurs sont responsables de la logique de contrôle de flux d’application et le référentiel est responsable de la logique d’accès aux données.

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");
        }

    }
}

Création d’une couche de service

Ainsi, la logique de contrôle de flux d’application appartient à un contrôleur et la logique d’accès aux données appartient à un dépôt. Dans ce cas, où placez-vous votre logique de validation ? Une option consiste à placer votre logique de validation dans une couche de service.

Une couche de service est une couche supplémentaire dans une application MVC ASP.NET qui sert de médiateur de communication entre un contrôleur et une couche de dépôt. La couche de service contient une logique métier. En particulier, il contient une logique de validation.

Par exemple, la couche de service produit dans listing 3 a une méthode CreateProduct(). La méthode CreateProduct() appelle la méthode ValidateProduct() pour valider un nouveau produit avant de le transmettre au dépôt de produit.

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();
    }
}

Le contrôleur de produit a été mis à jour dans la liste 4 pour utiliser la couche de service au lieu de la couche de dépôt. La couche contrôleur communique avec la couche de service. La couche de service communique avec la couche du référentiel. Chaque couche a une responsabilité distincte.

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");
        }

    }
}

Notez que le service produit est créé dans le constructeur du contrôleur de produit. Lorsque le service produit est créé, le dictionnaire d’état du modèle est passé au service. Le service produit utilise l’état du modèle pour transmettre les messages d’erreur de validation au contrôleur.

Découplage de la couche de service

Nous n’avons pas réussi à isoler les couches de contrôleur et de service à un seul égard. Les couches de contrôleur et de service communiquent via l’état du modèle. En d’autres termes, la couche de service dépend d’une fonctionnalité particulière de l’infrastructure MVC ASP.NET.

Nous voulons isoler autant que possible la couche de service de notre couche de contrôleur. En théorie, nous devrions être en mesure d’utiliser la couche de service avec n’importe quel type d’application et pas seulement une application MVC ASP.NET. Par exemple, à l’avenir, nous pourrions créer un serveur frontal WPF pour notre application. Nous devons trouver un moyen de supprimer la dépendance à ASP.NET’état du modèle MVC de notre couche de service.

Dans la liste 5, la couche de service a été mise à jour afin qu’elle n’utilise plus l’état du modèle. Au lieu de cela, il utilise n’importe quelle classe qui implémente l’interface IValidationDictionary.

Listing 5 - Models\ProductService.cs (découplé)

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();
    }
}

L’interface IValidationDictionary est définie dans listing 6. Cette interface simple a une seule méthode et une seule propriété.

Listing 6 - Models\IValidationDictionary.cs

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

La classe de listing 7, nommée classe ModelStateWrapper, implémente l’interface IValidationDictionary. Vous pouvez instancier la classe ModelStateWrapper en passant un dictionnaire d’état de modèle au constructeur.

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
    }
}

Enfin, le contrôleur mis à jour dans listing 8 utilise modelStateWrapper lors de la création de la couche de service dans son constructeur.

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");
        }

    }
}

L’utilisation de l’interface IValidationDictionary et de la classe ModelStateWrapper nous permet d’isoler complètement notre couche de service de notre couche de contrôleur. La couche de service ne dépend plus de l’état du modèle. Vous pouvez passer n’importe quelle classe qui implémente l’interface IValidationDictionary à la couche de service. Par exemple, une application WPF peut implémenter l’interface IValidationDictionary avec une classe de collection simple.

Résumé

L’objectif de ce tutoriel était de discuter d’une approche pour effectuer la validation dans une application MVC ASP.NET. Dans ce tutoriel, vous avez appris à déplacer toute votre logique de validation à partir de vos contrôleurs et dans une couche de service distincte. Vous avez également appris à isoler votre couche de service de votre couche de contrôleur en créant une classe ModelStateWrapper.