30

How is the best way to validate a model in MVC.Net where I want to accept a minimum/maximum.

Not individual min/max values for a field. But separate fields for a user to specify a minimum/maximum.

public class FinanceModel{
   public int MinimumCost {get;set;}
   public int MaximumCost {get;set;}
}

So I need to ensure that MinimumCost is always less than Maximum cost.

Dani
  • 1,825
  • 2
  • 15
  • 29
Ben Ford
  • 1,354
  • 2
  • 14
  • 35

5 Answers5

29

There is a NuGet package called Foolproof which provides these annotations for you. That said - writing a custom attribute is both pretty easy and good practice.

Using Foolproof would look like:

public class FinanceModel{
   public int MinimumCost {get;set;}

   [GreaterThan("MinimumCost")]
   public int MaximumCost {get;set;}
}
Matthew
  • 9,851
  • 4
  • 46
  • 77
  • 1
    Accepted the custom validator just as a learning tool. Thanks for the reference to Foolproof. Will keep it handy just incase anyway. – Ben Ford Sep 02 '13 at 21:01
  • Foolproof does not seem to accept custom error messages. – Manuel Reis Aug 03 '15 at 11:14
  • 2
    Custom error messages are specified as such [GreaterThan("MinimumCost"), ErrorMessage = "Must be more than Minimum Cost"] – Chris Apr 27 '16 at 15:57
  • 5
    A little correction here: The error message should be like this, [GreaterThan("MinimumCost", ErrorMessage = "Must be more than Minimum Cost")] – César León Jul 25 '16 at 19:03
27

You can use a custom validation attribute here is my example with dates. But you can use it with ints too.

First, here is the model :

public DateTime Beggining { get; set; }

[IsDateAfterAttribute("Beggining", true, ErrorMessageResourceType = typeof(LocalizationHelper), ErrorMessageResourceName = "PeriodErrorMessage")]
public DateTime End { get; set; }

And here is the attribute itself :

public sealed class IsDateAfterAttribute : ValidationAttribute, IClientValidatable
{
    private readonly string testedPropertyName;
    private readonly bool allowEqualDates;

    public IsDateAfterAttribute(string testedPropertyName, bool allowEqualDates = false)
    {
        this.testedPropertyName = testedPropertyName;
        this.allowEqualDates = allowEqualDates;
    }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var propertyTestedInfo = validationContext.ObjectType.GetProperty(this.testedPropertyName);
        if (propertyTestedInfo == null)
        {
            return new ValidationResult(string.Format("unknown property {0}", this.testedPropertyName));
        }

        var propertyTestedValue = propertyTestedInfo.GetValue(validationContext.ObjectInstance, null);

        if (value == null || !(value is DateTime))
        {
            return ValidationResult.Success;
        }

        if (propertyTestedValue == null || !(propertyTestedValue is DateTime))
        {
            return ValidationResult.Success;
        }

        // Compare values
        if ((DateTime)value >= (DateTime)propertyTestedValue)
        {
            if (this.allowEqualDates && value == propertyTestedValue)
            {
                return ValidationResult.Success;
            }
            else if ((DateTime)value > (DateTime)propertyTestedValue)
            {
                return ValidationResult.Success;
            }
        }

        return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
    }

    public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
    {
        var rule = new ModelClientValidationRule
        {
            ErrorMessage = this.ErrorMessageString,
            ValidationType = "isdateafter"
        };
        rule.ValidationParameters["propertytested"] = this.testedPropertyName;
        rule.ValidationParameters["allowequaldates"] = this.allowEqualDates;
        yield return rule;
    }
Vencovsky
  • 28,550
  • 17
  • 109
  • 176
