ASP.NET MVC: Colocando validação no domínio

Blz pessoal

Neste artigo, mostrarei uma forma simples de validar erros na camada de domínio da aplicação (onde estão as regras de negócio) e tratá-los na camada de apresentação. Com isso, garantimos melhor separação de responsabilidades, com as regras de negócio validadas no domínio e as validações básicas de preenchimento de campos, na camada de apresentação.

Para chegarmos ao nosso objetivo, utilizaremos uma classe de exceções customizada e um método de extensão para popular facilmente o ModelState.

Vamos tomar por exemplo a entidade Cliente. Em nosso domínio, todo cliente deverá ter um nome e um CPF, obrigatoriamente. Sendo assim, poderíamos chamar a validação dessas regras no construtor da classe, como abaixo:

public class Cliente
{
    public string Nome { get; private set; }
    public string Cpf { get; private set; }

    public Cliente(string nome, string cpf)
    {
        Validar(nome, cpf);

        Nome = nome;
        Cpf = cpf;
    }

    private void Validar(string nome, string cpf)
    {
        var regrasException = new RegrasException<Cliente>();

        if (string.IsNullOrWhiteSpace(nome))
            regrasException.AdicionarErroPara(x => x.Nome, "Nome deve ser informado");

        if (string.IsNullOrWhiteSpace(cpf))
            regrasException.AdicionarErroPara(x => x.Cpf, "CPF deve ser informado");

        // poderia incluir outras validacoes, como o DV do CPF....

        // se algum erro foi adicionado à lista de erros, dispara exceção
        if (regrasException.Erros.Any())
            throw regrasException;
    }
}

No exemplo acima, utilizamos a classe RegrasException, que nada mais é do que uma Exception customizada que armazena todos os erros em uma lista e pode ser usada em todo o seu domínio.

Sua implementação é a seguinte:

public class RegrasException : Exception
{
    protected IList<ViolacaoDeRegra> _erros = new List<ViolacaoDeRegra>();
    private readonly Expression<Func<object, object>> _objeto = x => x;

    public IEnumerable<ViolacaoDeRegra> Erros { get { return _erros; } }

    internal void AdicionarErroAoModelo(string mensagem)
    {
        _erros.Add(new ViolacaoDeRegra { Propriedade = _objeto, Mensagem = mensagem });
    }
}

Mas não acabou por aí. Notem que, na entidade Cliente, estamos usando uma versão fortemente tipada de RegrasException, feita para permitir o uso de expressões lambdas para referenciar as propriedades da classe sendo validada. Sua implementação segue abaixo:

public class RegrasException<TModelo> : RegrasException
{
    internal void AdicionarErroPara<TPropriedade>(Expression<Func<TModelo, TPropriedade>> propriedade, string mensagem)
    {
        _erros.Add(new ViolacaoDeRegra { Propriedade = propriedade, Mensagem = mensagem });
    }
}

Percebam que, como RegrasException<TModelo> herda de RegrasException, poderíamos fazer regrasException.AdicionarErroAoModelo(“mensagem”) para adicionar um erro que não se aplica a uma propriedade em particular. (Veremos, mais adiante, como essa diferenciação (AdicionarErroPara e AdicionarErroAoModelo) será útil ao exibirmos os erros em nossas views.)

Vejam também que, nas classes acima, os erros são guardados em uma coleção de ViolacaoDeRegra, que é uma mera estrutura de dados:

public class ViolacaoDeRegra
{
    public LambdaExpression Propriedade { get; internal set; }
    public string Mensagem { get; internal set; }
}

Agora veremos como é simples tratarmos os erros em nossa aplicação ASP.NET MVC, utilizando o ModelState para salvarmos os erros encontrados. No entanto, para evitarmos duplicação de código em diversas actions, criaremos um método de extensão em nosso projeto web para adicionar os erros de RegrasException ao ModelState:

