web-dev-qa-db-fra.com

Validation de modèle personnalisé des propriétés dépendantes à l'aide d'annotations de données

Depuis maintenant, j'ai utilisé l'excellente bibliothèque FluentValidation pour valider mes classes de modèles. Dans les applications Web, je l'utilise en conjonction avec le plugin jquery.validate pour effectuer également la validation côté client. Un inconvénient est qu'une grande partie de la logique de validation est répétée côté client et n'est plus centralisée en un seul endroit.

Pour cette raison, je recherche une alternative. Il y a beaucoup exemples montrant l'utilisation des annotations de données pour effectuer la validation du modèle. Cela semble très prometteur. Une chose que je n'ai pas pu découvrir est de savoir comment valider une propriété qui dépend d'une autre valeur de propriété.

Prenons par exemple le modèle suivant:

public class Event
{
    [Required]
    public DateTime? StartDate { get; set; }
    [Required]
    public DateTime? EndDate { get; set; }
}

Je voudrais m'assurer que EndDate est supérieur à StartDate. Je pourrais écrire un attribut de validation personnalisé étendant ValidationAttribute afin d'effectuer une logique de validation personnalisée. Malheureusement, je n'ai pas trouvé de moyen d'obtenir l'instance de modèle:

public class CustomValidationAttribute : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        // value represents the property value on which this attribute is applied
        // but how to obtain the object instance to which this property belongs?
        return true;
    }
}

J'ai trouvé que CustomValidationAttribute semble faire le travail car il a cette propriété ValidationContext qui contient l'instance d'objet en cours de validation. Malheureusement, cet attribut n'a été ajouté que dans .NET 4.0. Ma question est donc la suivante: puis-je obtenir les mêmes fonctionnalités dans .NET 3.5 SP1?


MISE À JOUR:

Il semble que FluentValidation prend déjà en charge validation côté client et métadonnées dans ASP.NET MVC 2.

Il serait néanmoins bon de savoir si des annotations de données pourraient être utilisées pour valider les propriétés dépendantes.

43
Darin Dimitrov

MVC2 est livré avec un exemple "PropertiesMustMatchAttribute" qui montre comment faire fonctionner DataAnnotations pour vous et il devrait fonctionner à la fois dans .NET 3.5 et .NET 4.0. Cet exemple de code ressemble à ceci:

[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public sealed class PropertiesMustMatchAttribute : ValidationAttribute
{
    private const string _defaultErrorMessage = "'{0}' and '{1}' do not match.";

    private readonly object _typeId = new object();

    public PropertiesMustMatchAttribute(string originalProperty, string confirmProperty)
        : base(_defaultErrorMessage)
    {
        OriginalProperty = originalProperty;
        ConfirmProperty = confirmProperty;
    }

    public string ConfirmProperty
    {
        get;
        private set;
    }

    public string OriginalProperty
    {
        get;
        private set;
    }

    public override object TypeId
    {
        get
        {
            return _typeId;
        }
    }

    public override string FormatErrorMessage(string name)
    {
        return String.Format(CultureInfo.CurrentUICulture, ErrorMessageString,
            OriginalProperty, ConfirmProperty);
    }

    public override bool IsValid(object value)
    {
        PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(value);
        object originalValue = properties.Find(OriginalProperty, true /* ignoreCase */).GetValue(value);
        object confirmValue = properties.Find(ConfirmProperty, true /* ignoreCase */).GetValue(value);
        return Object.Equals(originalValue, confirmValue);
    }
}

Lorsque vous utilisez cet attribut, plutôt que de le placer sur une propriété de votre classe modèle, vous le placez sur la classe elle-même:

[PropertiesMustMatch("NewPassword", "ConfirmPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public class ChangePasswordModel
{
    public string NewPassword { get; set; }
    public string ConfirmPassword { get; set; }
}

Lorsque "IsValid" est appelé sur votre attribut personnalisé, l'instance de modèle entière lui est transmise afin que vous puissiez obtenir les valeurs de propriété dépendantes de cette façon. Vous pouvez facilement suivre ce modèle pour créer un attribut de comparaison de date, ou même un attribut de comparaison plus général.

Brad Wilson a un bon exemple sur son blog montrant comment ajouter la partie côté client de la validation également, bien que je ne suis pas sûr si cet exemple fonctionnera à la fois dans .NET 3.5 et .NET 4.0.

29
Travis Illig

Au lieu de PropertiesMustMatch, CompareAttribute peut être utilisé dans MVC3. Selon ce lien http://devtrends.co.uk/blog/the-complete-guide-to-validation-in-asp.net-mvc-3-part-1 :

public class RegisterModel
{
    // skipped

    [Required]
    [ValidatePasswordLength]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }                       

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password", ErrorMessage = "The password and confirmation do not match.")]
    public string ConfirmPassword { get; set; }
}

CompareAttribute est un nouveau validateur très utile qui ne fait pas réellement partie de System.ComponentModel.DataAnnotations, mais a été ajouté à System.Web.Mvc = DLL par l'équipe. Bien qu'il ne soit pas particulièrement bien nommé (la seule comparaison qu'il fait est de vérifier l'égalité, donc peut-être EqualTo serait plus évident), il est facile de voir à partir de l'utilisation que ce validateur vérifie que la valeur d'une propriété est égale à la valeur d'une autre propriété. Vous pouvez voir dans le code que l'attribut prend une propriété de chaîne qui est le nom de l'autre propriété que vous comparez. L'utilisation classique de ce type de validateur c'est pourquoi nous l'utilisons ici: confirmation du mot de passe.

7
orcy

Cela a pris un peu de temps depuis que votre question a été posée, mais si vous aimez toujours les métadonnées (au moins parfois), ci-dessous il y a encore une autre solution alternative, qui vous permet de fournir diverses expressions logiques aux attributs:

[Required]
public DateTime? StartDate { get; set; }    
[Required]
[AssertThat("StartDate != null && EndDate > StartDate")]
public DateTime? EndDate { get; set; }

Cela fonctionne aussi bien pour le serveur que pour le côté client. Plus de détails peut être trouvé ici .

4
jwaliszko

Parce que les méthodes des DataAnnotations de .NET 3.5 ne vous permettent pas de fournir l'objet réel validé ou un contexte de validation, vous devrez faire un peu de ruse pour accomplir cela. Je dois admettre que je ne suis pas familier avec ASP.NET MVC, donc je ne peux pas dire comment faire exactement en conjonction avec MCV, mais vous pouvez essayer d'utiliser une valeur statique de thread pour passer l'argument lui-même. Voici un exemple avec quelque chose qui pourrait fonctionner.

Créez d'abord une sorte de "portée d'objet" qui vous permet de passer des objets sans avoir à les passer à travers la pile d'appels:

public sealed class ContextScope : IDisposable 
{
    [ThreadStatic]
    private static object currentContext;

    public ContextScope(object context)
    {
        currentContext = context;
    }

    public static object CurrentContext
    {
        get { return context; }
    }

    public void Dispose()
    {
        currentContext = null;
    }
}

Ensuite, créez votre validateur pour utiliser le ContextScope:

public class CustomValidationAttribute : ValidationAttribute
{
    public override bool IsValid(object value)
    {
         Event e = (Event)ObjectContext.CurrentContext;

         // validate event here.
    }
}

Et enfin et surtout, assurez-vous que l'objet est passé à travers le ContextScope:

Event eventToValidate = [....];
using (var scope new ContextScope(eventToValidate))
{
    DataAnnotations.Validator.Validate(eventToValidate);
}

Est-ce utile?

3
Steven