Condividi tramite


Convalida con un livello di servizio (C#)

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 è possibile mantenere una forte separazione delle preoccupazioni isolando il livello di servizio dal livello controller.

L'obiettivo di questa esercitazione è descrivere un metodo di esecuzione della 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 delle preoccupazioni

Quando si compila un'applicazione MVC ASP.NET, non è consigliabile inserire la logica di 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. La raccomandazione è che si inserisce 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 - Modelli\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();
    }

}

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 di repository consente di mantenere una separazione pulita dei problemi. I controller sono responsabili della logica di controllo del flusso dell'applicazione e il repository è responsabile della logica di accesso ai dati.

Elenco 2 - Controller\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");
        }

    }
}

Creazione di un livello di servizio

Quindi, la logica di controllo del flusso dell'applicazione appartiene a un controller e alla logica di accesso ai dati appartiene a un repository. In questo 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 di servizio contiene la logica di business. In particolare, contiene la logica di convalida.

Ad esempio, il livello del servizio prodotto in List 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 - Modelli\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();
    }
}

Il controller prodotto è stato aggiornato in Elenco 4 per usare il livello di servizio anziché il livello del repository. Il livello controller parla con il livello di servizio. Il livello di servizio parla con il livello del repository. Ogni livello ha una responsabilità separata.

Elenco 4 - Controller\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");
        }

    }
}

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

Disaccoppiamento del livello di servizio

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

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. Ad esempio, in futuro, potrebbe essere necessario creare un front-end WPF per l'applicazione. È necessario trovare un modo per rimuovere la dipendenza da ASP.NET stato del modello MVC dal livello di servizio.

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

Elenco 5 - Modelli\ProductService.cs (disaccoppiato)

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'interfaccia IValidationDictionary è definita nell'elenco 6. Questa interfaccia semplice include un singolo metodo e una singola proprietà.

Elenco 6 - Modelli\IValidationDictionary.cs

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

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

Elenco 7 - Modelli\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
    }
}

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

Elenco 8 - Controller\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'uso dell'interfaccia IValidationDictionary e della classe ModelStateWrapper consente di isolare completamente il livello di servizio dal livello controller. Il livello di servizio non dipende più dallo stato del modello. È possibile passare qualsiasi classe che implementa l'interfaccia IValidationDictionary al livello di servizio. Ad esempio, un'applicazione WPF potrebbe implementare l'interfaccia IValidationDictionary con una semplice classe di raccolta.

Riepilogo

L'obiettivo di questa esercitazione è quello di discutere un approccio per eseguire la 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.