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:
Country
is USA then State
is required.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:
CompareField
- This is the field it will use to check whether or not validation is required. CompareValue
- This is the value it will compare to to decide whether or not validation is required.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:
StateString
(this comes back invalid)State
(this comes back valid)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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With