Corrigi hoje o problema que encontrei ontem quando fiz a validação com model binder (se você não leu, leia até o final para entender o problema). A correção levou menos de vinte minutos e estou bastante satisfeito. Aproveitei e fiz alguns testes unitários também.

O problema:

Toda minha lógica de validação estava no model binder, e por isso meu modelo de domínio (composto de uma única classe: e-mail) estava anêmico, ou seja, sem comportamento, era um mero agregador de dados.

A solução:

Movi toda a lógica para a entidade EmailMessage, e criei uma coleção de erros que o binder utiliza para setar seus erros.

A classe de mensagem ficou assim:

    [ModelBinder(typeof(EmailMessageBinder))]
    public class EmailMessage
    {                
        public string FromEmailAddress { get; set; }
        public string Message { get; set; }
        public string To { get; set; }
        public string FromName { get; set; }

        private string _subject;
        public string Subject
        { 
            get { return _subject; }
            set  
            {
                if (string.IsNullOrEmpty(value))
                    _subject = value;
                else
                    _subject = "[GIGGIO.NET] " + value; 
            }
        }

        public ICollection<KeyValuePair<string, string>> ValidationErrors()
        {
            var errors = new List<KeyValuePair<string, string>>();
            errors.AddValidation(() => this.FromEmailAddress, 
                (object p) => !string.IsNullOrEmpty((string)p), "Campo obrigatório não preenchido.");
            errors.AddRequiredStringValidation(() => this.Message, "Campo obrigatório não preenchido.");
            errors.AddRequiredStringValidation(() => this.FromName, "Campo obrigatório não preenchido.");
            errors.AddRequiredStringValidation(() => this.Subject, "Campo obrigatório não preenchido.");
            errors.AddRequiredStringValidation(() => this.To, "Campo obrigatório não preenchido.");
            errors.AddValidation(() => this.FromEmailAddress,
                delegate(object p)
                {
                    if (p == null) return false;
                    var regexEmail = @"umaregexgigante";
                    return Regex.Match((string)p, regexEmail).Success;
                }
                , "Endereço de origem inválido.");

            return errors;
        }
    }

Notem que a coleção retornada pelo método ValidationErrors não é um Dictionary<string, string>, porque dicionários não permitem chaves duplicadas, e eu precisava deste suporte. Utilizei então uma coleção de pares de chaves e valores. Funcionou bem.

Notem ainda que toda a validação é feita a partir de lambdas e métodos anônimos. Elas utilizam um método de extensão. Os métodos trabalham sobre minha coleção de chaves e valores, e são estes:

    internal static class ValidationExtensions
    {
        public static void AddValidation(this ICollection<KeyValuePair<string, string>> errors,
                                         Expression<Func<object>> getValue,
                                         Func<object, bool> validate,
                                         string errorMessage)
        {
            var value = getValue.Compile()();
            if (!validate(value))
            {
                var memberName = ((System.Linq.Expressions.MemberExpression)getValue.Body).Member.Name;
                errors.Add(new KeyValuePair<string, string>(memberName, errorMessage));
            }
        }

        public static void AddRequiredStringValidation(this ICollection<KeyValuePair<string, string>> errors,
                                         Expression<Func<object>> getValue,
                                         string errorMessage)
        {
            AddValidation(errors,
                getValue,
                (object p) => !string.IsNullOrEmpty((string)p),
                errorMessage);            
        }
        
    }

No primeiro há uma validação genérica. Eu usei ela na primeira validação (linha 26 da EmailMessage), só para exemplificar, e na última (linha 32 da EmailMessage). Eu passo aos métodos a propriedade a validar (como uma expression, passada sempre por uma lambda), a função de validação (um delegate criado a partir de uma lambda ou método anônimo), e a mensagem de erro. Aliás, eu adoro Lambdas, são muito úteis. Esta, no entanto, foi a primeira vez que usei Expressions em um caso real, onde usei para extrair o nome da propriedade (linha 11 das extensões) e para obter o valor da propriedade através da compilação da Expression em um delegate (linha 10). Prático, não?

Na segunda é uma validação de strings obrigatórias, ou seja, vou sempre verificar se a string é inexistente ou vazia. Ela utiliza a função genérica para fazer seu trabalho, passando para ela a lambda de verificação da string. Por causa disso ela não precisa receber a função de validação, já que sempre faço verificação via IsNullOrEmpty.

Validação simples e reutilizável. Excelente.

