Wie verwende ich IValidatableObject?

180

Ich verstehe, dass dies IValidatableObjectverwendet wird, um ein Objekt so zu validieren, dass man Eigenschaften miteinander vergleichen kann.

Ich hätte gerne noch Attribute zum Überprüfen einzelner Eigenschaften, möchte aber in bestimmten Fällen Fehler bei einigen Eigenschaften ignorieren.

Versuche ich es im folgenden Fall 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.
             */ 
        }
    }
}
zrg
quelle

Antworten:

167

Zunächst einmal danke ich @ paper1337, dass Sie mich auf die richtigen Ressourcen hingewiesen haben ... Ich bin nicht registriert, daher kann ich ihn nicht abstimmen. Bitte tun Sie dies, wenn jemand anderes dies liest.

Hier erfahren Sie, wie Sie das erreichen, was ich versucht habe.

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

Mit Validator.TryValidateProperty()wird die Ergebnissammlung erweitert, wenn die Validierung fehlschlägt. Wenn keine Validierung fehlschlägt, wird der Ergebnissammlung nichts hinzugefügt, was ein Hinweis auf den Erfolg ist.

Validierung durchführen:

    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, validateAllPropertiesauf false zu setzen , damit diese Methode funktioniert. Wenn validateAllPropertiesfalse ist, werden nur Eigenschaften mit einem [Required]Attribut überprüft. Dadurch kann die IValidatableObject.Validate()Methode die bedingten Validierungen verarbeiten.

zrg
quelle
Ich kann mir kein Szenario vorstellen, in dem ich das verwenden würde. Können Sie mir ein Beispiel geben, wo Sie dies verwenden würden?
Stefan Vasiljevic
Wenn Ihre Tabelle Verfolgungsspalten enthält (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 (Entwickler müssen sich nicht mehr daran erinnern, sie explizit festzulegen). Sie würden natürlich vor dem Speichern validieren. Sie markieren die Spalte "Ersteller" also nicht als erforderlich, sondern validieren sie gegen alle anderen Spalten / Eigenschaften.
MetalPhoenix
Das Problem bei dieser Lösung besteht darin, dass Sie jetzt vom Aufrufer abhängig sind, damit Ihr Objekt ordnungsgemäß validiert wird.
Cocogza
Um diese Antwort zu verbessern, könnte man Reflection verwenden, um alle Eigenschaften mit Validierungsattributen zu finden, und dann TryValidateProperty aufrufen.
Paul Chernoch
78

Zitat aus Jeff Handleys Blogpost zu Validierungsobjekten und -eigenschaften mit Validator :

Bei der Validierung eines Objekts wird in Validator.ValidateObject der folgende Prozess angewendet:

  1. Überprüfen Sie Attribute auf Eigenschaftsebene
  2. Wenn Validatoren ungültig sind, brechen Sie die Validierung ab und geben Sie die Fehler zurück.
  3. Überprüfen Sie die Attribute auf Objektebene
  4. Wenn Validatoren ungültig sind, brechen Sie die Validierung ab und geben Sie die Fehler zurück.
  5. Wenn auf dem Desktop-Framework und dem Objekt IValidatableObject implementiert ist, rufen Sie die Validate-Methode auf und geben Sie alle Fehler zurück.

Dies weist darauf hin, dass das, was Sie versuchen, nicht sofort funktioniert, da die Validierung in Schritt 2 abgebrochen wird. Sie können versuchen, Attribute zu erstellen, die von den integrierten Attributen erben, und insbesondere das Vorhandensein einer aktivierten Eigenschaft (über eine Schnittstelle) überprüfen, bevor Sie deren normale Validierung durchführen. Alternativ können Sie die gesamte Logik zur Validierung der Entität in die ValidateMethode einfügen.

Chris schreit
quelle
36

Nur um ein paar Punkte hinzuzufügen:

Da die Validate()Methodensignatur zurückgegeben IEnumerable<>wird, yield returnkönnen damit die Ergebnisse träge generiert werden. Dies ist von Vorteil, wenn einige der Validierungsprüfungen E / A- oder CPU-intensiv 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");
        }

Wenn Sie verwenden MVC ModelState, können Sie die Fehler des Validierungsergebnisses ModelStatewie folgt in Einträge konvertieren (dies kann hilfreich sein, wenn Sie die Validierung in einem benutzerdefinierten Modellordner durchführen ):

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)));
}
StuartLC
quelle
Schön! Lohnt es sich, Attribute und Reflexionen in der Validate-Methode zu verwenden?
Schalk
4

Ich habe eine abstrakte Klasse für die allgemeine Verwendung zur Validierung implementiert

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


    }
}
Guneysos
quelle
1
Ich liebe diesen Stil der Verwendung benannter Parameter, der das Lesen von Code erheblich erleichtert.
Drizin
0

Das Problem mit der akzeptierten Antwort besteht darin, dass es jetzt vom Aufrufer abhängt, ob das Objekt ordnungsgemäß validiert wird. Ich würde entweder das RangeAttribute entfernen und die Bereichsüberprüfung innerhalb der Validate-Methode durchführen, oder ich würde eine benutzerdefinierte Attributunterklasse RangeAttribute erstellen, die den Namen der erforderlichen Eigenschaft als Argument für den Konstruktor verwendet.

Beispielsweise:

[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;
    }
}
Cocogza
quelle
0

Ich mochte die Antwort von cocogza mit der Ausnahme, dass das Aufrufen von base.IsValid zu einer Stapelüberlaufausnahme führte, da die IsValid-Methode immer wieder neu eingegeben wurde. Deshalb habe ich es für eine bestimmte Art der Validierung geändert, in meinem Fall 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!

rjacobsen0
quelle
0

Das, was ich an iValidate nicht mag, ist, dass es nur nach allen anderen Überprüfungen ausgeführt wird.
Zumindest auf unserer Website würde es außerdem während eines Sicherungsversuchs erneut ausgeführt. Ich würde vorschlagen, dass Sie einfach eine Funktion erstellen und Ihren gesamten Validierungscode darin platzieren. Alternativ können Sie für Websites Ihre "spezielle" Validierung im Controller haben, nachdem das Modell erstellt wurde. Beispiel:

 public ActionResult Update([DataSourceRequest] DataSourceRequest request, [Bind(Exclude = "Terminal")] Driver driver)
    {

        if (db.Drivers.Where(m => m.IDNumber == driver.IDNumber && m.ID != driver.ID).Any())
        {
            ModelState.AddModelError("Update", string.Format("ID # '{0}' is already in use", driver.IDNumber));
        }
        if (db.Drivers.Where(d => d.CarrierID == driver.CarrierID
                                && d.FirstName.Equals(driver.FirstName, StringComparison.CurrentCultureIgnoreCase)
                                && d.LastName.Equals(driver.LastName, StringComparison.CurrentCultureIgnoreCase)
                                && (driver.ID == 0 || d.ID != driver.ID)).Any())
        {
            ModelState.AddModelError("Update", "Driver already exists for this carrier");
        }

        if (ModelState.IsValid)
        {
            try
            {
John Lord
quelle