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?