2010-08-03 18 views
136

Ich verstehe, dass IValidatableObject verwendet wird, um ein Objekt auf eine Weise zu validieren, die man Eigenschaften miteinander vergleichen kann.Wie verwende ich IValidatableObject?

Ich hätte immer noch gerne Attribute, um einzelne Eigenschaften zu validieren, aber ich möchte Fehler in einigen Eigenschaften in bestimmten Fällen ignorieren.

Ich versuche, es in dem Fall unten falsch zu verwenden? Wenn nicht, wie implementiere ich das?

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

Antwort

133

Zunächst einmal, dank @ paper1337 für das Zeigen auf die richtigen Ressourcen ... Ich bin nicht registriert, also kann ich ihn nicht wählen, bitte tun Sie es, wenn jemand anderes dies liest.

Hier ist, wie Sie erreichen, was ich versuchte zu erreichen.

Validierbares Klasse:

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

Validator.TryValidateProperty() Verwendung zur Ergebnis Sammlung wird, wenn es Validierungen gescheitert sind. Wenn keine Validierung fehlgeschlagen ist, wird der Ergebnissammlung nichts hinzugefügt, was ein Anzeichen für den Erfolg ist.

Dadurch könnte die Validierung:

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 ist wichtig, validateAllProperties auf false zu setzen für diese Methode zu arbeiten. Wenn validateAllProperties falsch ist, werden nur Eigenschaften mit einem [Required] Attribut überprüft. Dies ermöglicht, dass die Methode IValidatableObject.Validate() die bedingten Prüfungen durchführt.

+0

Ich kann nicht an ein Szenario denken, wo ich das verwenden würde. Können Sie mir ein Beispiel geben, wo Sie das verwenden würden? –

+0

Wenn Sie Tracking-Spalten in Ihrer Tabelle haben (z. B. der Benutzer, der sie erstellt hat). Es ist in der Datenbank erforderlich, aber Sie geben die SaveChanges im Kontext ein, um sie zu füllen (ohne dass Entwickler daran denken müssen, es explizit festzulegen). Sie würden natürlich vor dem Speichern validieren. Daher markieren Sie die Spalte "Creator" nicht wie erforderlich, sondern validieren sie für alle anderen Spalten/Eigenschaften. – MetalPhoenix

+0

Das Problem mit dieser Lösung besteht darin, dass Sie jetzt vom Aufrufer abhängig sind, damit Ihr Objekt ordnungsgemäß validiert wird. – cocogza

67

Zitat aus Jeff Handley's Blog Post on Validation Objects and Properties with Validator:

Wenn ein Objekt Validieren wird der folgende Prozess in Validator.ValidateObject angewendet:

  1. Validate-Eigenschaft-Level-Attribute
  2. Wenn Alle Validatoren sind ungültig. Die Validierung wird abgebrochen, indem diezurückgegeben wirdVersagen (n)
  3. Validate der Objektebene Attribute
  4. Wenn irgendwelche Validatoren sind ungültig, abbrechen Validierung Rückgabe des Fehler (s)
  5. Wenn auf dem Desktop-Framework und das Objekt implementiert IValidatableObject, dann seine Methode Validate rufen und zurück jeden Fehler (s)

Dies zeigt, dass das, was Sie versuchen zu arbeiten, funktioniert nicht sofort, da die Validierung in Schritt 2 abgebrochen wird. Sie könnten versuchen, Attribute zu erstellen, die von den integrierten Einsen erben und speziell nach dem Vorhandensein einer aktivierten Eigenschaft suchen (über eine Schnittstelle), bevor Sie ihre normale Validierung durchführen. Alternativ können Sie die gesamte Logik für die Validierung der Entität in die Validate-Methode einfügen.

+2

+1 Gut zu wissen Informationen! – HOKBONG

26

einfach ein paar Punkte hinzuzufügen:

Da die Validate() Methodensignatur IEnumerable<> zurückgibt, dass yield return träge verwendet werden können, um die Ergebnisse zu erzeugen - das ist, wenn einige der Validierungsprüfungen von Vorteil ist, IO oder CPU-Kapazität sind.

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

Auch wenn Sie MVC ModelState verwenden, können Sie die Überprüfungsergebnis Ausfälle ModelState Einträge wie folgt konvertieren (dies nützlich sein könnte, wenn Sie die Validierung in einem custom model binder tun):

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

Nice one! Lohnt es sich, Attribute und Reflektionen in der Validate-Methode zu verwenden? – Schalk

3

Ich implementierte eine allgemeine Verwendung abstrakte Klasse für die Validierung

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

Ich liebe diesen Stil der Verwendung benannter Parameter, macht Code viel einfacher zu lesen. – drizin

0

Das Problem mit der angenommenen Antwort ist, dass es jetzt auf die Aufrufer für das Objekt ordnungsgemäß validiert werden. Ich würde entweder das RangeAttribute entfernen und die Bereichsüberprüfung innerhalb der Validate-Methode durchführen, oder ich würde ein benutzerdefiniertes Attribut-Unterklassen-RangeAttribute erstellen, das den Namen der erforderlichen Eigenschaft als Argument für den Konstruktor verwendet.

Zum Beispiel:

[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

Ich mochte cocogza's answer außer dass Aufruf base.IsValid in einem Stapelüberlauf Ausnahme geführt hat, wie es die IsValid Methode immer wieder neu eingeben würde. Also änderte ich es für eine bestimmte Art der Validierung, in meinem Fall war es für eine E-Mail-Adresse.

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

Das funktioniert viel besser! Es stürzt nicht ab und erzeugt eine nette Fehlermeldung. Hoffe das hilft jemandem!