Validación con un nivel de servicio (C#)

por Stephen Walther

Aprenda a mover la lógica de validación fuera de las acciones del controlador y a introducirla en una capa de servicio distinta. En este tutorial, Stephen Walther explica cómo puede separar claramente sus problemas aislando la capa de servicio de la capa de controlador.

El objetivo de este tutorial es describir un método de realizar la validación en una aplicación ASP.NET MVC. En este tutorial, aprenderá a mover la lógica de validación fuera de los controladores y a introducirla en una capa de servicio distinta.

Separar los problemas

Al compilar una aplicación ASP.NET MVC, no debe colocar la lógica de la base de datos dentro de las acciones del controlador. La combinación de lógica de la base de datos y del controlador hace que la aplicación sea más difícil de mantener con el tiempo. La recomendación es colocar toda la lógica de base de datos en una capa de repositorio distinta.

Por ejemplo, la lista 1 contiene un repositorio simple denominado ProductRepository. El repositorio de productos contiene todo el código de acceso a los datos de la aplicación. La lista también incluye la interfaz IProductRepository que implementa el repositorio de productos.

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

}

El controlador de la lista 2 usa la capa de repositorio en sus acciones Index() y Create(). Observe que este controlador no contiene ninguna lógica de base de datos. La creación de una capa de repositorio le permite mantener una separación limpia de los problemas. Los controladores son responsables de la lógica de control del flujo de la aplicación, mientras que el repositorio es responsable de la lógica de acceso a los datos.

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

    }
}

Creación de una capa de servicio

Por lo tanto, la lógica de control del flujo de aplicación pertenece a un controlador y la lógica de acceso a los datos pertenece a un repositorio. En ese caso, ¿dónde se coloca la lógica de validación? Una opción es colocarla en una capa de servicio.

Una capa de servicio es una capa adicional en una aplicación ASP.NET MVC que media la comunicación entre un controlador y una capa de repositorio. La capa de servicio contiene lógica de negocios. En concreto, contiene lógica de validación.

Por ejemplo, la capa de servicio de producto de la lista 3 tiene un método CreateProduct(). El método CreateProduct() llama al método ValidateProduct() para validar un nuevo producto antes de pasarlo al repositorio de productos.

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

El controlador Product se ha actualizado en la lista 4 para usar la capa de servicio en lugar de la capa de repositorio. La capa de controlador se comunica con la capa de servicio. La capa de servicio se comunica con la capa de repositorio. Cada capa tiene una responsabilidad distinta.

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

    }
}

Observe que el servicio de producto se crea en el constructor del controlador de producto. Cuando se crea el servicio de producto, el diccionario de estado del modelo se pasa al servicio. El servicio de producto usa el estado del modelo para volver a pasar los mensajes de error de validación al controlador.

Desacoplamiento de la capa de servicio

No hemos podido aislar las capas de controlador y servicio en un solo aspecto. Las capas de controlador y servicio se comunican mediante el estado del modelo. En otras palabras, la capa de servicio tiene una dependencia de una característica determinada del marco ASP.NET MVC.

Queremos aislar lo más posible la capa de servicio de la capa de controlador. En teoría, deberíamos poder usar la capa de servicio con cualquier tipo de aplicación, no solo con aplicaciones ASP.NET MVC. Por ejemplo, en el futuro, quizás queramos compilar un front-end de WPF para nuestra aplicación. Deberíamos encontrar una manera de quitar la dependencia del estado del modelo de ASP.NET MVC de nuestra capa de servicio.

En la lista 5, la capa de servicio se ha actualizado y ya no usa el estado del modelo. En su lugar, usa cualquier clase que implemente la interfaz IValidationDictionary.

Lista 5: Models\ProductService.cs (desacoplado)

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

La interfaz IValidationDictionary se define en la lista 6. Esta interfaz simple tiene un único método y una sola propiedad.

Lista 6: Models\IValidationDictionary.cs

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

La clase de la lista 7, llamada clase ModelStateWrapper, implementa la interfaz IValidationDictionary. Puede crear una instancia de la clase ModelStateWrapper pasando un diccionario de estados de modelo al constructor.

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

Por último, el controlador actualizado de la lista 8 usa ModelStateWrapper al crear la capa de servicio en su constructor.

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

    }
}

El uso de la interfaz IValidationDictionary y la clase ModelStateWrapper nos permite aislar completamente la capa de servicio de la capa de controlador. La capa de servicio ya no depende del estado del modelo. Puede pasar cualquier clase que implemente la interfaz IValidationDictionary a la capa de servicio. Por ejemplo, una aplicación WPF podría implementar la interfaz IValidationDictionary con una clase de colección simple.

Resumen

El objetivo de este tutorial era analizar un enfoque para realizar la validación en una aplicación ASP.NET MVC. En este tutorial, ha aprendido a mover toda la lógica de validación fuera de los controladores e introducirla en una capa de servicio distinta. También ha aprendido a aislar la capa de servicio de la capa de controlador mediante la creación de una clase ModelStateWrapper.