Come posso usare IValidatableObject?


182

Capisco che IValidatableObjectviene utilizzato per convalidare un oggetto in un modo che consente di confrontare le proprietà l'una con l'altra.

Vorrei comunque avere degli attributi per convalidare le singole proprietà, ma in alcuni casi desidero ignorare gli errori di alcune proprietà.

Sto cercando di usarlo in modo errato nel caso seguente? In caso contrario, 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.
             */ 
        }
    }
}

Risposte:


168

Prima di tutto, grazie a @ paper1337 per avermi indicato le giuste risorse ... Non sono registrato, quindi non posso votarlo, per favore fallo se qualcun altro lo legge.

Ecco come realizzare ciò che stavo cercando di fare.

Classe valida:

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

L'uso Validator.TryValidateProperty()si aggiungerà alla raccolta dei risultati in caso di convalide non riuscite. Se non viene eseguita una convalida non riuscita, non verrà aggiunto nulla alla raccolta dei risultati, il che indica un esito positivo.

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

È importante impostare validateAllPropertiessu false affinché questo metodo funzioni. Quando validateAllPropertiesè falso, [Required]vengono controllate solo le proprietà con un attributo. Ciò consente al IValidatableObject.Validate()metodo di gestire le convalide condizionali.


Non riesco a pensare a uno scenario in cui lo userei. Puoi darmi un esempio di dove lo useresti?
Stefan Vasiljevic,

Se nella tabella sono presenti colonne di tracciamento (ad esempio l'utente che l'ha creato). È richiesto nel database ma si inserisce nel SaveChanges nel contesto per popolarlo (eliminando la necessità per gli sviluppatori di ricordare di averlo impostato esplicitamente). Ovviamente, dovresti confermare prima di salvare. Quindi non contrassegnare la colonna "creatore" come richiesto ma convalidare con tutte le altre colonne / proprietà.
MetalPhoenix,

Il problema con questa soluzione è che ora si dipende dal chiamante per la corretta validazione dell'oggetto.
Cocogza,

Per migliorare questa risposta, si potrebbe usare la riflessione per trovare tutte le proprietà che hanno attributi di validazione, quindi chiamare TryValidateProperty.
Paul Chernoch,

78

Citazione dal post sul blog di Jeff Handley su oggetti e proprietà di validazione con validatore :

Quando si convalida un oggetto, in Validator.ValidateObject viene applicato il seguente processo:

  1. Convalida attributi a livello di proprietà
  2. Se alcuni validatori non sono validi, interrompere la convalida restituendo gli errori
  3. Convalida gli attributi a livello di oggetto
  4. Se alcuni validatori non sono validi, interrompere la convalida restituendo gli errori
  5. Se sul framework desktop e l'oggetto implementa IValidatableObject, chiama il suo metodo Validate e restituisci eventuali errori

Ciò indica che ciò che si sta tentando di fare non funzionerà immediatamente perché la convalida verrà interrotta al passaggio 2. È possibile provare a creare attributi che ereditano da quelli incorporati e verificare specificamente la presenza di una proprietà abilitata (tramite un'interfaccia) prima di eseguire la loro normale convalida. In alternativa, è possibile inserire tutta la logica per la convalida dell'entità nel Validatemetodo.


36

Solo per aggiungere un paio di punti:

Poiché Validate()viene restituita la firma del metodo IEnumerable<>, che yield returnpuò essere utilizzata per generare pigramente i risultati, ciò è vantaggioso se alcuni dei controlli di convalida richiedono IO o 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 gli errori del risultato della convalida in ModelStatevoci come segue (ciò potrebbe essere utile se si sta eseguendo la convalida in un raccoglitore modello personalizzato ):

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

Ben fatto! Vale la pena usare gli attributi e la riflessione nel metodo Convalida?
Schalk,

4

Ho implementato una classe astratta di utilizzo 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;
        }


    }
}

1
Adoro quello stile di utilizzo dei parametri nominati, rende il codice molto più facile da leggere.
drizin,

0

Il problema con la risposta accettata è che ora dipende dal chiamante per la corretta validazione dell'oggetto. Vorrei rimuovere RangeAttribute e fare la convalida dell'intervallo all'interno del metodo Convalida oppure creerei una sottoclasse degli attributi personalizzati RangeAttribute che prende il nome della proprietà richiesta come argomento sul costruttore.

Per 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 è piaciuta la risposta di cocogza, tranne quella che ha chiamato base.IsValid ha provocato un'eccezione di overflow dello stack in quanto reinserirebbe nuovamente il metodo IsValid. Quindi l'ho modificato per un determinato tipo di convalida, nel mio caso era per 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;
    }
}

Funziona molto meglio! Non si arresta in modo anomalo e genera un bel messaggio di errore. Spero che questo aiuti qualcuno!


0

La cosa che non mi piace di iValidate è che sembra funzionare solo DOPO tutte le altre convalide.
Inoltre, almeno nel nostro sito, verrà eseguito nuovamente durante un tentativo di salvataggio. Ti suggerirei semplicemente di creare una funzione e inserire tutto il codice di convalida in quello. In alternativa per i siti Web, è possibile avere la convalida "speciale" nel controller dopo la creazione del modello. Esempio:

 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
            {
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.