1

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?

5
  • 1
    Out of curiosity, why are you using a IModelValidator here, as opposed to a ValidationAttribute? The latter tend to be a bit more straightforward, and should still satisfy your requirements. Commented Apr 15, 2020 at 10:33
  • @JeremyCaney because I don't want to apply lots of validation attribute on the field like required, reqularexpression, etc. I want all my work to be done by my custom attribute itself. Also I want to ask how to capture invalid inputs for the field eg when I enter string value on the field then how to capture this. Commented Apr 15, 2020 at 10:39
  • 1
    Well, I meant a custom ValidationAttribute, which would allow you to customize the logic. As far as your IModelValidator is concerned, I’d need to verify a few points before answering. But I thought that the Model property would return a nullable instance of whatever the model type was—so DateTime? in this case. If so, the value some wouldn’t get bound, and the Model will return null. I’m not sure what the behavior of Convert.DateTime() would be for a null value, but I’d have thought it would throw an InvalidFormatException, or possibly default to DateTime.MinValue. Commented Apr 15, 2020 at 10:44
  • Even if I use derive my custom class with ValidationAttribute then also the custom validator does not fire for invalid inputs. Commented Apr 15, 2020 at 11:05
  • 1
    I’ve provided a (lengthy) explanation of what’s happening below, and why it might not be a concern. In addition, however, I wanted to highlight an un(der)documented feature which allows you to create composite validators using the ValidationProviderAttribute. This doesn’t answer your immediate question, but it does address your objective of grouping multiple ValidationAttributes together. Commented Apr 16, 2020 at 6:14

1 Answer 1

3

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.
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.