112

I have a Model with 4 properties which are of type string. I know you can validate the length of a single property by using the StringLength annotation. However I want to validate the length of the 4 properties combined.

What is the MVC way to do this with data annotation?

I'm asking this because I'm new to MVC and want to do it the correct way before making my own solution.

Andrei
  • 42,814
  • 35
  • 154
  • 218
Danny van der Kraan
  • 5,344
  • 6
  • 31
  • 41
  • 2
    Have you looked at Fluent Validation? It handles complex scenarios much better than Data Annotations – levelnis Apr 19 '13 at 08:35
  • Take a look at highly recommended solutions.... http://www.dotnetcurry.com/ShowArticle.aspx?ID=776 – Niks Apr 19 '13 at 08:57
  • Thanks for answering. I'll check out Fluent Validation, never heard of it. And Niks, Darin basically wrote out what the article at the link you posted explained. So, thank you... Awesome stuff! – Danny van der Kraan Apr 19 '13 at 09:46

6 Answers6

181

You could write a custom validation attribute:

public class CombinedMinLengthAttribute: ValidationAttribute
{
    public CombinedMinLengthAttribute(int minLength, params string[] propertyNames)
    {
        this.PropertyNames = propertyNames;
        this.MinLength = minLength;
    }

    public string[] PropertyNames { get; private set; }
    public int MinLength { get; private set; }

    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        var properties = this.PropertyNames.Select(validationContext.ObjectType.GetProperty);
        var values = properties.Select(p => p.GetValue(validationContext.ObjectInstance, null)).OfType<string>();
        var totalLength = values.Sum(x => x.Length) + Convert.ToString(value).Length;
        if (totalLength < this.MinLength)
        {
            return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
        }
        return null;
    }
}

and then you might have a view model and decorate one of its properties with it:

public class MyViewModel
{
    [CombinedMinLength(20, "Bar", "Baz", ErrorMessage = "The combined minimum length of the Foo, Bar and Baz properties should be longer than 20")]
    public string Foo { get; set; }
    public string Bar { get; set; }
    public string Baz { get; set; }
}
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 4
    Thanks for answering, I accepted your answer. Feel a bit embarrassed actually. You wrote out the entire solution! Hehe. Only had to change the IsValid function to check for max length. So is this the accepted MVC solution for these types of problems? – Danny van der Kraan Apr 19 '13 at 09:43
  • 7
    @DannyvanderKraan, yes, this is the accepted way. Of course this sucks so badly that I never use it and use FluentValidation.NET instead to perform validation. – Darin Dimitrov Apr 19 '13 at 10:05
  • Lol. Okok, I don't know what that is. Why is it better? – Danny van der Kraan Apr 19 '13 at 11:06
  • 11
    Here: http://fluentvalidation.codeplex.com/. You could have just written a simple validator for the view model that might have looked like this (a single line of code): `this.RuleFor(x => x.Foo).Must((x, foo) => x.Foo.Length + x.Bar.Length + x.Baz.Length < 20).WithMessage("The combined minimum length of the Foo, Bar and Baz properties should be longer than 20");`. Now look at the code in my answer that you need to write with the data annotations and tell me which one you prefer. The declarative validation model is very poor compared to an imperative model. – Darin Dimitrov Apr 19 '13 at 13:14
  • Agreed. Thank you. I'll look into Fluent Validation. – Danny van der Kraan Apr 23 '13 at 09:05
  • 1
    This is a bit late, but does anyone know if there is a different setting that you have to "turn on" in order to allow custom data annotations? I know about adding a namespace for unobstrusive js on the web.config file, but anywhere else? – Jose Oct 15 '13 at 21:26
  • 1
    I've been looking for *this* all morning! I've mplemented it, and unfortunately when `IsValid` is called the `validationContext` is null. Any idea what I did wrong? :-( – Grimm The Opiner Nov 06 '13 at 12:50
  • does this kind of validation work at client side `CombinedMinLength()` required js will be injected for client side validation ? – Mou Jan 15 '16 at 08:43
  • 1
    @Mou, for this to work you will need to write a corresponding client side extension to the standard validators because this is custom server side logic. – Darin Dimitrov Jan 15 '16 at 13:52
  • @DarinDimitrov thanks for this answer, But now if I hit a controller actoin having MyViewModel model; with ajax am seeing it is working in server side. But if I want to show the error message in client side when click on submit button what I have to do? please inform me. – Muhammad Ashikuzzaman Dec 14 '17 at 08:31
102

Self validated model

Your model should implement an interface IValidatableObject. Put your validation code in Validate method:

public class MyModel : IValidatableObject
{
    public string Title { get; set; }
    public string Description { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Title == null)
            yield return new ValidationResult("*", new [] { nameof(Title) });

        if (Description == null)
            yield return new ValidationResult("*", new [] { nameof(Description) });
    }
}

