Validación de modelo personalizado de propiedades dependientes utilizando Anotaciones de datos

Desde ahora, he usado la excelente biblioteca FluentValidation para validar mis clases modelo. En las aplicaciones web, lo uso junto con el complemento jquery.validate para realizar la validación del lado del cliente también. Una desventaja es que gran parte de la lógica de validación se repite en el lado del cliente y ya no está centralizada en un solo lugar.

Por esta razón, estoy buscando una alternativa. Hay muchos ejemplos que muestran el uso de anotaciones de datos para realizar la validación del modelo. Se ve muy prometedor. Una cosa que no pude averiguar es cómo validar una propiedad que depende de otro valor de propiedad.

Tomemos por ejemplo el siguiente modelo:

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

Me gustaría asegurarme de que EndDate sea ​​mayor que StartDate . Podría escribir un atributo de validación personalizado que extienda ValidationAttribute para realizar una lógica de validación personalizada. Lamentablemente, no pude encontrar una forma de obtener la instancia del 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; } } 

Descubrí que CustomValidationAttribute parece hacer el trabajo porque tiene esta propiedad ValidationContext que contiene la instancia del objeto que se está validando. Lamentablemente, este atributo se ha agregado solo en .NET 4.0. Entonces mi pregunta es: ¿puedo lograr la misma funcionalidad en .NET 3.5 SP1?


ACTUALIZAR:

Parece que FluentValidation ya admite validación y metadatos del lado del cliente en ASP.NET MVC 2.

Sin embargo, sería bueno saber si las anotaciones de datos podrían usarse para validar propiedades dependientes.

MVC2 viene con un ejemplo de “PropertiesMustMatchAttribute” que muestra cómo funciona DataAnnotations para usted y debería funcionar en .NET 3.5 y .NET 4.0. Ese código de muestra se ve así:

 [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); } } 

Cuando usa ese atributo, en lugar de ponerlo en una propiedad de su clase de modelo, lo coloca en la clase misma:

 [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; } } 

Cuando se llama a “IsValid” en su atributo personalizado, se le pasa toda la instancia del modelo para que pueda obtener los valores de propiedad dependientes de esa manera. Podría seguir fácilmente este patrón para crear un atributo de comparación de fechas, o incluso un atributo de comparación más general.

Brad Wilson tiene un buen ejemplo en su blog que muestra cómo agregar la parte del lado del cliente de la validación, aunque no estoy seguro si ese ejemplo funcionará en .NET 3.5 y .NET 4.0.

Tuve este problema y recientemente abrí mi solución: http://foolproof.codeplex.com/

La solución a prueba de tontos al ejemplo anterior sería:

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

En lugar de PropertiesMustMatch, CompareAttribute que se puede usar en MVC3. De acuerdo con este enlace 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 es un validador nuevo y muy útil que, en realidad, no forma parte de System.ComponentModel.DataAnnotations, pero el equipo lo ha agregado a la DLL System.Web.Mvc. Aunque no tiene un nombre muy particular (la única comparación que hace es verificar la igualdad, entonces quizás EqualTo sería más obvio), es fácil ver por el uso que este validador verifica que el valor de una propiedad sea igual al valor de otra propiedad . Puede ver en el código que el atributo toma una propiedad de cadena que es el nombre de la otra propiedad que está comparando. El uso clásico de este tipo de validador es para lo que lo estamos usando aquí: confirmación de contraseña.

Debido a que los métodos de las Anotaciones de datos de .NET 3.5 no le permiten suministrar el objeto real validado o un contexto de validación, tendrá que hacer un poco de engaño para lograr esto. Debo admitir que no estoy familiarizado con ASP.NET MVC, por lo que no puedo decir cómo hacerlo exactamente en conjunción con MCV, pero puede intentar usar un valor estático de subprocesos para pasar el argumento en sí. Aquí hay un ejemplo con algo que podría funcionar.

Primero crea algún tipo de ‘scope del objeto’ que te permita pasar objetos sin tener que pasarlos a través de la stack de llamadas:

 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; } } 

Luego, crea tu validador para usar ContextScope:

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

Y por último, pero no por eso menos importante, asegúrese de que el objeto haya pasado por el ContextScope:

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

¿Esto es útil?

Tomó un poco de tiempo desde que se hizo su pregunta, pero si todavía le gustan los metadatos (al menos algunas veces), a continuación hay otra solución alternativa, que le permite proporcionar varias expresiones lógicas a los atributos:

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

Funciona tanto para el servidor como para el lado del cliente. Más detalles se pueden encontrar aquí .