web-dev-qa-db-fra.com

Attributs de validation personnalisés: comparaison de deux propriétés dans le même modèle

Existe-t-il un moyen de créer un attribut personnalisé dans ASP.NET Core pour valider si une propriété de date est inférieure à une autre propriété de date dans un modèle en utilisant ValidationAttribute.

Disons que j'ai ceci:

public class MyViewModel 
{
    [Required]
    [CompareDates]
    public DateTime StartDate { get; set; }

    [Required]
    public DateTime EndDate { get; set; } = DateTime.Parse("3000-01-01");
}

J'essaie d'utiliser quelque chose comme ça:

    public class CompareDates : ValidationAttribute
{
    public CompareDates()
        : base("") { }

    public override bool IsValid(object value)
    {
        return base.IsValid(value);
    }

}

J'ai trouvé un autre SO post qui propose d'utiliser une autre bibliothèque, mais je préfère m'en tenir à ValidationAttribute si c'était faisable.

15
Hussein Salman

Vous pouvez créer un attribut de validation personnalisé pour comparer deux propriétés. C'est une validation côté serveur:

public class MyViewModel
{
    [DateLessThan("End", ErrorMessage = "Not valid")]
    public DateTime Begin { get; set; }

    public DateTime End { get; set; }
}

public class DateLessThanAttribute : ValidationAttribute
{
    private readonly string _comparisonProperty;

    public DateLessThanAttribute(string comparisonProperty)
    {
         _comparisonProperty = comparisonProperty;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        ErrorMessage = ErrorMessageString;
        var currentValue = (DateTime)value;

        var property = validationContext.ObjectType.GetProperty(_comparisonProperty);

        if (property == null)
            throw new ArgumentException("Property with this name not found");

        var comparisonValue = (DateTime)property.GetValue(validationContext.ObjectInstance);

        if (currentValue > comparisonValue)
            return new ValidationResult(ErrorMessage);

        return ValidationResult.Success;
    }
}

Mise à jour : Si vous avez besoin d'une validation côté client pour cet attribut, vous devez implémenter une interface IClientModelValidator:

public class DateLessThanAttribute : ValidationAttribute, IClientModelValidator
{
    ...
    public void AddValidation(ClientModelValidationContext context)
    {
        var error = FormatErrorMessage(context.ModelMetadata.GetDisplayName());
        context.Attributes.Add("data-val", "true");
        context.Attributes.Add("data-val-error", error);
    }
}

La méthode AddValidation ajoutera des attributs à vos entrées depuis context.Attributes.

enter image description here

Vous pouvez en savoir plus ici IClientModelValidator

32
Alexander Gore

Comme une option possible auto-validation:

Vous avez juste besoin d'implémenter une interface IValidatableObject avec la méthode Validate, où vous pouvez mettre votre code de validation.

public class MyViewModel : IValidatableObject
{
[Required]
public DateTime StartDate { get; set; }

[Required]
public DateTime EndDate { get; set; } = DateTime.Parse("3000-01-01");

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        int result = DateTime.Compare(StartDate , EndDate);
        if (result < 0)
        {
            yield return new ValidationResult("start date must be less than the end date!", new [] { "ConfirmEmail" });
        }
    }
}
12
Curiousdev

Sur la base de la réponse d'Alexander Gore, je suggère une validation meilleure et générique (et elle est compatible avec le noyau .Net). Lorsque vous souhaitez comparer des propriétés à l'aide de la logique GreatherThan ou LessThan (quels que soient les types), vous pouvez valider si elles ont implémenté l'interface IComparable. Si les deux propriétés sont valides, vous pouvez utiliser l'implémentation CompareTo. Cette règle s'applique également aux types DateTime et aux nombres

LessThan

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public class LessThanAttribute : ValidationAttribute
{
    private readonly string _comparisonProperty;

    public LessThanAttribute(string comparisonProperty)
    {
        _comparisonProperty = comparisonProperty;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        ErrorMessage = ErrorMessageString;

        if (value.GetType() == typeof(IComparable))
        {
            throw new ArgumentException("value has not implemented IComparable interface");
        }

        var currentValue = (IComparable)value;

        var property = validationContext.ObjectType.GetProperty(_comparisonProperty);

        if (property == null)
        {
            throw new ArgumentException("Comparison property with this name not found");
        }

        var comparisonValue = property.GetValue(validationContext.ObjectInstance);

        if (comparisonValue.GetType() == typeof(IComparable))
        {
            throw new ArgumentException("Comparison property has not implemented IComparable interface");
        }

        if (!ReferenceEquals(value.GetType(), comparisonValue.GetType()))
        {
            throw new ArgumentException("The properties types must be the same");
        }

        if (currentValue.CompareTo((IComparable)comparisonValue) >= 0)
        {
            return new ValidationResult(ErrorMessage);
        }

        return ValidationResult.Success;
    }
}

