Limitations of MVC2’s ModelValidatorProviders
January 13, 2010 – 12:08
Update 6 Feb 2010: Most of the issues that I’ve mentioned here have been addressed in MVC2 RC2. See this post for details.
One of the new features in ASP.NET MVC 2 is the ability the ability to define multiple validation providers. These are used by the DefaultModelBinder to automatically validate your model objects when they are passed to action parameters.
I wanted to try and write one of these for FluentValidation (see this post) but after trying this it has become quite clear that the ModelValidatorProvider API does have some limitations which have caused me some problems.
Problem 1: Property-level validation
To understand this problem, first we need to look at how different validation frameworks perform their work.
When using FluentValidation, the validators work against a pre-populated object. For example:
public class CustomerValidator : AbstractValidator<Customer{ public CustomerValidator() { RuleFor(x => x.Surname).NotNull(); RuleFor(x => x.CreditLimit).GreaterThanOrEqualTo(0); } } var customer = new Customer { CreditLimit = -1, Surname = null }; var validator = new CustomerValidator(); var results = validator.Validate(customer);
You cannot reliably validate individual properties by themselves because they may have dependencies on *other* properties being set. For example:
RuleFor(x => x.Surname).NotEqual(x => x.Forename);
This rule says tht the Surname and Forename properties must not be equal. In this case, you cannot validate the surname property by itself without having first set the value for the Forename property. Other validation frameworks (eg the Castle Project’s validation attributes) work in a similar way.
The MVC Framework’s DefaultModelBinder is primarily designed to work with the System.ComponentModel.DataAnnotations attributes. These work in a different way – they validate individual values, not the entire object. To support this, the DefaultModelBinder allows validating both the individual property values (in the OnPropertyValidated method) as well as validating the entire instance (in the OnModelUpdated method).
Here are the relevant methods from DefaultModelBinder:
//OnPropertyValidated - called for each property being bound. protected virtual void OnPropertyValidated(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor, object value) { ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name]; propertyMetadata.Model = value; string modelStateKey = CreateSubPropertyName(bindingContext.ModelName, propertyMetadata.PropertyName); // run validation foreach (ModelValidator validator in propertyMetadata.GetValidators(controllerContext)) { foreach (ModelValidationResult validationResult in validator.Validate(bindingContext.Model)) { bindingContext.ModelState.AddModelError(CreateSubPropertyName(modelStateKey, validationResult.MemberName), validationResult.Message); } } // only add a "value was required" error message if there were no validation errors if (bindingContext.ModelState.IsValidField(modelStateKey)) { if (value == null && !TypeHelpers.TypeAllowsNullValue(propertyDescriptor.PropertyType)) { bindingContext.ModelState.AddModelError(modelStateKey, GetValueRequiredResource(controllerContext)); } } }
//OnModelUpdated - called once the entire object has been bound. protected virtual void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext) { IDataErrorInfo errorProvider = bindingContext.Model as IDataErrorInfo; if (errorProvider != null) { string errorText = errorProvider.Error; if (!String.IsNullOrEmpty(errorText)) { bindingContext.ModelState.AddModelError(bindingContext.ModelName, errorText); } } if (!IsModelValid(bindingContext)) { return; } foreach (ModelValidator validator in bindingContext.ModelMetadata.GetValidators(controllerContext)) { foreach (ModelValidationResult validationResult in validator.Validate(null)) { bindingContext.ModelState.AddModelError(CreateSubPropertyName(bindingContext.ModelName, validationResult.MemberName), validationResult.Message); } } }
On first glance, this looks like it will work. When OnPropertyValidated calls GetValidators, I can return an empty collection of validators and do all of my work in a single validator in OnModelUpdated. Unfortunately, this does not work.
See the end of OnPropertyValidated? Here, it checks if there have been any validation errors for the property and if not then it will go ahead and run its own built-in field validation anyway for non-nullable value types – it will put a message saying “A value was required” into ModelState. And you can’t turn this off. The *only way* to stop this built-in validation is to support property-level validation in your custom ValidatorProvider. Not very useful if the underlying framework does not support this.
To work around the above issue, I ended up adding property-level validation to FluentValidation, but this is not a completely reliable solution because of the issues related to cross-property validation I mentioned above.
Problem 2: The DataAnnotationsModelValidatorProvider is greedy
The next problem that I ran into is that the DataAnnotationsModelValidatorProvider will always run required field validation, even if you’re not using the DataAnnotations attributes!
Here is an example that uses FluentValidation to validate an object where a null value is being bound to a non-nullable value type:
[Validator(typeof(FooValidator))] public class Foo { public int Id { get; set; } } public class FooValidator : AbstractValidator<Foo> { public FooValidator() { RuleFor(x => x.Id).NotEmpty(); } } [Test] public void AddsOneError() { var foo = new Foo(); ModelValidatorProviders.Providers.Add( new FluentValidationModelValidatorProvider(new AttributedValidatorFactory()) ); var meta = ModelMetadataProviders.Current.GetMetadataForProperty(() => null, typeof(Foo), "Id"); var validators = ModelValidatorProviders.Providers.GetValidators(meta, new ControllerContext()); var results = validators.SelectMany(x => x.Validate(foo)).ToList(); Assert.AreEqual(1, results.Count()); //fails - 2 instead of 1 }
This test fails because there are two error messages rather than one:
The Id field is required. 'Id' should not be empty.
…the first of these errors comes from the DataAnnotationsModelValidatorProvider even though I have NOT decorated the Id property with a [Required] attribute.
Why does this happen? The answer is in DataAnnotationsModelValidatorProvider’s GetValidators method:
if (metadata.IsRequired && !attributes.Any(a => a is RequiredAttribute)) { attributes = attributes.Concat(new[] { new RequiredAttribute() }); }
This checks to see if the property is a non-nullable type and it does not have a RequiredAttribute, then it assumes that the property should have a RequiredAttribute anyway!
You can work around this by removing the DataAnnotationsModelValidatorProvider:
ModelValidatorProviders.Providers.Clear(); ModelValidatorProviders.Providers.Add( new FluentValidationModelValidatorProvider(new AttributedValidatorFactory()) );
…and the test now passes.
Unfortunately, this is far from ideal. This means you cannot use the DataAnnotations attributes to validate one model and a custom provider for a different model. You have to choose one or the other.
One way to work around this problem is to use a composite validator provider that wraps the underlying providers:
public class CompositeValidatorProvider : ModelValidatorProvider { public static void Bootstrap(ModelValidatorProviderCollection providerCollection, params ModelValidatorProvider[] customProviders) { var validators = customProviders.ToList(); validators.AddRange(providerCollection); providerCollection.Clear(); providerCollection.Add(new CompositeValidatorProvider(validators)); } readonly IEnumerable<ModelValidatorProvider> innerInnerProviders; protected CompositeValidatorProvider(IEnumerable<ModelValidatorProvider> innerProviders) { this.innerInnerProviders = innerProviders; } public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context) { foreach(var provider in innerInnerProviders) { var validators = provider.GetValidators(metadata, context).ToList(); if(validators.Count > 0) { return validators; } } return Enumerable.Empty<ModelValidator>(); } }
This custom provider can be used to wrap the built-in providers (including the DataAnnotations provider) as well as your own custom providers. In its GetValidators method, it iterates over each of the inner providers and asks each one to create a collection of validators. It only returns validators from the first provider that returns a non-empty collection. This means that if your custom provider can validate the object then the DataAnnotations provider will not be executed.
Now, the following test passes:
[Test] public void AddsOneError() { var foo = new Foo(); var fluentProvider = new FluentValidationModelValidatorProvider(new AttributedValidatorFactory()); CompositeValidatorProvider.Bootstrap(ModelValidatorProviders.Providers, fluentProvider); var meta = ModelMetadataProviders.Current.GetMetadataForProperty(() => null, typeof(Foo), "Id"); var validators = ModelValidatorProviders.Providers.GetValidators(meta, new ControllerContext()); var results = validators.SelectMany(x => x.Validate(foo)).ToList(); Assert.AreEqual(1, results.Count()); }
This raises the question: if you have to resort to doing this, what is the point in allowing multiple validation providers to be registered in the first place?
I am really hoping that the MVC team will fix this fairly significant problem before RTM, but as MVC2 is already at the RC stage I think this will be unlikely.
4 Responses
This is insane and super frustrating. Why does the asp.net team keep building half arse solutions to everything its driving me crazy.
Why not just cull the ModelValidationProviders and make some helper classes that will turn FluentValidation rules directly into jQuery validators rules? It just seems like all the abstraction is a waste of time and effort. Sometimes I think that using ajax and server side validation is better because it is more DRY
Oh and using the DefaultModelBinder to run validation, what a nightmare! I think I will stick to using this technique http://richarddingwall.name/2009/08/19/asp-net-mvc-tdd-and-fluent-validation/
Hi Jake,
I was thinking the same thing. At this point trying to integrate with MVC2 is too much pain so I will probably not include this with FluentValidation 1.2. Integrating directly with jQuery validation is probably a better idea.
Jeremy
[...] Limitations of MVC2’s ModelValidatorProviders – Jeremy Skinner talks about some of the problems he ran into working with the ASP.NET MVC 2 ModelValidatorProvider API when trying to write a provider for the FluentValidation library. [...]