2010-08-03 8 views
136

Capisco che IValidatableObject sia utilizzato per convalidare un oggetto in un modo che consente di confrontare le proprietà l'una con l'altra.Come utilizzare IValidatableObject?

Mi piacerebbe ancora avere attributi per convalidare le singole proprietà, ma in certi casi voglio ignorare i guasti su alcune proprietà.

Sto tentando di utilizzarlo in modo errato nel caso di seguito? Se no, come posso implementarlo?

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. 
     */ 
    } 
} 
} 

risposta

133

Prima di tutto, grazie a @ paper1337 per me indicando le giuste risorse ... non sono registrati in modo che non lo può votare fino, si prega di farlo se qualcun altro legge questo.

Ecco come realizzare ciò che stavo cercando di fare.

classe convalidabile:

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

Utilizzando Validator.TryValidateProperty() aggiungerà alla raccolta dei risultati se ci sono riuscito convalide. Se non c'è una validazione fallita, nulla sarà aggiunto alla raccolta dei risultati che è un'indicazione di successo.

Facendo la validazione:

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

E 'importante impostare validateAllProperties su false per questo metodo di lavorare. Quando validateAllProperties è FALSE Solo con un attributo [Required] sono controllati. Ciò consente al metodo IValidatableObject.Validate() di gestire le convalide condizionali.

+0

Non riesco a pensare a uno scenario in cui vorrei usarlo. Puoi darmi un esempio di dove lo useresti? –

+0

Se sono presenti colonne di tracciamento nella tabella (ad esempio l'utente che l'ha creata). È richiesto nel database ma si accede alle SaveChanges nel contesto per popolarlo (eliminando la necessità per gli sviluppatori di ricordarsi di impostarlo esplicitamente). Vorresti, naturalmente, confermare prima di salvare. Quindi non contrassegni la colonna "creatore" come richiesto ma convalida contro tutte le altre colonne/proprietà. – MetalPhoenix

+0

Il problema con questa soluzione è che ora si dipende dal chiamante affinché l'oggetto venga validato correttamente. – cocogza

67

preventivo da Jeff Handley's Blog Post on Validation Objects and Properties with Validator:

Nel convalidare un oggetto, il seguente processo viene applicato in Validator.ValidateObject:

  1. Convalida proprietà a livello di attributi
  2. Se eventuali validatori non sono validi, interrompe la convalida restituendo fallimento (s)
  3. Convalida attributi a livello di oggetto
  4. Se i validatori sono validi, interrompere la convalida restituendo il fallimento (s)
  5. Se sul quadro desktop e l'oggetto implementa IValidatableObject, quindi chiamare il suo metodo Convalida e restituire qualsiasi fallimento (s)

Questo indica che ciò che si stanno tentando di fare non funzionerà immediatamente perché la convalida annullerà al punto 2. Si potrebbe provare a creare attributi che ereditano da quelle built-in e in particolare verificare la presenza di una proprietà enabled (tramite un'interfaccia) prima di eseguire la loro convalida normale. In alternativa, è possibile inserire tutta la logica per la convalida dell'entità nel metodo Validate.

+2

+1 Buono a sapersi! – HOKBONG

26

solo per aggiungere un paio di punti:

Poiché la firma Validate() metodo restituisce IEnumerable<>, che yield return può essere utilizzato per generare pigramente i risultati - questo è utile se alcuni dei controlli di convalida sono 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"); 
     } 

Inoltre, se si utilizza MVC ModelState, è possibile convertire i fallimenti dei risultati di convalida per ModelState voci come segue (questo potrebbe essere utile se si sta facendo la convalida in 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

Bello! Vale la pena utilizzare attributi e riflessioni nel metodo Validate? – Schalk

3

ho implementato una classe astratta uso generale per la convalida

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

Mi piace lo stile di usare i parametri con nome, rende il codice molto più facile da leggere. – drizin

0

il problema con la risposta accettata è che ora dipende dalla chiamante per l'oggetto deve essere correttamente convalidato. Vorrei rimuovere il RangeAttribute e fare la convalida dell'intervallo all'interno del metodo Validate o vorrei creare una sottoclasse di attributi personalizzati RangeAttribute che prende il nome della proprietà richiesta come argomento sul costruttore.

Ad esempio:

[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

mi è piaciuto cocogza's answer tranne che chiamare base.IsValid ha provocato un'eccezione di overflow dello stack come sarebbe reinserire di nuovo e di nuovo il metodo IsValid. Quindi l'ho modificato per un tipo specifico di convalida, nel mio caso si trattava di un indirizzo e-mail.

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

Questo funziona molto meglio! Non si blocca e produce un bel messaggio di errore. Spero che questo aiuti qualcuno!

Problemi correlati