GreaterThan

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public class GreaterThanAttribute : ValidationAttribute
{
    private readonly string _comparisonProperty;

    public GreaterThanAttribute(string comparisonProperty)
    {
        _comparisonProperty = comparisonProperty;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        ErrorMessage = ErrorMessageString;

        if (value.GetType() == typeof(IComparable))
        {
            throw new ArgumentException("value has not implemented IComparable interface");
        }

        var currentValue = (IComparable)value;

        var property = validationContext.ObjectType.GetProperty(_comparisonProperty);

        if (property == null)
        {
            throw new ArgumentException("Comparison property with this name not found");
        }

        var comparisonValue = property.GetValue(validationContext.ObjectInstance);

        if (comparisonValue.GetType() == typeof(IComparable))
        {
            throw new ArgumentException("Comparison property has not implemented IComparable interface");
        }

        if (!ReferenceEquals(value.GetType(), comparisonValue.GetType()))
        {
            throw new ArgumentException("The properties types must be the same");
        }

        if (currentValue.CompareTo((IComparable)comparisonValue) < 0)
        {
            return new ValidationResult(ErrorMessage);
        }

        return ValidationResult.Success;
    }
}

Dans un contexte de réservation, un exemple pourrait être le suivant:

public DateTime CheckInDate { get; set; }

[GreaterThan("CheckInDate", ErrorMessage = "CheckOutDate must be greater than CheckInDate")]
public DateTime CheckOutDate { get; set; }
3
Jaime Marín

Vous pouvez comparer les deux dates dans la méthode IsValid.

public class CompareDates : ValidationAttribute
{
    protected override ValidationResult
            IsValid(object value, ValidationContext validationContext)
    {
        //get your startdate & end date from model and value


        //perform comparison
        if (StartDate < EndDate)
        {
            return new ValidationResult
                ("start date must be less than the end date");
        }
        else
        {
            return ValidationResult.Success;
        }
    }
}
2
Balaji Marimuthu

Basé sur la réponse de Jaime et le commentaire de Jeffrey concernant la nécessité d'avoir un seul attribut pour inférieur à, inférieur à ou égal à, égal à, supérieur à, supérieur à ou égal à.

Le code ci-dessous gérera toutes les conditions avec un seul attribut.

public enum ComparisonType
{
    LessThan,
    LessThanOrEqualTo,
    EqualTo,
    GreaterThan,
    GreaterThanOrEqualTo
}

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter)]
public class ComparisonAttribute : ValidationAttribute
{
    private readonly string _comparisonProperty;
    private readonly ComparisonType _comparisonType;

    public ComparisonAttribute(string comparisonProperty, ComparisonType comparisonType)
    {
        _comparisonProperty = comparisonProperty;
        _comparisonType = comparisonType;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        ErrorMessage = ErrorMessageString;

        if (value.GetType() == typeof(IComparable))
        {
            throw new ArgumentException("value has not implemented IComparable interface");
        }

        var currentValue = (IComparable) value;

        var property = validationContext.ObjectType.GetProperty(_comparisonProperty);

        if (property == null)
        {
            throw new ArgumentException("Comparison property with this name not found");
        }

        var comparisonValue = property.GetValue(validationContext.ObjectInstance);

        if (comparisonValue.GetType() == typeof(IComparable))
        {
            throw new ArgumentException("Comparison property has not implemented IComparable interface");
        }

        if (!ReferenceEquals(value.GetType(), comparisonValue.GetType()))
        {
            throw new ArgumentException("The properties types must be the same");
        }

        bool compareToResult;

        switch (_comparisonType)
        {
            case ComparisonType.LessThan:
                compareToResult = currentValue.CompareTo((IComparable) comparisonValue) >= 0;
                break;
            case ComparisonType.LessThanOrEqualTo:
                compareToResult = currentValue.CompareTo((IComparable) comparisonValue) > 0;
                break;
            case ComparisonType.EqualTo:
                compareToResult = currentValue.CompareTo((IComparable) comparisonValue) != 0;
                break;
            case ComparisonType.GreaterThan:
                compareToResult = currentValue.CompareTo((IComparable) comparisonValue) <= 0;
                break;
            case ComparisonType.GreaterThanOrEqualTo:
                compareToResult = currentValue.CompareTo((IComparable) comparisonValue) < 0;
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }

        return compareToResult ? new ValidationResult(ErrorMessage) : ValidationResult.Success;
    }
}

Dans le contexte de la réservation, un exemple serait le suivant:

public DateTime CheckInDate { get; set; }

[Comparison("CheckInDate", ComparisonType.EqualTo, ErrorMessage = "CheckOutDate must be equal to CheckInDate")]
public DateTime CheckOutDate { get; set; }
1
rschoenbach