public static class ExtensoesDeRegrasException
{
    public static void CopiarErrosPara(this RegrasException ex, ModelStateDictionary modelState, string prefixo = null)
    {
        prefixo = string.IsNullOrWhiteSpace(prefixo) ? "" : prefixo + ".";

        foreach (var erro in ex.Erros)
        {
            var propriedade = ExpressionHelper.GetExpressionText(erro.Propriedade);
            var chave = string.IsNullOrWhiteSpace(propriedade) ? "" : prefixo + propriedade;
            modelState.AddModelError(chave, erro.Mensagem);
        }
    }
}

No método de extensão acima, informamos um parâmetro opcional (prefixo) que pode ser usado caso você esteja usando prefixos em sua view (ex.: Cliente.Nome).

————————————————————————-
Nota: Erros de Propriedade versus Erros de Modelo

O método de extensão checa o contéudo da propriedade vinda de “erro”, para determinar se é um erro “geral” da entidade do domínio (adicionado à coleção de erros usando AdicionarErroAoModelo) ou se é um erro específico de alguma propriedade (ou seja, adicionado à coleção de erros usando AdicionarErroPara).

O ModelState sabe a diferença entre esses 2 tipos de erros pelo parâmetro “chave” em AddModelError. Quando a chave está vazia, dizemos que é um erro de modelo (Model-Level Error), caso contrário, é um erro de propriedade (Property-Level Error), onde a chave é o nome da propriedade.

Sabendo essa diferença, podemos ter mais flexibidade para exibirmos os erros na view, usando o helper Html.ValidationSummary. Ao chamarmos Html.ValidationSummary() ou Html.ValidationSummary(false), exibimos somente os erros Model-Level, deixando os erros Property-Level para serem exibidos separadamente (por ex., cada um ao lado do campo correspondente).

Se quisermos exibir TODOS os erros em um único lugar, basta chamarmos Html.ValidationSummary(true).
————————————————————————-

Finalizando o exemplo, tratamos RegrasException em nossa action e chamamos o método de extensão para copiar todos os erros contidos na exceção vinda do domínio para o ModelState:

[HttpPost]
public ActionResult Salvar(ClienteViewModel clienteViewModel)
{
    Cliente cliente;
    try
    {
        cliente = new Cliente(clienteViewModel.Nome, clienteViewModel.Cpf);
    }
    catch (RegrasException ex)
    {
        ex.CopiarErrosPara(ModelState);
    }

    if (ModelState.IsValid)
    {
        // se tudo ok persiste cliente e redireciona
    }

    // se chegou até aqui, reexibe a mesma view para mostrar os erros do ModelState para o usuário
    return View(clienteViewModel);
}

Com isso, ao tentar criar um Cliente sem informar o nome ou o CPF, uma RegrasException será capturada pela action e seus erros copiados para o ModelState. Dessa forma, a propriedade IsValid do ModelState estará em “false” e os erros poderão ser exibidos na view utilizando-se o helper Html.ValidationSummary().

Conclusão

Vimos como é bem simples colocar a validação em nossa camada de domínio e tratá-las em nossa aplicação, garantindo assim melhor separação de responsabilidades e evitando regras do domínio “esparramadas” pela camada de apresentação.

Espero que tenham gostado.

[]s e até a próxima!

Anúncios