Please notice: this is a server-side validation. It doesn't work on client-side. You validation will be performed only after form submission.

Andrei
  • 42,814
  • 35
  • 154
  • 218
  • Thanks for answering Andrei. While your solution would work too I choose Darin's because it's more reusable. – Danny van der Kraan Apr 19 '13 at 09:44
  • 6
    yield return new ValidationResult("The title is mandatory.", "Title"); would add the property name, useful in grouping validation errors for display if necessary. – Mike Kingscott Sep 26 '13 at 16:11
  • 5
    Note that this validation method is only called after all validation attributes have passed validation. – Pedro Jul 24 '14 at 21:01
  • 3
    This worked well for me since my validation was very specific. Adding a custom attribute would have been overkill for me since the validation was not going to be re-used. – Steve S Jun 11 '16 at 12:02
  • This is what I am looking for. Thank you! – Amol Jadhav Jun 04 '19 at 06:07
28

ExpressiveAnnotations gives you such a possibility:

[Required]
[AssertThat("Length(FieldA) + Length(FieldB) + Length(FieldC) + Length(FieldD) > 50")]
public string FieldA { get; set; }
jwaliszko
  • 16,942
  • 22
  • 92
  • 158
11

To improve Darin's answer, it can be bit shorter:

public class UniqueFileName : ValidationAttribute
{
    private readonly NewsService _newsService = new NewsService();

    public override bool IsValid(object value)
    {
        if (value == null) { return false; }

        var file = (HttpPostedFile) value;

        return _newsService.IsFileNameUnique(file.FileName);
    }
}

Model:

[UniqueFileName(ErrorMessage = "This file name is not unique.")]

Do note that an error message is required, otherwise the error will be empty.

Jamie
  • 3,031
  • 5
  • 36
  • 59
8

Background:

Model validations are required for ensuring that the received data we receive is valid and correct so that we can do the further processing with this data. We can validate a model in an action method. The built-in validation attributes are Compare, Range, RegularExpression, Required, StringLength. However we may have scenarios wherein we required validation attributes other than the built-in ones.

Custom Validation Attributes

public class EmployeeModel 
{
    [Required]
    [UniqueEmailAddress]
    public string EmailAddress {get;set;}
    public string FirstName {get;set;}
    public string LastName {get;set;}
    public int OrganizationId {get;set;}
}

To create a custom validation attribute, you will have to derive this class from ValidationAttribute.

public class UniqueEmailAddress : ValidationAttribute
{
    private IEmployeeRepository _employeeRepository;
    [Inject]
    public IEmployeeRepository EmployeeRepository
    {
        get { return _employeeRepository; }
        set
        {
            _employeeRepository = value;
        }
    }
    protected override ValidationResult IsValid(object value,
                        ValidationContext validationContext)
    {
        var model = (EmployeeModel)validationContext.ObjectInstance;
        if(model.Field1 == null){
            return new ValidationResult("Field1 is null");
        }
        if(model.Field2 == null){
            return new ValidationResult("Field2 is null");
        }
        if(model.Field3 == null){
            return new ValidationResult("Field3 is null");
        }
        return ValidationResult.Success;
    }
}

Hope this helps. Cheers !

References

KLIM8D
  • 582
  • 1
  • 8
  • 25
Yasser Shaikh
  • 46,934
  • 46
  • 204
  • 281
1

A bit late to answer, but for who is searching. You can easily do this by using an extra property with the data annotation:

public string foo { get; set; }
public string bar { get; set; }

[MinLength(20, ErrorMessage = "too short")]
public string foobar 
{ 
    get
    {
        return foo + bar;
    }
}

That's all that is too it really. If you really want to display in a specific place the validation error as well, you can add this in your view:

@Html.ValidationMessage("foobar", "your combined text is too short")

doing this in the view can come in handy if you want to do localization.

Hope this helps!

Leo Muller
  • 1,421
  • 1
  • 12
  • 20