Validação com uma camada de serviço (C#)
por Stephen Walther
Saiba como mover sua lógica de validação para fora das ações do controlador e para uma camada de serviço separada. Neste tutorial, Stephen Walther explica como você pode manter uma separação acentuada de preocupações isolando sua camada de serviço da camada do controlador.
O objetivo deste tutorial é descrever um método de execução de validação em um aplicativo MVC ASP.NET. Neste tutorial, você aprenderá a mover sua lógica de validação para fora dos controladores e para uma camada de serviço separada.
Separando preocupações
Ao criar um aplicativo MVC ASP.NET, você não deve colocar a lógica do banco de dados dentro das ações do controlador. Misturar o banco de dados e a lógica do controlador torna seu aplicativo mais difícil de manter ao longo do tempo. A recomendação é que você coloque toda a lógica do banco de dados em uma camada de repositório separada.
Por exemplo, a Listagem 1 contém um repositório simples chamado ProductRepository. O repositório de produtos contém todo o código de acesso a dados para o aplicativo. A listagem também inclui a interface IProductRepository que o repositório de produtos implementa.
Listagem 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();
}
}
O controlador na Listagem 2 usa a camada do repositório em suas ações Index() e Create(). Observe que esse controlador não contém nenhuma lógica de banco de dados. A criação de uma camada de repositório permite que você mantenha uma separação limpo de preocupações. Os controladores são responsáveis pela lógica de controle de fluxo do aplicativo e o repositório é responsável pela lógica de acesso a dados.
Listagem 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");
}
}
}
Criando uma camada de serviço
Portanto, a lógica de controle de fluxo do aplicativo pertence a um controlador e a lógica de acesso a dados pertence a um repositório. Nesse caso, onde você coloca sua lógica de validação? Uma opção é colocar a lógica de validação em uma camada de serviço.
Uma camada de serviço é uma camada adicional em um aplicativo MVC ASP.NET que media a comunicação entre um controlador e uma camada de repositório. A camada de serviço contém lógica de negócios. Em particular, ele contém a lógica de validação.
Por exemplo, a camada de serviço do produto na Listagem 3 tem um método CreateProduct(). O método CreateProduct() chama o método ValidateProduct() para validar um novo produto antes de passar o produto para o repositório do produto.
Listagem 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();
}
}
O Controlador de produto foi atualizado na Listagem 4 para usar a camada de serviço em vez da camada do repositório. A camada do controlador se comunica com a camada de serviço. A camada de serviço se comunica com a camada do repositório. Cada camada tem uma responsabilidade separada.
Listagem 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 o serviço de produto é criado no construtor do controlador de produto. Quando o serviço de produto é criado, o dicionário de estado do modelo é passado para o serviço. O serviço de produto usa o estado do modelo para passar mensagens de erro de validação de volta para o controlador.
Desacoplando a camada de serviço
Falha ao isolar o controlador e as camadas de serviço em um aspecto. O controlador e as camadas de serviço se comunicam por meio do estado do modelo. Em outras palavras, a camada de serviço tem uma dependência de um recurso específico do ASP.NET estrutura MVC.
Queremos isolar a camada de serviço da camada do controlador o máximo possível. Em teoria, devemos ser capazes de usar a camada de serviço com qualquer tipo de aplicativo e não apenas um aplicativo MVC ASP.NET. Por exemplo, no futuro, talvez queiramos criar um front-end do WPF para nosso aplicativo. Devemos encontrar uma maneira de remover a dependência de ASP.NET estado do modelo MVC de nossa camada de serviço.
Na Listagem 5, a camada de serviço foi atualizada para que ela não use mais o estado do modelo. Em vez disso, ele usa qualquer classe que implemente a interface IValidationDictionary.
Listagem 5 – Models\ProductService.cs (separado)
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();
}
}
A interface IValidationDictionary é definida na Listagem 6. Essa interface simples tem um único método e uma única propriedade.
Listagem 6 – Models\IValidationDictionary.cs
namespace MvcApplication1.Models
{
public interface IValidationDictionary
{
void AddError(string key, string errorMessage);
bool IsValid { get; }
}
}
A classe na Listagem 7, chamada classe ModelStateWrapper, implementa a interface IValidationDictionary. Você pode instanciar a classe ModelStateWrapper passando um dicionário de estado de modelo para o construtor.
Listagem 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 fim, o controlador atualizado na Listagem 8 usa o ModelStateWrapper ao criar a camada de serviço em seu construtor.
Listagem 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");
}
}
}
Usar a interface IValidationDictionary e a classe ModelStateWrapper nos permite isolar completamente nossa camada de serviço da camada do controlador. A camada de serviço não depende mais do estado do modelo. Você pode passar qualquer classe que implemente a interface IValidationDictionary para a camada de serviço. Por exemplo, um aplicativo WPF pode implementar a interface IValidationDictionary com uma classe de coleção simples.
Resumo
O objetivo deste tutorial era discutir uma abordagem para executar a validação em um aplicativo MVC ASP.NET. Neste tutorial, você aprendeu a mover toda a lógica de validação para fora dos controladores e para uma camada de serviço separada. Você também aprendeu a isolar a camada de serviço da camada do controlador criando uma classe ModelStateWrapper.