Search code examples
asp.net-coremodel-bindingasp.net-core-3.1customvalidatormodel-validation

How to create a custom validator in ASP.NET Core that fires for invalid input too?


I have created a custom validator for a DateTime field in ASP.NET Core 3.1 as shown below:

[CustomDate]
public DateTime DOB { get; set; }

public class CustomDate : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        //… some logic
    }
}

However, my problem is that this custom validator fires only when I put a date value in the text box control. It does not fire for invalid inputs like e.g. when I put a string 'aaa' in the text box.

My question is how to make this custom validator fire even for invalid inputs like 'string', etc.

The reason is I want to make this custom validator replace [Required], [ReqularExpression], etc. A sort of 'One ring (validator) to rule them all'. How can I achieve that?


Solution

  • TL;DR: When you submit a value that can't be converted to DateTime, model binding fails. Since there is already a validation error associated with the property, subsequent validation—including your CustomDate validator—doesn't fire. Your property is still getting validated, however: If you enter a value of aaa, ModelState.IsValid will return false.


    The code you had originally posted should be working fine—but I suspect it's not working the way you're expecting it to. Most notably, your confusion likely stems from the following statement:

    "…this custom validator fires only when I put a date value in the text box control."

    That is also true! Let me walk through the process.

    Original Code

    To help illustrate this, I hope you don't mind me resurrecting your original code sample, as it's useful to have concrete reference to work off of.

    [CustomDate]
    public DateTime DOB { get; set; }
    
    public class CustomDate : Attribute, IModelValidator
    {
        public IEnumerable<ModelValidationResult> Validate(ModelValidationContext context)
        {
            if (Convert.ToDateTime(context.Model) > DateTime.Now)
                return new List<ModelValidationResult> {
                    new ModelValidationResult("", "Invalid - future date")
                };
            else if (Convert.ToDateTime(context.Model) < new DateTime(1970, 1, 1))
                return new List<ModelValidationResult> {
                    new ModelValidationResult("", "Invalid - date is less than 1970 year")
                };
            else
                return Enumerable.Empty<ModelValidationResult>();
        }
    }
    

    Validation Process

    Before I walk through the process, there are four underlying considerations that are important to be aware of here:

    1. Model binding occurs before model validation.
    2. Model binding will raise its own validation errors if binding fails.
    3. Validation attributes are only evaluated on properties that remain IsValid.
    4. The ModelValidationContext.Model property is typed to the validated property—so, in this case, a DateTime value.

    Use Case #1: Invalid Value

    Given these considerations, here's what's happening when you submit a value of e.g. aaa in the field mapped to your validated DOB property:

    1. The model binder attempts to bind a value of aaa to a DateTime property.
    2. The model binder fails, adding a ModelError to your ModelStateDictionary.
    3. Your CustomDate validator never fires because the field has already failed validation.

    Use Case #2: Missing Value

    It's instructive to look at another test case. Instead of putting in aaa, just don't put a value in at all. In this case, the process looks a bit different:

    1. The model binder doesn't find a value for your DateTime property, so no binding occurs.
    2. Your model's property initializes to DateTime's default value of 0001-01-01 00:00:00.
    3. Your CustomDate validator fires, adding a ModelError because "Invalid - date is less than 1970 year".

    Analysis

    As you can see above, it is true that your CustomDate validator isn't firing when a bogus date is submitted. But that doesn't mean that validation isn't occurring. Instead, validation has already happened and failed. If you enter a valid date—or don't enter a date at all—then a model binding error won't occur, and your CustomDate validator will be executed as expected.

    Revisiting Your Question

    "How to make this custom validator to fire even for invalid inputs like 'string' etc."

    Ultimately, I haven't answered that question. But I think my answer will explain why that's happening, and why your input is still getting validated despite that. Keep in mind that even if your CustomDate validator did fire, it would act the same as if you hadn't submitted a value at all, since the context.Model value would have defaulted to 0001-01-01 00:00:00. The main difference is that you're not getting the same error message, since the error is coming from a different source.

    Forcing Validation

    I don't recommend this, but if you really wanted your CustomDate validator to fire, you could apply it to a string property instead. In that case, model binding wouldn't fail and your CustomDate validator would still get called. If you pursue this, you'll need to put in additional validation to ensure that the date is in the correct format (e.g., by preempting or handling InvalidFormatExceptions). But, of course, your date would be stored as a string, which likely isn't what you want.

    Code Suggestions

    This is a bit outside the scope of your original question, but while I'm here I'd recommend the following:

    1. You won't need to do a Convert.ToDateTime() in your validator; your context.Model field is already a DateTime. You just need to cast it back to a DateTime object (e.g., (DateTime)context.Model) so your code knows that.
    2. At minimum, you should consider using <input type="date" /> (reference) which, on most browsers, will restrict input to a correct date while also providing a basic date picker.
    3. Alternatively, there are a number of more sophisticated date/time controls written in JavaScript that you might consider implementing if you require more control over the presentation and client-side validation.