Come si usa IValidatableObject?

Capisco che IValidatableObject è usato per convalidare un object in un modo che consente di confrontare le proprietà l’una con l’altra.

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

Sto tentando di usarlo in modo errato nel caso qui sotto? 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 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. */ } } } 

Prima di tutto, grazie a @ paper1337 per avermi indirizzato alle risorse giuste … Non sono registrato, quindi non posso votarlo, per favore fallo se qualcuno 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 Validate(ValidationContext validationContext) { var results = new List(); 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’utilizzo di Validator.TryValidateProperty() si aggiungerà alla raccolta dei risultati se sono presenti convalide non riuscite. Se non c’è una validazione fallita, nulla sarà aggiunto alla raccolta dei risultati che è un’indicazione di successo.

Facendo la convalida:

  public void DoValidation() { var toValidate = new ValidateMe() { Enable = true, Prop1 = 1, Prop2 = 2 }; bool validateAllProperties = false; var results = new List(); bool isValid = Validator.TryValidateObject( toValidate, new ValidationContext(toValidate, null, null), results, validateAllProperties); } 

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

Citazione dal blog Post di Jeff Handley su Validation Objects and Properties with Validator :

Durante la convalida di un object, in Validator.ValidateObject viene applicato il seguente processo:

  1. Convalidare gli attributi a livello di proprietà
  2. Se i validatori non sono validi, annulla la convalida restituendo l’errore / i
  3. Convalidare gli attributi a livello di object
  4. Se i validatori non sono validi, annulla la convalida restituendo l’errore / i
  5. Se sul framework desktop e l’object implementa IValidatableObject, quindi chiama il suo metodo Validate e restituisce qualsiasi errore (s)

Questo indica che ciò che stai tentando di fare non funzionerà immediatamente perché la convalida verrà interrotta al punto 2. Si può 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 validazione. In alternativa, è ansible inserire tutta la logica per la convalida dell’entity framework nel metodo Validate .

Solo per aggiungere un paio di punti:

Poiché la firma del metodo Validate() restituisce IEnumerable<> , è ansible utilizzare yield return per generare generosamente i risultati. Ciò è utile se alcuni dei controlli di convalida sono intenti all’IO o alla CPU.

 public IEnumerable 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 , è ansible convertire gli errori dei risultati di convalida in voci ModelState come segue (ciò potrebbe essere utile se si esegue 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))); } 

Ho implementato una class astratta di 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 Validate() { var vResults = new List(); 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; } } } 

Il problema con la risposta accettata è che ora dipende dal chiamante affinché l’object sia validato correttamente. Vorrei rimuovere il RangeAttribute e fare la convalida dell’intervallo all’interno del metodo Validate o vorrei creare una sottoclass di 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; } } 

Mi è piaciuta la risposta di cocogza tranne quella chiamata base. IsValid ha provocato un’eccezione di overflow dello stack in quanto potrebbe reinserire il metodo IsValid ancora e ancora. 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; } } 

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