ENPT

Validação condicional no ASP.NET

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