¿Cómo uso IValidatableObject?

Entiendo que IValidatableObject se usa para validar un objeto de una manera que permite comparar propiedades entre sí.

Todavía me gustaría tener atributos para validar propiedades individuales, pero quiero ignorar fallas en algunas propiedades en ciertos casos.

¿Estoy tratando de usarlo incorrectamente en el caso de abajo? Si no, ¿cómo implemento esto?

 public class ValidateMe : IValidatableObject { [Required] public bool Enable { get; set; } [Range(1, 5)] public int Prop1 { get; set; } [Range(1, 5)] public int Prop2 { get; set; } public IEnumerable Validate(ValidationContext validationContext) { if (!this.Enable) { /* Return valid result here. * I don't care if Prop1 and Prop2 are out of range * if the whole object is not "enabled" */ } else { /* Check if Prop1 and Prop2 meet their range requirements here * and return accordingly. */ } } } 

En primer lugar, gracias a @ paper1337 por señalarme los recursos correctos … No estoy registrado, así que no puedo votarlo, hágalo si alguien más lee esto.

He aquí cómo lograr lo que estaba tratando de hacer.

Clase validable:

 public class ValidateMe : IValidatableObject { [Required] public bool Enable { get; set; } [Range(1, 5)] public int Prop1 { get; set; } [Range(1, 5)] public int Prop2 { get; set; } public IEnumerable Validate(ValidationContext validationContext) { var results = new List(); if (this.Enable) { Validator.TryValidateProperty(this.Prop1, new ValidationContext(this, null, null) { MemberName = "Prop1" }, results); Validator.TryValidateProperty(this.Prop2, new ValidationContext(this, null, null) { MemberName = "Prop2" }, results); // some other random test if (this.Prop1 > this.Prop2) { results.Add(new ValidationResult("Prop1 must be larger than Prop2")); } } return results; } } 

El uso de Validator.TryValidateProperty() se agregará a la colección de resultados si hay validaciones fallidas. Si no hay una validación fallida, nada se agregará a la colección de resultados, lo que es una indicación de éxito.

Haciendo la validación:

  public void DoValidation() { var toValidate = new ValidateMe() { Enable = true, Prop1 = 1, Prop2 = 2 }; bool validateAllProperties = false; var results = new List(); bool isValid = Validator.TryValidateObject( toValidate, new ValidationContext(toValidate, null, null), results, validateAllProperties); } 

Es importante establecer validateAllProperties en falso para que este método funcione. Cuando validateAllProperties es falso, solo se validateAllProperties las propiedades con un atributo [Required] . Esto permite que el método IValidatableObject.Validate() maneje las validaciones condicionales.

Cita de la publicación del blog de Jeff Handley sobre objetos de validación y propiedades con validador :

Al validar un objeto, se aplica el siguiente proceso en Validator.ValidateObject:

  1. Validar atributos de nivel de propiedad
  2. Si alguno de los validadores no es válido, cancela la validación devolviendo la (s) falla (s)
  3. Validar los atributos de nivel de objeto
  4. Si alguno de los validadores no es válido, cancela la validación devolviendo la (s) falla (s)
  5. Si en el marco de escritorio y el objeto implementa IValidatableObject, llame a su método Validate y devuelva cualquier error (s)

Esto indica que lo que está intentando hacer no funcionará de inmediato porque la validación abortará en el paso # 2. Podría tratar de crear atributos que hereden de los integrados y comprobar específicamente la presencia de una propiedad habilitada (a través de una interfaz) antes de realizar su validación normal. Alternativamente, podría poner toda la lógica para validar la entidad en el método Validate .

Solo para agregar un par de puntos:

Debido a que la firma del método Validate() devuelve IEnumerable<> , ese yield return se puede utilizar para generar los resultados de forma perezosa; esto es beneficioso si algunas de las comprobaciones de validación son intensivas en IEnumerable<>

 public IEnumerable Validate(ValidationContext validationContext) { if (this.Enable) { // ... if (this.Prop1 > this.Prop2) { yield return new ValidationResult("Prop1 must be larger than Prop2"); } 

Además, si está utilizando MVC ModelState , puede convertir los fallos de los resultados de validación a ModelState entradas de ModelState siguiente manera (esto podría ser útil si está haciendo la validación en una carpeta de modelo personalizada ):

 var resultsGroupedByMembers = validationResults .SelectMany(vr => vr.MemberNames .Select(mn => new { MemberName = mn ?? "", Error = vr.ErrorMessage })) .GroupBy(x => x.MemberName); foreach (var member in resultsGroupedByMembers) { ModelState.AddModelError( member.Key, string.Join(". ", member.Select(m => m.Error))); } 

Implementé una clase abstracta de uso general para la validación

 using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace App.Abstractions { [Serializable] abstract public class AEntity { public int Id { get; set; } public IEnumerable Validate() { var vResults = new List(); var vc = new ValidationContext( instance: this, serviceProvider: null, items: null); var isValid = Validator.TryValidateObject( instance: vc.ObjectInstance, validationContext: vc, validationResults: vResults, validateAllProperties: true); /* if (true) { yield return new ValidationResult("Custom Validation","A Property Name string (optional)"); } */ if (!isValid) { foreach (var validationResult in vResults) { yield return validationResult; } } yield break; } } } 

El problema con la respuesta aceptada es que ahora depende de que la persona que llama para que el objeto sea debidamente validado. Quitaría RangeAttribute y haría la validación de rango dentro del método Validate o crearía un atributo personalizado subclases RangeAttribute que toma el nombre de la propiedad requerida como argumento en el constructor.

Por ejemplo:

 [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] class RangeIfTrueAttribute : RangeAttribute { private readonly string _NameOfBoolProp; public RangeIfTrueAttribute(string nameOfBoolProp, int min, int max) : base(min, max) { _NameOfBoolProp = nameOfBoolProp; } public RangeIfTrueAttribute(string nameOfBoolProp, double min, double max) : base(min, max) { _NameOfBoolProp = nameOfBoolProp; } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { var property = validationContext.ObjectType.GetProperty(_NameOfBoolProp); if (property == null) return new ValidationResult($"{_NameOfBoolProp} not found"); var boolVal = property.GetValue(validationContext.ObjectInstance, null); if (boolVal == null || boolVal.GetType() != typeof(bool)) return new ValidationResult($"{_NameOfBoolProp} not boolean"); if ((bool)boolVal) { return base.IsValid(value, validationContext); } return null; } } 

Me gustó la respuesta de cocogza, excepto esa base de llamadas. IsValid resultó en una excepción de desbordamiento de la stack, ya que volvería a ingresar el método IsValid una y otra vez. Así que lo modifiqué para ser un tipo específico de validación, en mi caso fue para una dirección de correo electrónico.

 [AttributeUsage(AttributeTargets.Property)] class ValidEmailAddressIfTrueAttribute : ValidationAttribute { private readonly string _nameOfBoolProp; public ValidEmailAddressIfTrueAttribute(string nameOfBoolProp) { _nameOfBoolProp = nameOfBoolProp; } protected override ValidationResult IsValid(object value, ValidationContext validationContext) { if (validationContext == null) { return null; } var property = validationContext.ObjectType.GetProperty(_nameOfBoolProp); if (property == null) { return new ValidationResult($"{_nameOfBoolProp} not found"); } var boolVal = property.GetValue(validationContext.ObjectInstance, null); if (boolVal == null || boolVal.GetType() != typeof(bool)) { return new ValidationResult($"{_nameOfBoolProp} not boolean"); } if ((bool)boolVal) { var attribute = new EmailAddressAttribute {ErrorMessage = $"{value} is not a valid e-mail address."}; return attribute.GetValidationResult(value, validationContext); } return null; } } 

Esto funciona mucho mejor! No se cuelga y produce un bonito mensaje de error. ¡Espero que esto ayude a alguien!