Pavel Ronin
  • 576
  • 4
  • 11
  • 9
    You need to complete this example with the client-side validation: `jQuery.validator.addMethod('isdateafter', function (value, element, params) { if (!/Invalid|NaN/.test(new Date(value))) { return new Date(value) > new Date(); } return isNaN(value) && isNaN($(params).val()) || (parseFloat(value) > parseFloat($(params).val())); }, ''); jQuery.validator.unobtrusive.adapters.add('isdateafter', {}, function (options) { options.rules['isdateafter'] = true; options.messages['isdateafter'] = options.message; });` – LoBo Oct 16 '14 at 11:22
  • There seems to be a bug due to the line `if (this.allowEqualDates && value == propertyTestedValue)`. This works: `if (this.allowEqualDates && value.Equals(propertyTestedValue))` or even this `if (this.allowEqualDates && (DateTime)value == (DateTime)propertyTestedValue)`. – publicgk Jun 09 '16 at 17:35
6

For client side validation using the allowEqualDates and propertyTested parameters (complement to Boranas answer above but too long for comment):

// definition for the isdateafter validation rule
if ($.validator && $.validator.unobtrusive) {
    $.validator.addMethod('isdateafter', function (value, element, params) {
        value = Date.parse(value);
        var otherDate = Date.parse($(params.compareTo).val());
        if (isNaN(value) || isNaN(otherDate))
            return true;
        return value > otherDate || (value == otherDate && params.allowEqualDates);
    });
    $.validator.unobtrusive.adapters.add('isdateafter', ['propertytested', 'allowequaldates'], function (options) {
        options.rules['isdateafter'] = {
            'allowEqualDates': options.params['allowequaldates'],
            'compareTo': '#' + options.params['propertytested']
        };
        options.messages['isdateafter'] = options.message;
    });
}

More information: unobtrusive validation, jquery validation

Nicolas Galler
  • 1,309
  • 10
  • 10
1

In VB for integers:

MODEL

<UtilController.IsIntegerGreatherOrEqualThan("PropertyNameNumberBegins", "PeriodErrorMessage")>
        Public Property PropertyNameNumberEnds As Nullable(Of Integer)

VALIDATION

Public Class IsIntegerGreatherOrEqualThan
        Inherits ValidationAttribute

        Private otherPropertyName As String
        Private errorMessage As String

        Public Sub New(ByVal otherPropertyName As String, ByVal errorMessage As String)
            Me.otherPropertyName = otherPropertyName
            Me.errorMessage = errorMessage
        End Sub

        Protected Overrides Function IsValid(thisPropertyValue As Object, validationContext As ValidationContext) As ValidationResult

            Dim otherPropertyTestedInfo = validationContext.ObjectType.GetProperty(Me.otherPropertyName)

            If (otherPropertyTestedInfo Is Nothing) Then
                Return New ValidationResult(String.Format("unknown property {0}", Me.otherPropertyName))
            End If

            Dim otherPropertyTestedValue = otherPropertyTestedInfo.GetValue(validationContext.ObjectInstance, Nothing)

            If (thisPropertyValue Is Nothing) Then
                Return ValidationResult.Success
            End If

            ''  Compare values
            If (CType(thisPropertyValue, Integer) >= CType(otherPropertyTestedValue, Integer)) Then
                Return ValidationResult.Success
            End If

            ''  Wrong
            Return New ValidationResult(errorMessage)
        End Function
    End Class
Dani
  • 1,825
  • 2
  • 15
  • 29
  • I removed "FormatErrorMessage" from the code as it was adding " 'The Field' + {errorMessage} + 'is invalid' ". I was doing a date check, so I replaced Integer with Date. Worked great and saved me time. Thank you. – PHBeagle Oct 20 '17 at 15:16
  • So errorMessage was displaying wrong message?, when I used it I didn't pay attention for it. – Dani Oct 21 '17 at 16:21
  • Not really wrong, just extra wording. By using "Return new ValidationResult(errorMessage)" then it was good. – PHBeagle Oct 21 '17 at 20:18
-8

Why you are not used Range Validator. Syntax:

    [Range(typeof(int), "0", "100", ErrorMessage = "{0} can only be between {1} and {2}")]
    public int Percentage { get; set; }
Khalid
  • 194
  • 4
  • 2
    If you look at my original question or the existing answers, you'll see the situation I'm trying to validate is where a user can select upper/lower bounds. Not when they have to enter a value between existing high/low values. – Ben Ford Sep 03 '13 at 13:10