Validação de modelo customizado de propriedades dependentes usando Anotações de Dados

Desde então eu usei a excelente biblioteca FluentValidation para validar minhas classs de modelo. Em aplicativos da web, eu o uso em conjunto com o plug-in jquery.validate para executar a validação do lado do cliente também. Uma desvantagem é que grande parte da lógica de validação é repetida no lado do cliente e não está mais centralizada em um único local.

Por essa razão, estou procurando uma alternativa. Existem muitos exemplos que mostram o uso de annotations de dados para realizar a validação do modelo. Parece muito promissor. Uma coisa que não consegui descobrir é como validar uma propriedade que depende de outro valor de propriedade.

Vamos pegar, por exemplo, o seguinte modelo:

public class Event { [Required] public DateTime? StartDate { get; set; } [Required] public DateTime? EndDate { get; set; } } 

Gostaria de garantir que EndDate é maior que StartDate . Eu poderia escrever um atributo de validação personalizado estendendo o ValidationAttribute para executar uma lógica de validação personalizada. Infelizmente não consegui encontrar uma maneira de obter a instância do modelo:

 public class CustomValidationAttribute : ValidationAttribute { public override bool IsValid(object value) { // value represents the property value on which this attribute is applied // but how to obtain the object instance to which this property belongs? return true; } } 

Descobri que o CustomValidationAttribute parece fazer o trabalho porque tem essa propriedade ValidationContext que contém a instância do object que está sendo validada. Infelizmente este atributo foi adicionado apenas no .NET 4.0. Então, minha pergunta é: posso obter a mesma funcionalidade no .NET 3.5 SP1?


ATUALIZAR:

Parece que o FluentValidation já suporta validação e metadados do lado do cliente no ASP.NET MVC 2.

Ainda assim, seria bom saber se as annotations de dados poderiam ser usadas para validar propriedades dependentes.

O MVC2 vem com uma amostra “PropertiesMustMatchAttribute” que mostra como fazer com que o DataAnnotations funcione para você e deve funcionar tanto no .NET 3.5 quanto no .NET 4.0. Esse código de amostra é assim:

 [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] public sealed class PropertiesMustMatchAttribute : ValidationAttribute { private const string _defaultErrorMessage = "'{0}' and '{1}' do not match."; private readonly object _typeId = new object(); public PropertiesMustMatchAttribute(string originalProperty, string confirmProperty) : base(_defaultErrorMessage) { OriginalProperty = originalProperty; ConfirmProperty = confirmProperty; } public string ConfirmProperty { get; private set; } public string OriginalProperty { get; private set; } public override object TypeId { get { return _typeId; } } public override string FormatErrorMessage(string name) { return String.Format(CultureInfo.CurrentUICulture, ErrorMessageString, OriginalProperty, ConfirmProperty); } public override bool IsValid(object value) { PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(value); object originalValue = properties.Find(OriginalProperty, true /* ignoreCase */).GetValue(value); object confirmValue = properties.Find(ConfirmProperty, true /* ignoreCase */).GetValue(value); return Object.Equals(originalValue, confirmValue); } } 

Quando você usa esse atributo, em vez de colocá-lo em uma propriedade de sua class de modelo, você o coloca na própria class:

 [PropertiesMustMatch("NewPassword", "ConfirmPassword", ErrorMessage = "The new password and confirmation password do not match.")] public class ChangePasswordModel { public string NewPassword { get; set; } public string ConfirmPassword { get; set; } } 

Quando “IsValid” é chamado em seu atributo customizado, toda a instância do modelo é passada para ele para que você possa obter os valores da propriedade dependente dessa maneira. Você pode facilmente seguir esse padrão para criar um atributo de comparação de datas ou até mesmo um atributo de comparação mais geral.

Brad Wilson tem um bom exemplo em seu blog mostrando como adicionar a parte do lado do cliente da validação também, embora eu não tenha certeza se esse exemplo funcionará no .NET 3.5 e .NET 4.0.

Eu tive esse problema e recentemente abri minha solução: http://foolproof.codeplex.com/

A solução infalível para o exemplo acima seria:

 public class Event { [Required] public DateTime? StartDate { get; set; } [Required] [GreaterThan("StartDate")] public DateTime? EndDate { get; set; } } 

Em vez do PropertiesMustMatch, o CompareAttribute pode ser usado no MVC3. De acordo com este link http://devtrends.co.uk/blog/the-complete-guide-to-validation-in-asp.net-mvc-3-part-1 :

 public class RegisterModel { // skipped [Required] [ValidatePasswordLength] [DataType(DataType.Password)] [Display(Name = "Password")] public string Password { get; set; } [DataType(DataType.Password)] [Display(Name = "Confirm password")] [Compare("Password", ErrorMessage = "The password and confirmation do not match.")] public string ConfirmPassword { get; set; } } 

CompareAttribute é um validador novo, muito útil que não é realmente parte de System.ComponentModel.DataAnnotations, mas foi adicionado à DLL System.Web.Mvc pela equipe. Embora não seja particularmente bem nomeado (a única comparação que faz é verificar a igualdade, então talvez EqualTo seja mais óbvio), é fácil ver pelo uso que este validador verifica se o valor de uma propriedade é igual ao valor de outra propriedade. . Você pode ver no código que o atributo recebe uma propriedade de string que é o nome da outra propriedade que você está comparando. O uso clássico deste tipo de validador é o que estamos usando aqui: confirmação de senha.

Como os methods do DataAnnotations do .NET 3.5 não permitem que você forneça o object real validado ou um contexto de validação, você terá que fazer um pouco de truques para conseguir isso. Devo admitir que não estou familiarizado com a ASP.NET MVC, então não posso dizer como fazer isso exatamente em conjunto com o MCV, mas você pode tentar usar um valor thread-static para passar o próprio argumento. Aqui está um exemplo com algo que pode funcionar.

Primeiro, crie algum tipo de ‘escopo de objects’ que permita passar objects sem precisar passá-los pela pilha de chamadas:

 public sealed class ContextScope : IDisposable { [ThreadStatic] private static object currentContext; public ContextScope(object context) { currentContext = context; } public static object CurrentContext { get { return context; } } public void Dispose() { currentContext = null; } } 

Em seguida, crie seu validador para usar o ContextScope:

 public class CustomValidationAttribute : ValidationAttribute { public override bool IsValid(object value) { Event e = (Event)ObjectContext.CurrentContext; // validate event here. } } 

E por último, mas não menos importante, assegure-se de que o object passou pelo ContextScope:

 Event eventToValidate = [....]; using (var scope new ContextScope(eventToValidate)) { DataAnnotations.Validator.Validate(eventToValidate); } 

Isso é útil?

Demorou um pouco desde que sua pergunta foi feita, mas se você ainda gosta de metadados (pelo menos às vezes), abaixo há ainda outra solução alternativa, que permite fornecer várias expressões lógicas para os atributos:

 [Required] public DateTime? StartDate { get; set; } [Required] [AssertThat("StartDate != null && EndDate > StartDate")] public DateTime? EndDate { get; set; } 

Funciona tanto para o servidor quanto para o lado do cliente. Mais detalhes podem ser encontrados aqui .