0

In ASP.NET MVC, I have a model class implementing IValidatableObject and have a property int? Day {get; set;}.

If the user tries to submit a string for this, ModelState is correctly marked as invalid, but the rest of my validation inside the Validate method doesn't run, so the user only sees this one error instead of everything.

I need all errors to be made available at once (it's a really frustrating user experience to get given one error at a time). So I was hoping to either be able to override this behaviour and set Day to be null or hoping that ASP.NET MVC has some built in attribute or something to do this for me (my own validation would then pick up Day as being null and ask the user to correct this).

My ViewModel:

public class FormViewModel : IValidatableObject
{
  public int? Day { get; set; }

  public IEnumerable<string> ErrorMessageOrdering { get; } = new List<string>()
  {
    nameof(Date),
  };

  public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
  {
    // This method doesn't get run when the user inputs "s" for Day. This is NOT the desired behaviour as other validations in this method wont run
    if (!Day.HasValue) 
    {
       yield return new ValidationResult("Date must include a day", new[] {nameof(Day)});
    }
    // ... Validate other fields
  }

}

My Controller:

[ServiceFilter(typeof(ConfigSettingsAttribute))]
public class HomeController : Controller
  [Route("/form")]
  [HttpPost]
  public async Task<IActionResult> FormAsync(FormViewModel model)
  {
    if (!ModelState.IsValid)
    {
      return View(model)
    }
  }
}

My View:

@model FormViewModel

@if (!ViewData.ModelState.IsValid)
{
  <partial name="_ErrorSummary" model=@(Model.ErrorMessageOrdering) />
}

<form method="post">
  <input asp-for="Day" type="text" pattern="[0-9]*" inputmode="numeric" maxlength="2">
  @* ... Other fields @*
</form>

When the user enters "s" for the Day input (the numeric html values seem to have no effect on restricting user input btw) the Validate method on the view model is skipped and the ModelState is populated with Day being an invalid field with the message The value 's' is invalid for Day..

When the user doesn't enter anything in the Day input, the Validate method does run and the ModelState is populated with Day being invalid with the message being what I set it to in Validate.

The problem here is that because Validate isn't called on the view model when the user enters a string (presumably because it failed to cast/bind values), any further errors don't get added to ModelState. I would prefer it if the framework set Day to be null if it can't bind instead of short circuiting. If I could override this behaviour that would also be great.

Note that my problem seems to be that Validate is not called if there are bind/casting errors. As opposed to the Validate method not being written correctly. When binding errors dont occur, I do get a list of validation failures as expected

1 Answer 1

1

Well - it's because YOU wrote it that way! Look at your code:

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
    // This method doesn't get run when the user inputs "s" for Day. This is NOT the desired behaviour as other validations in this method wont run
    if (!Day.HasValue) 
    {
        return yield new ValidationResult("Date must include a day", new[] {nameof(Day)});
    }
    // ... Validate other fields
}

When your first check returns false - you return the error (using return yield new ValidationResult(....)) - so the method execution stops here.

If you want to avoid this - you can build a List<ValidationResult> internally, and add any error detected to that list - and then return it once at the end - with all the detected errors in it.

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
    List<ValidationResult> results = new List<ValidationResult>();

    // This method doesn't get run when the user inputs "s" for Day. This is NOT the desired behaviour as other validations in this method wont run
    if (!Day.HasValue) 
    {
        results.Add(new ValidationResult("Date must include a day", new[] {nameof(Day)}));
    }

    // ... Validate other fields
    if (someOtherCondition)
    {
        results.Add(new ValidationResult("Some other condition was true - returning another error", new[] {nameof(Day)}));
    }

    // and more - as needed
    ..

    // in the end - return the list
    return results;
}

Of course, this approach only works if an error you detected earlier doesn't make it impossible to continue on - if e.g. some of your data is null, you might not be able to do any further checking.

But basically - instead of returning back for each individual error, this allows you to validate numerous aspects and report back once, with a list of validation results - now your UI needs to be able to handle getting back multiple validation results, and displaying them, too!

Sign up to request clarification or add additional context in comments.

2 Comments

The Validate method doesnt even run when the input cannot be cast to an int. I can breakpoint it / console log it whatever it doesnt hit the method. Also the yield return thing is weird, I'm not sure why the team chose to do it this way, but it seems to have the same effect as making, adding and returning a list as you suggested. For sanity sake I rewrote it to get rid of yield entirely and the same behaviour is exhibited
@JackReeve: yes, the Validate method only gets called when all the declarative data annotations have been satisfied - so if your input violates one of those (e.g. inputting an alphanumeric character into a "day" field which should be converted to an INT value), then Validate will never be called. That's just the way MS designed and built this - cannot change it, as far as I know

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.