Comment utiliser IValidatableObject?

182

Je comprends que cela IValidatableObjectsert à valider un objet d'une manière qui permet de comparer les propriétés les unes par rapport aux autres.

J'aimerais toujours avoir des attributs pour valider les propriétés individuelles, mais je veux ignorer les échecs sur certaines propriétés dans certains cas.

Est-ce que j'essaye de ne pas l'utiliser correctement dans le cas ci-dessous? Sinon, comment puis-je mettre en œuvre 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.
             */ 
        }
    }
}
zrg
la source

Réponses:

170

Tout d'abord, merci à @ paper1337 de m'avoir indiqué les bonnes ressources ... Je ne suis pas inscrit, donc je ne peux pas le voter, s'il vous plaît, faites-le si quelqu'un d'autre lit ceci.

Voici comment accomplir ce que j'essayais de faire.

Classe validable:

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'utilisation Validator.TryValidateProperty()ajoutera à la collection de résultats en cas d'échec des validations. S'il n'y a pas d'échec de la validation, rien ne sera ajouté à la collection de résultats, ce qui est une indication de 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 validateAllPropertiessur false pour que cette méthode fonctionne. Quand validateAllPropertiesest faux, seules les propriétés avec un [Required]attribut sont vérifiées. Cela permet à la IValidatableObject.Validate()méthode de gérer les validations conditionnelles.

zrg
la source
Je ne peux pas penser à un scénario où j'utiliserais cela. Pouvez-vous me donner un exemple où vous utiliseriez cela?
Stefan Vasiljevic
Si vous avez des colonnes de suivi dans votre tableau (comme l'utilisateur qui l'a créé). Il est requis dans la base de données mais vous entrez dans SaveChanges dans le contexte pour le remplir (éliminant le besoin pour les développeurs de se souvenir de le définir explicitement). Vous devez, bien sûr, valider avant de sauvegarder. Donc, vous ne marquez pas la colonne "créateur" comme obligatoire mais validez par rapport à toutes les autres colonnes / propriétés.
MetalPhoenix
Le problème avec cette solution est que maintenant vous dépendez de l'appelant pour que votre objet soit correctement validé.
cocogza
Pour améliorer cette réponse, vous pouvez utiliser la réflexion pour trouver toutes les propriétés qui ont des attributs de validation, puis appeler TryValidateProperty.
Paul Chernoch
78

Citation du billet de blog de Jeff Handley sur les objets et propriétés de validation avec Validator :

Lors de la validation d'un objet, le processus suivant est appliqué dans Validator.ValidateObject:

  1. Valider les attributs au niveau de la propriété
  2. Si des validateurs ne sont pas valides, abandonnez la validation en renvoyant le ou les échecs
  3. Validez les attributs au niveau de l'objet
  4. Si des validateurs ne sont pas valides, abandonnez la validation en renvoyant le ou les échecs
  5. Si sur le framework de bureau et que l'objet implémente IValidatableObject, appelez sa méthode Validate et retournez tout échec

Cela indique que ce que vous essayez de faire ne fonctionnera pas directement car la validation sera abandonnée à l'étape 2. Vous pouvez essayer de créer des attributs qui héritent des attributs intégrés et vérifier spécifiquement la présence d'une propriété activée (via une interface) avant d'effectuer leur validation normale. Vous pouvez également mettre toute la logique de validation de l'entité dans la Validateméthode.

Vous pouvez également jeter un œil à l'implémentation exacte de la Validatorclasse ici

Chris crie
la source
36

Juste pour ajouter quelques points:

Étant donné que la Validate()signature de la méthode retourne IEnumerable<>, cela yield returnpeut être utilisé pour générer les résultats paresseusement - cela est avantageux si certains des contrôles de validation sont gourmands en E / S ou en 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");
        }

De plus, si vous utilisez MVC ModelState, vous pouvez convertir les échecs de résultats de validation en ModelStateentrées comme suit (cela peut être utile si vous effectuez la validation dans un classeur de modèles 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)));
}
StuartLC
la source
Joli! Vaut-il la peine d'utiliser les attributs et la réflexion dans la méthode Validate?
Schalk
4

J'ai implémenté une classe abstraite d'usage général 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;
        }


    }
}
guneysus
la source
1
J'adore ce style d'utilisation de paramètres nommés, ce qui rend le code beaucoup plus facile à lire.
drizin
0

Le problème avec la réponse acceptée est qu'elle dépend désormais de l'appelant pour que l'objet soit correctement validé. Je supprimerais le RangeAttribute et ferais la validation de plage dans la méthode Validate ou je créerais un attribut personnalisé sous-classant RangeAttribute qui prend le nom de la propriété requise comme argument sur le 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;
    }
}
Cocogza
la source
0

J'ai aimé la réponse de cocogza, sauf que l'appel de base.IsValid a entraîné une exception de débordement de pile car il ré-entrerait encore et encore la méthode IsValid. Donc je l'ai modifié pour être pour un type de validation spécifique, dans mon cas c'était pour 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 plante pas et produit un joli message d'erreur. J'espère que cela aide quelqu'un!

rjacobsen0
la source
0

Ce que je n'aime pas à propos d'iValidate, c'est qu'il semble ne fonctionner qu'APRÈS toutes les autres validations.
De plus, au moins sur notre site, il s'exécuterait à nouveau lors d'une tentative de sauvegarde. Je vous suggère simplement de créer une fonction et d'y placer tout votre code de validation. Alternativement, pour les sites Web, vous pouvez avoir votre validation «spéciale» dans le contrôleur après la création du modèle. Exemple:

 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
la source