2010-08-03 37 views
136

Entiendo que IValidatableObject se usa para validar un objeto de una manera que permite comparar propiedades entre sí.¿Cómo uso IValidatableObject?

Me gustaría tener atributos para validar propiedades individuales, pero quiero ignorar las 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<ValidationResult> 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. 
     */ 
    } 
} 
} 

Respuesta

133

En primer lugar, gracias a @ paper1337 por indicarme los recursos correctos ... No estoy registrado, así que no puedo votarle, por favor 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<ValidationResult> Validate(ValidationContext validationContext) 
    { 
     var results = new List<ValidationResult>(); 
     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; 
    } 
} 

Usando Validator.TryValidateProperty() se sumará a la colección resultados si hay validaciones fallidos. 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<ValidationResult>(); 

     bool isValid = Validator.TryValidateObject(
      toValidate, 
      new ValidationContext(toValidate, null, null), 
      results, 
      validateAllProperties); 
    } 

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

+0

No puedo pensar en un escenario en el que usaría esto. ¿Puedes darme un ejemplo de dónde usarías esto? –

+0

Si tiene columnas de seguimiento en su tabla (como el usuario que lo creó). Se requiere en la base de datos, pero usted entra en SaveChanges en el contexto para rellenarlo (eliminando la necesidad de que los desarrolladores recuerden establecerlo explícitamente). Por supuesto, usted debería validar antes de guardar. Por lo tanto, no marque la columna "creador" según sea necesario, sino que valide en contra de todas las demás columnas/propiedades. – MetalPhoenix

+0

El problema con esta solución es que ahora depende de que la persona que llama para que su objeto sea debidamente validado. – cocogza

67

Presupuesto de Jeff Handley's Blog Post on Validation Objects and Properties with Validator:

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

    atributos
  1. Validar propiedad de nivel
  2. Si cualquier validador no es válido, abortar la validación devolviendo el fallo (s)
  3. Validar los atributos a nivel de objeto
  4. Si validadores no son válidos, abortar la validación devolver el fallo (s)
  5. Si en el marco de escritorio y el objeto implementa IValidatableObject, a continuación, llamar a su método Validar y devolver cualquier fallo (s)

Esto indica que lo que se están intentando hacer no funcionarán 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, puede poner toda la lógica para validar la entidad en el método Validate.

+2

+1 ¡Es bueno saber la información! – HOKBONG

26

sólo para añadir un par de puntos:

Debido a que la firma Validate() método devuelve IEnumerable<>, que yield return se puede utilizar para generar perezosamente los resultados - esto es beneficioso si algunas de las comprobaciones de validación son IO o la CPU.

public IEnumerable<ValidationResult> 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 fracasos resultado de la validación a ModelState entradas de la siguiente manera (esto podría ser útil si usted está haciendo la validación en un custom model binder):

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))); 
} 
+0

¡Bonito! ¿Vale la pena usar los atributos y la reflexión en el método Validate? – Schalk

3

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<ValidationResult> Validate() 
     { 
      var vResults = new List<ValidationResult>(); 

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


    } 
} 
+0

Me encanta ese estilo de usar parámetros con nombre, hace que el código sea mucho más fácil de leer. – drizin

0

El problema con la respuesta aceptada es que ahora depende de la llamante 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; 
    } 
} 
0

me gusta cocogza's answer excepto que llamar base.IsValid dio lugar a una excepción de desbordamiento de pila, ya que sería volver a introducir 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!