Question

Je comprends que IValidatableObject est utilisé pour valider un objet d'une manière qui nous laissent un comparer les propriétés contre l'autre.

Je voudrais encore avoir des attributs pour valider les propriétés individuelles, mais je veux d'ignorer les défaillances sur certaines propriétés dans certains cas.

Suis-je en train de l'utiliser de manière incorrecte dans le cas ci-dessous? Sinon, comment puis-je mettre cela?

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.
             */ 
        }
    }
}
Était-ce utile?

La solution

Tout d'abord, grâce à @ paper1337 pour moi pointant vers les bonnes ressources ... Je ne suis pas inscrit, donc je ne peux pas le voter en, s'il vous plaît le faire si quelqu'un d'autre lit cela.

Voici comment accomplir ce que je voulais faire.

class Validatable:

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

Utilisation Validator.TryValidateProperty() va ajouter à la collection de résultats s'il y a échoué à l'. S'il n'y a pas une validation a échoué alors rien ne sera ajouter à la collection de résultat qui est une indication du succès.

Faire la validation:

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

Il est important de définir validateAllProperties à false pour que cette méthode fonctionne. Lorsque validateAllProperties est faux propriétés seulement avec un attribut [Required] sont vérifiées. Cela permet à la méthode de IValidatableObject.Validate() gérer les validations conditionnelles.

Autres conseils

Citation de Jeff Handley Blog post sur la validation des objets et des propriétés avec Validator :

  

Lors de la validation d'un objet, la   Procédé suivant est appliqué en   Validator.ValidateObject:

     
      
  1. Valider niveau de propriété attributs
  2.   
  3. Si des validateurs ne sont pas valides, abort validation retour de la   échec (s)
  4.   
  5. Valider les attributs de niveau objet
  6.   
  7. Si des validateurs ne sont pas valides, abort validation retour de la   échec (s)
  8.   
  9. Si le cadre de bureau et les outils d'objet   IValidatableObject, puis appeler son   Valider et retourner tout procédé   échec (s)
  10.   

Ceci indique que ce que vous essayez de faire ne fonctionnera pas hors-the-box, car la validation abandonnera à l'étape 2. Vous pouvez essayer de créer des attributs qui héritent de ceux intégrés et vérifier spécifiquement la présence d'une propriété est activée (via une interface) avant d'effectuer leur validation normale. Sinon, vous pouvez mettre toute la logique de validation de l'entité dans la méthode Validate.

Il suffit d'ajouter quelques points:

Parce que la méthode retourne la signature de Validate() IEnumerable<>, que yield return peut être utilisé pour générer paresseusement les résultats -. ce qui est bénéfique si certains des contrôles de validation sont IO ou UC

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

En outre, si vous utilisez MVC ModelState, vous pouvez convertir les échecs de résultats de validation ModelState entrées comme suit (cela pourrait être utile si vous faites la validation dans un liant modèle personnalisé ):

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

Je mis en place un usage général classe abstraite pour la validation

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


    }
}

Le problème avec la réponse acceptée est que cela dépend maintenant de l'appelant pour l'objet soit correctement validé. Je soit supprimer le RangeAttribute et faire la validation de la plage dans la méthode Valider ou je voudrais créer un attribut personnalisé qui prend RangeAttribute le sous-classement le nom de la propriété requise comme argument du constructeur.

Par exemple:

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

J'ai aimé réponse cocogza sauf que l'appel base.IsValid a donné lieu à une exception de débordement de pile comme il rentrerait le encore et encore la méthode isValid. Donc, je l'ai modifié pour être un type de validation spécifique, dans mon cas, il était une adresse 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;
    }
}

Cela fonctionne beaucoup mieux! Il ne tombe pas en panne et produit un beau message d'erreur. Espérons que cela aide quelqu'un!

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top