Observação que nada tem a ver com o assunto: Infelizmente não há ainda contravariância, então fui obrigado a usar uma Expression<Func<object>>, em vez de usar uma Expression<Func<string>>. Se recebesse uma expressão de string não conseguiria repassá-la ao método AddValidation, porque ele entenderia que são tipos diferentes, e não compilaria (venha logo C# 4…).

No Binder mudou tudo. Toda a responsabilidade de validação foi removida. Ele simplesmente olha para a coleção de erros e acrescenta os erros à sua coleção de model errors (veja linha 17):

    public class EmailMessageBinder : DefaultModelBinder
    {
        protected override bool OnModelUpdating(ControllerContext controllerContext, 
                                                ModelBindingContext bindingContext)
        {
            var message = (EmailMessage)bindingContext.Model;
            message.To = "seuemail@seudominio.net";
            return base.OnModelUpdating(controllerContext, bindingContext);
        }
               
        protected override void OnModelUpdated(ControllerContext controllerContext, 
                                               ModelBindingContext bindingContext)
        {
            var message = (EmailMessage)bindingContext.Model;
            var validationErrors = message.ValidationErrors();            
            foreach (var validationError in validationErrors)
                bindingContext.ModelState.AddModelError(validationError.Key, validationError.Value);
            base.OnModelUpdated(controllerContext, bindingContext);
        }

Bem mais limpo e claro, não é? Estou usando o método OnModelUpdated, que é onde você realiza as últimas ações sobre o objeto de modelo.

No controlador nada mudou.

Vejam o teste unitário, testando apenas as regras de negócio de validação:

        [TestMethod()]
        public void ValidationErrorsTest()
        {
            EmailMessage target1 = new EmailMessage();
            var errors1 = target1.ValidationErrors();

            EmailMessage target2 = new EmailMessage()
            {
                To = string.Empty,
                FromEmailAddress = string.Empty,
                Message = string.Empty,
                FromName = string.Empty,
                Subject = string.Empty
            };
            var errors2 = target2.ValidationErrors();

            Action<ICollection<KeyValuePair<string, string>>> Verify = 
                    delegate(ICollection<KeyValuePair<string, string>> errors)
            {
                Assert.AreEqual(2, (from pairs in errors where pairs.Key == "FromEmailAddress" select pairs).Count());
                Assert.AreEqual(1, (from pairs in errors where pairs.Key == "Message" select pairs).Count());
                Assert.AreEqual(1, (from pairs in errors where pairs.Key == "FromName" select pairs).Count());
                Assert.AreEqual(1, (from pairs in errors where pairs.Key == "Subject" select pairs).Count());
                Assert.AreEqual(1, (from pairs in errors where pairs.Key == "To" select pairs).Count());
                Assert.AreEqual(6, errors.Count);
            };

            Verify(errors1);
            Verify(errors2);            
        }

Notem que faço dois testes, um com as propriedades nulas, e outro com elas vazias. Os resultados tem que ser os mesmos, então fiz a validação através de uma lambda para não ficar me repetindo. Simples e claro, certo? Já imaginaram o trabalho que ia dar testar essa regra de validação da maneira anterior, onde o binder era o responsável pela validação? Bem mais complicado.

Esse foi um caso real, espero que tenha sido um bom exemplo não só de ASP.Net MVC, mas também de separação de resposabilidades e testes. O que acharam?


Postado na(s) categoria(s) ASP.Net MVC pelo giovanni bassi em 5 de fevereiro de 2009 às 13:32 | Tags: ,

Comentários


Brazil Gustavo Ayres
abril 21. 2009 11:25
Gustavo Ayres
Giovani,

Eu achei essa implementação bem legal, mas surgiram várias dúvidas:
- se eu estiver usando um Service Layer o ideal seria que todas essas validações estivessem nele e não direto no model, certo?
- Nesse caso o meu Binder deveria conhecer o meu SL e eu passaria o ModelState para ele para que o SL não dependesse do MVC?
- O meu SL deveria ter um método publico que nada mais faz do que validar o usuário?

Não sei se o melhor é perguntar aqui ou no seu fórum. Qualquer coisa me avisa q eu pergunto lá.

Valeu

no site


abril 21. 2009 14:58
Giovanni Bassi
Gustavo,
Entendo que quem deve validar a entidade é ela mesma, e não um objeto separado, da forma que mostrei aqui.
Você ainda pode usar IDataErrorInfo, que facilita bastante.

http://unplugged.giggio.net/http://unplugged.giggio.net/


Brazil Jéferson Spencer
maio 1. 2009 15:23
Jéferson Spencer
Bah Giovanni gostei muito da sua solução e me fez pensar muito em como resolver o problema de modelos anêmicos.
Contudo o que você fez não fica preso ao LINQ? Será que não seria melhor substituir a chamada ao LINQ por um reflection na classe ValidationExtension no método AddValidation? Isso é possível?

no site


maio 4. 2009 16:17
Giovanni Bassi
Jéferson, neste caso não tem LINQ. É tudo lambdas...

http://unplugged.giggio.net/http://unplugged.giggio.net/

Comentar


(Vai mostrar seu Gravatar)

  Country flag

biuquote
  • Comentário
  • Pré-visualização
Loading



Quem é Giovanni Bassi

Giovanni Bassi Sou uma pessoa apaixonada por tecnologia e especificamente por .Net. Sou consultor independente especialista em .Net, focado em arquitetura e melhores práticas. Tenho dezenas de artigos publicados na .Net Magazine, revista da qual sou editor técnico. Ministro palestras e cursos de vez em quando, e quando dá tempo eu respiro um pouco. Mais detalhes nesta página.

Busca

Selos

MVP

MCPD

MCSD

.Net Magazine

Abaixo ao if!

Calendário

«  março 2010  »
seteququsedo
22232425262728
1234567
891011121314
15161718192021
22232425262728
2930311234
Ver detalhamento de posts no calendário

Blogs interessantes

    OPMLDownload OPML file

    Postagens recentes

    Comentários recentes

    Disclaimer / Aviso
    As opiniões colocadas neste blog são minhas e pessoais e não expressam necessariamente as opiniões de meus empregadores, pareceiros e amigos. Da mesma forma, os comentários feitos por leitores do blog não expressam a minha opinião.

    © Copyright 2010 .Net Unplugged
    Log in