ChatGPT解决这个技术问题 Extra ChatGPT

ASP.NET MVC: Custom Validation by DataAnnotation

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.

Have you looked at Fluent Validation? It handles complex scenarios much better than Data Annotations
Take a look at highly recommended solutions.... dotnetcurry.com/ShowArticle.aspx?ID=776
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!

D
Darin Dimitrov

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; }
}

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?
@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.
Here: 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.
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?
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? :-(
A
Andrei

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.


Thanks for answering Andrei. While your solution would work too I choose Darin's because it's more reusable.
yield return new ValidationResult("The title is mandatory.", "Title"); would add the property name, useful in grouping validation errors for display if necessary.
Note that this validation method is only called after all validation attributes have passed validation.
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.
This is what I am looking for. Thank you!
j
jwaliszko

ExpressiveAnnotations gives you such a possibility:

[Required]
[AssertThat("Length(FieldA) + Length(FieldB) + Length(FieldC) + Length(FieldD) > 50")]
public string FieldA { get; set; }

This is brilliant! my prayers got answered :)
Just found this answer and it's just saved loads of time. ExpressiveAnnotations are brilliant!
J
Jamie

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.


K
KLIM8D

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

Code Project - Custom Validation Attribute in ASP.NET MVC3

Haacked - ASP.NET MVC 2 Custom Validation


L
Leo Muller

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!