Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET MVC Model Binding and Validation Order

I have a model (simplified) as follows:

public UserModel {
    ...
    public USState State {get; set; }
    public string StateString {get; set; }
    public Country Country {get; set; }
    ...
}

The validation rules I need are:

  1. If Country is USA then State is required.
  2. If Country is not USA then StateString is required.

I've created a custom validation attribute RequiredIfAttribute. This works fine so I'm not going to fill the question with it's implementation. It has three required members:

  1. CompareField - This is the field it will use to check whether or not validation is required.
  2. CompareValue - This is the value it will compare to to decide whether or not validation is required.
  3. CompareType - This is how it will compare the value to decide whether or not validation is required.

So with this, I update my model as such:

public UserModel {
    ...
    [RequiredIf("Country", Country.USA, EqualityType.Equals)]    
    public USState State {get; set; }
    [RequiredIf("Country", Country.USA, EqualityType.NotEquals)] 
    public string StateString {get; set; }
    [Required]                                                   
    public Country Country {get; set; }
    ...
}

I should note here that my RequiredIfAttribute also has client side validation. This works perfectly.

Now on to the problem...

I'm posting the following values:

State = AL
StateString = null
Country = USA

This meets my validation rules and should be valid. Here's the but. ModelState is telling me it is not valid. Apparently by StateString field is required. That's not what I specified. Why aren't my validation rules applied as expected?

(If you know what's wrong at this point, then don't feel obliged to read the rest of the question)

So here's what's happening. The RequiredIfAttribute is being triggered three times. But wait, I'm only using it twice. It is is being triggered like so:

  1. Trigger on StateString (this comes back invalid)
  2. Trigger on State (this comes back valid)
  3. Trigger on StateString (this comes back valid)

This is pretty odd. It's validating StateString twice, first time it passes, second time it fails. The plot thickens...

I looked into this further to find that the first time it tries to validate StateString, Country is not set. The second time it tries to validate StateString, Country is set. Looking closer, it seem that the first attempt to validate StateString has occurred before my model has been fully bound. All properties (not listed in the sample model) that are below StateString (in code) are not bound. The second attempt to validate StateString, all properties are bound.

I have solved the problem, but I'm not confident in it because I simply do not trust it. To get my validation to work as expected, I rearranged the model as such (attributes removed for brevity):

public UserModel {
    ...
    public Country Country {get; set; }
    public USState State {get; set; }
    public string StateString {get; set; }
    ...
}

The RequiredIfAttribute still triggers three times as above but ModelState tells me that the posted data (as above) is now valid, like magic!

What I'm seeing is this (my assumptions):

1. Start binding (property by property, top to bottom in code (risky))
2. Arrive at `StateString` and decide to try and validate
3. Finish binding
4. Validate all properties

I really have two questions:
1. Why is this behaviour exhibited?
2. How can I stop this behaviour?

like image 587
Paul Fleming Avatar asked Nov 12 '22 17:11

Paul Fleming


1 Answers

There are a significant number of intricacies in the model binding process. Complex models will be fully re-validated.

I suggest that for a better understanding of the process you take a dive into the source code to appreciate what is really happening.

http://aspnetwebstack.codeplex.com/SourceControl/latest#src/System.Web.Http/ModelBinding/Binders/MutableObjectModelBinder.cs

There is a post-processing phase:

// post-processing, e.g. property setters and hooking up validation
ProcessDto(actionContext, bindingContext, dto);
bindingContext.ValidationNode.ValidateAllProperties = true; // complex models require full validation

There is a pre-processing phase:

// 'Required' validators need to run first so that we can provide useful error messages if
// the property setters throw, e.g. if we're setting entity keys to null. See comments in
// DefaultModelBinder.SetProperty() for more information.

There doesn't seem to be a whole lot of ways to influence this, aside from implementing your own model binder.

like image 88
Matt Esch Avatar answered Nov 15 '22 11:11

Matt Esch