A validação condicional pode ser muito útil, e fico feliz por ter encontrado uma forma elegante de fazê-la. Você pode usar o mesmo View-Model em duas actions diferentes do seu controller e ter validações distintas para cada uma delas.
Cenário de exemplo
Para ajudar a ilustrar o problema e a solução, vamos descrever um cenário simples de gerenciamento de usuários. Teremos um controller que adiciona e edita usuários. Vou abstrair a camada de negócio para que possamos focar apenas na validação de usuário.
namespace Groups.Web.Admin.Controllers
{
[RoutePrefix("users")]
public class UsersController : BaseController
{
[HttpGet, Route("add")]
public virtual ActionResult Add()
{
return View(new UserViewModel());
}
[HttpPost, Route("add")]
[ValidateAntiForgeryToken]
public virtual ActionResult Add(UserViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
// Business logic to add a new user
}
[HttpGet, Route("edit/{id}")]
public ActionResult Edit(Guid id)
{
// Find and return User for editing
}
[HttpPost, Route("edit/{id}")]
[ValidateAntiForgeryToken]
public ActionResult Edit(UserViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
// Business logic to edit a user
}
}
}
using System.ComponentModel.DataAnnotations;
using Groups.Domain.Entities;
namespace Groups.Web.Admin.ViewModels
{
public class UserViewModel : BaseViewModel
{
public UserViewModel() { }
public UserViewModel(User user)
{
Id = user.Id;
Name = user.Name;
Email = user.Email;
}
public string Name { get; set; }
public string Email { get; set; }
[DataType(DataType.Password)]
public string Password { get; set; }
public override string ToString() => Name;
}
}
Observe que não temos nenhuma validação em nosso View-Model; vamos adicioná-las em instantes.
Instalando o Fluent Validation
O que usaremos para validação é uma biblioteca chamada Fluent Validation. É uma solução atraente para validação no ambiente .NET. Para usá-la, a primeira coisa que precisamos fazer é instalá-la via NuGet.
Install-Package FluentValidation.MVC5
O próximo passo é inicializar a biblioteca Fluent Validation na inicialização da aplicação, usando o método Configure. Para isso, basta adicionar a seguinte linha ao seu método Application_Start:
FluentValidationModelValidatorProvider.Configure();
Se você estiver usando Unity para injeção de dependência, considere também adicionar a seguinte linha no seu método de registro.
container
.RegisterType<
RuleSetForClientSideMessagesAttribute,
RuleSetForClientSideMessagesAttribute>(
new InjectionConstructor(typeof(string)));
Adicionando validação
Agora que temos a biblioteca Fluent Validation funcionando em nosso projeto ASP.NET MVC 5, vamos adicionar validação às nossas classes View-Model. A forma como faremos isso é criando uma classe de validação que referenciaremos a partir do View-Model.
using System.Web;
using FluentValidation;
namespace Groups.Web.Admin.ViewModels
{
public class UserViewModelValidator
: AbstractValidator<UserViewModel>
{
public UserViewModelValidator()
{
RuleFor(x => x.Name).NotEmpty();
RuleFor(x => x.Email).NotEmpty().EmailAddress();
RuleSet("AddOnly", () =>
{
RuleFor(x => x.Password).NotEmpty();
});
RuleSet("EditOnly", () =>
{
RuleFor(x => x.Id).NotEmpty();
});
}
}
}
O RuleSet é uma das coisas mais legais que o Fluent Validation tem a oferecer, e é o que o torna tão flexível. Usaremos essas regras com o controller na próxima seção. Agora, podemos decorar nossa classe de modelo com o atributo Validator.
using System.ComponentModel.DataAnnotations;
using FluentValidation.Attributes;
using Groups.Domain.Entities;
namespace Groups.Web.Admin.ViewModels
{
[Validator(typeof(UserViewModelValidator))]
public class UserViewModel : BaseViewModel
{
public UserViewModel() { }
public UserViewModel(User user)
{
Id = user.Id;
Name = user.Name;
Email = user.Email;
}
public string Name { get; set; }
public string Email { get; set; }
[DataType(DataType.Password)]
public string Password { get; set; }
public override string ToString() => Name;
}
}
A única coisa nova aqui é a linha 7, onde definimos qual validador usaremos para esta classe View-Model.
Configurando a validação condicional
Agora é quando a mágica acontece. Podemos especificar em nossas actions quais RuleSets queremos usar para cada uma delas. Podemos usar o atributo RuleSetForClientSideMessages para os métodos HttpGet e CustomizeValidator para a vinculação do nosso modelo durante eventos Post. E é tudo o que precisamos fazer.
namespace Groups.Web.Admin.Controllers
{
[RoutePrefix("users")]
public class UsersController : BaseController
{
[HttpGet, Route("add")]
[RuleSetForClientSideMessages("default", "AddOnly")]
public virtual ActionResult Add()
{
(...)
}
[HttpPost, Route("add")]
[ValidateAntiForgeryToken]
public virtual ActionResult Add(
[CustomizeValidator(RuleSet = "default,AddOnly")]
UserViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
(...)
}
[HttpGet, Route("edit/{id}")]
[RuleSetForClientSideMessages("default", "EditOnly")]
public ActionResult Edit(Guid id)
{
(...)
}
[HttpPost, Route("edit/{id}")]
[ValidateAntiForgeryToken]
public ActionResult Edit(
[CustomizeValidator(RuleSet = "default,EditOnly")]
UserViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
(...)
}
}
}
Conclusão
E, de verdade, é tudo o que você precisa fazer. Não há dúvidas sobre por que essa biblioteca é tão popular. Essa abordagem também é boa se você quiser fazer validações que exigem lógica de negócio, por exemplo: verificar no banco de dados se o e-mail já existe:
using System.Web;
using FluentValidation;
using Groups.Business.Services;
using Groups.Web.Admin.Configuration;
using Groups.Web.Admin.Extensions;
using Groups.Web.Admin.Resources;
using Microsoft.Practices.Unity;
namespace Groups.Web.Admin.ViewModels
{
public class UserViewModelValidator : AbstractValidator<UserViewModel>
{
public UserViewModelValidator()
{
var userService = UnityConfig.GetConfiguredContainer().Resolve<IUserService>();
var loggedUser = HttpContext.Current.Session.GetLoggedUser();
RuleFor(x => x.Name).NotEmpty();
RuleFor(x => x.Email).NotEmpty().EmailAddress();
RuleFor(x => x.Email).Must((root, email) =>
!userService.AnyWithEmail(email, root.Id, loggedUser.Group.Id))
.WithMessage(MainResource.ErrorEmailAlreadyRegistered);
RuleSet("AddOnly", () =>
{
RuleFor(x => x.Password).NotEmpty();
});
RuleSet("EditOnly", () =>
{
RuleFor(x => x.Id).NotEmpty();
});
}
}
}