Aspnetcore: IValidatableObject.Validate Only Called If Attribute-Defined Validation Rules Pass?

Created on 11 Jan 2017  ·  3Comments  ·  Source: dotnet/aspnetcore

Hello,

I have an object with a [Required]-decorated property that implements IValidatableObject. During model binding, if the required property is null, the object's Validate() method is never called. Is Validate() only called if attribute-configured validation passes?

In my case, if a user submits a form with validation errors, they first see a message about the missing field. Then, once that error has been resolved and the form resubmitted, they are presented with an additional list of errors—this time, produced by Validate(). Is there a way to trigger all validation to occur at the same time so that the user is presented with a list of _all_ errors needing their attention when they first submit the form?

Thanks,
Ben

Versions: ASP.Net Core 1.1.0/.Net 4.6.1

Example Code

public class Import : IValidatableObject
{
  [Required]
  public IFormFile File { get; set; }

  public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { /* ... */ }
  //...
}

_From the controller:_

[HttpPost, ActionName("Index")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> IndexPost()
{
  var model = new ViewModels.Import();

  var bindingResult = await TryUpdateModelAsync(model, "", i => i.StartDate, i => i.EndDate, i => i.ImportTypeId, i => i.File);
  if (!bindingResult || ModelState.ValidationState != ModelValidationState.Valid) {
    return View(model);
  }

  // ...
}

If the user fails to populate the required field, they'll see an error message about it but they won't see the other error messages that Validate() might add.

All 3 comments

This issue is being closed because it has not been updated in 3 months.

We apologize if this causes any inconvenience. We ask that if you are still encountering this issue, please log a new issue with updated information and we will investigate.

I am experiencing this behavior as well in Asp.Net Core 2.0, with a referenced library containing an IValidatableObject targeting .Net Standard 2.0.
It is not until all [Required] validations are satisfied that the custom validations in Validate() are executed. In other words, I have to post an object to my controller with all required fields before anything else will be reported in modelstate.
The modelstate seems to care about required first, then everything else. I haven't tried other standard validation attributes like [StringLength()].

This is really annoying bug. Why close it?

Its hard to architect things and tell people dataannotations work then you find IValidatableObject.Validate runs as per above. It's an issue and this SHOULD be reopened.

Meanwhile I have to deal with programmers who think this is good programming:
public static List ValidateUpdateDates(ICommand command)
{
var newErrors = new List();

        var validator = new DataAnnotationsValidator();
        var results = new List<ValidationResult>();
        var isValid = validator.TryValidate(command, out results);

        if (!(command is UpdatePromotionCommand cpc))
            return newErrors;

        newErrors.AddRange(ValidateDatesAreValidFormat(cpc));

        if (
          !string.IsNullOrEmpty(cpc.PurchasePeriodStart)
          && !string.IsNullOrEmpty(cpc.PurchasePeriodEnd)
          && DateTime.TryParse(cpc.PurchasePeriodStart, out DateTime purchasePeriodStartDate)
          && DateTime.TryParse(cpc.PurchasePeriodEnd, out DateTime purchasePeriodEndDate)
          && purchasePeriodStartDate > purchasePeriodEndDate
        )
        {
            newErrors.Add(new ECError("Purchase Period End cannot be before Purchase Period Start."));
        }

        if (
          !string.IsNullOrEmpty(cpc.ClaimPeriodStart)
          && !string.IsNullOrEmpty(cpc.ClaimPeriodEnd)
          && DateTime.TryParse(cpc.ClaimPeriodStart, out DateTime claimPeriodStartDate)
          && DateTime.TryParse(cpc.ClaimPeriodEnd, out DateTime claimPeriodEndDate)
          && claimPeriodStartDate > claimPeriodEndDate
        )
        {
            newErrors.Add(new ECError("Claim Period End cannot be before Claim Period Start."));
        }
Was this page helpful?
0 / 5 - 0 ratings