11 comentários em “ASP.NET MVC: Colocando validação no domínio

  1. Robson parabens pelo artigo, te juro que eu estava precisando justamente disso. Estou desenvolvendo um sistema na empresa onde trabalho que to utilizando dominio, inveis de fica enxendo o model de classes. Fora isso o sistema além de ser desenvolvimento para a internet ele também sera desenvolvido pra desktop.

  2. Boa noite robson, eu estou com uma duvida, queria sabe se você pode ma da uma luz. Eu tenho um dominio para Pessoa e um dominio pra endereco. No arquivo cshtml eu utilizo @model dominio,pessoa, pra pode usa as expressoes lambdas, mas quando eu tento coloca mais um @model pro dominio.endereco gera erro. E eu queria pra pode usa as expressoes lambdas para o endereco também. Se voce puder me da uma dica eu agradeco.

    1. Olá.
      Primeiro, ao dizer “dominio para Pessoa” provavelmente voce está querendo dizer que tem um entidade Pessoa que faz parte do dominio.
      So é possivel um “model” em sua view. Você pode encapsular todas as informações que voce precisa em um objeto especifico para a view, uma especie de DTO, que no ASP.NET MVC, é chamado de View Model. Não entendi muito bem a questão de voce querer usar lambdas no endereço. Teria que saber melhor como estão modelados Pessoa/Endereco e a finalidade da sua view.
      []s

  3. Sim quando eu me refiro ao Dominio pessoa é a entidade pessoa, e tambem tenho a entidade endereco.

    Quando eu me referi as expressoes lambdas, quiz dizer @Html.ValidationMessageFor(p => p.Nome) pra apresenta o erro para o usuario.

    Nesta view contem informações de cadastro. nome, cpf, endereco etc etc

  4. Robson em relação a pergunta te fiz por ultimo, já consegui resolve. Obrigado pela atenção, porem no seu exemplo, esta dando o erro na sequinte situacao. if (regrasException.Erros.Any()) o vs gera o erro no Any()

  5. Boa noite robson. Seguindo seu exemplo, eu consegui que ele me retorna-se os erros na view como vc demonstrou. Porem ele tnw ta segurando nos inputs as informações dos campos. Abaixo segue o pedaco de como eu to usando o controller e a view.

    1 – Controller
    [HttpPost]
    public ActionResult Index(FormCollection form)
    {
    if (ModelState.IsValid)
    {
    faco persistencia
    }
    return view(form)
    }

    2 – View
    @Html.ValidationSummary(true, “Preencha corretamente todos os campos obrigatórios marcados com ‘*’.”)
    @using (Html.BeginForm())
    {

    @Html.ValidationMessage(“Codigo”)
    }

    Obs.: não estou utilizando @model.
    Como disse la em cima, ele me retorna os erros na view, porem as informações sao apagas dos inputs. Se vc pode me da uma ajuda, fico muito agradecido.

    1. Olá, Renan
      Obrigado!

      Desculpe pela demora… na correria, acabei não vendo o comentário.

      Em construtores, é comum adicionar guard-clauses impedindo que um objeto seja criado de modo inconsistente. Em outras palavras, se você pediu os parâmetros no construtor, é porque o caller DEVE passá-los. Portanto, passar NULL é algo inesperado e você pode tratar e disparar uma exceção (customizada ou um ArgumentNullException).

      Para os outros casos, saber quando algo é excepcional é mais complicado porque depende de um contexto, como bem explicado neste famoso artigo do Martin Fowler: http://martinfowler.com/articles/replaceThrowWithNotification.html. Neste mesmo artigo, ele sugere o uso de Notifications ao invés de exceptions para regras de validação. É uma abordagem que testarei em projetos futuros. No momento, continuo usando exceções para essas validações (acumulando e disparando todos os problemas em uma única exception, como mostrado no meu post). Pode ser considerado “má prática” do ponto de vista de performance, mas ainda é a solução adotada (e inclusive promovida) por grande parte da comunidade.

      Cabe a você adotar uma abordagem – recomendaria avaliar as notifications – e seguir uma consistência ao longo do projeto.

      []s!

  6. Ótimo post! Estava procurando uma alternativa para o lançamento de exceptions e acabei implementando uma variação da sua solução.

    Usei sua ideia junto com a proposta do Fowler para notificação e cheguei num formato interessante em que exponho o método Validar que retorna uma coleção de validações (notificação).

    No construtor eu chamo o validar e retorno uma exception caso não passe na validação, assim apenas levanto uma exception caso aconteça alguma coisa inesperada (chamada do construtor sem validar a entidade ou objeto de valor).

    1. Oi, Douglas. Obrigado!

      Este artigo já é antigo. Como mencionei em outra resposta, consideraria primeiro o Notification Pattern proposto pelo Fowler sim. Só dispararia exceção no construtor.
      Mesmo assim, que bom que te ajudou!
      []s

Participe! Vamos trocar uma ideia sobre desenvolvimento de software!

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s