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.