Excluded properties via BindAttribute and ModelValidator in ASP.NET MVC 3
Good evening,
I'm having trouble with model binding and validation but I don't know whether it's a normal behavior : the problem is that, is spite of BindAttribute (with his property Excluded correctly filled), the excluded properties are validated but not removed in the ModelState dictionary... so I get errors in my views... concerning an excluded property! Doh!
So, is there a way to get the "non-excluded-properties" list, directly in my model validator so I can tell my validation service not to validate excluded properties?
Here are the validator provider and the validator itself (just an internal wrapper around the great FluentValidator)
internal sealed class ValidationProvider : ModelValidatorProvider {
private readonly IValidationFactory _validationFactory;
public ValidationProvider(IValidationFactory validationFactory) {
_validationFactory = validationFactory;
}
public 开发者_JAVA技巧override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context) {
if (metadata.ModelType != null) {
IValidationService validationService;
if (_validationFactory.TryCreateServiceFor(metadata.ModelType, out validationService)) {
yield return new ValidationAdapter(metadata, context, validationService);
}
}
}
private sealed class ValidationAdapter : ModelValidator {
private readonly IValidationService _validationService;
internal ValidationAdapter(ModelMetadata metadata,
ControllerContext controllerContext,
IValidationService validationService)
: base(metadata, controllerContext) {
_validationService = validationService;
}
public override IEnumerable<ModelValidationResult> Validate(object container) {
if (Metadata.Model != null) {
IEnumerable<ValidationFault> validationFaults;
if (!_validationService.TryValidate(Metadata.Model, out validationFaults)) {
return validationFaults.Select(fault => new ModelValidationResult {
MemberName = fault.PropertyInfo.Name,
Message = fault.FaultedRule.Message
});
}
}
return Enumerable.Empty<ModelValidationResult>();
}
}
}
And here is the action :
public class MyModel {
public string Test { get; set; }
public string Name { get; set; }
}
[HttpPost]
public ActionResult Test([Bind(Exclude = "Test")] MyModel model) {
if (ModelState.IsValid) {
...
}
return View();
}
Here, I get errors for excluded "Test" property... Huh!
Thanks!
This is the expected behavior. This change (always doing whole-model validation) was made late in the MVC 2 ship cycle based on customer feedback (and the principle of least surprise).
More information:
http://bradwilson.typepad.com/blog/2010/01/input-validation-vs-model-validation-in-aspnet-mvc.html
For those who want avoiding the "validate everything then delete unwanted properties" scenario, I've extended the default model binder using a nested model metadata provider (because the "Properties" property of ModelMetadata is readonly...) :
So, now, I can only validate "non-excluded properties" :
public class OldWayValidationBinder : DefaultModelBinder {
private readonly ModelMetadataProvider _metadataProvider;
public ValidationBinder(ModelMetadataProvider metadataProvider) {
_metadataProvider = metadataProvider;
}
protected ModelMetadata CreateModelMetadata(ModelBindingContext bindingContext) {
var metadataProvider = new ModelMetadataProviderAdapter(
_metadataProvider, bindingContext.PropertyFilter);
return new ModelMetadata(metadataProvider,
bindingContext.ModelMetadata.ContainerType,
() => bindingContext.ModelMetadata.Model,
bindingContext.ModelMetadata.ModelType,
bindingContext.ModelMetadata.PropertyName);
}
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext) {
base.OnModelUpdated(controllerContext, new ModelBindingContext(bindingContext) {
ModelMetadata = CreateModelMetadata(bindingContext)
});
}
private sealed class ModelMetadataProviderAdapter : ModelMetadataProvider {
private readonly ModelMetadataProvider _innerMetadataProvider;
private readonly Predicate<string> _propertyFilter;
internal ModelMetadataProviderAdapter(
ModelMetadataProvider innerMetadataProvider,
Predicate<string> propertyFilter) {
_innerMetadataProvider = innerMetadataProvider;
_propertyFilter = propertyFilter;
}
public override IEnumerable<ModelMetadata> GetMetadataForProperties(object container, Type containerType) {
return _innerMetadataProvider.GetMetadataForProperties(container, containerType)
.Where(metadata => _propertyFilter(metadata.PropertyName));
}
public override ModelMetadata GetMetadataForProperty(Func<object> modelAccessor, Type containerType, string propertyName) {
return _innerMetadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName);
}
public override ModelMetadata GetMetadataForType(Func<object> modelAccessor, Type modelType) {
return _innerMetadataProvider.GetMetadataForType(modelAccessor, modelType);
}
}
}
internal sealed class ValidationProvider : ModelValidatorProvider {
private readonly IValidationFactory _validationFactory;
public ValidationProvider(IValidationFactory validationFactory) {
_validationFactory = validationFactory;
}
public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context) {
if (metadata.ModelType != null) {
IValidationService validationService;
if (_validationFactory.TryCreateServiceFor(metadata.ModelType, out validationService)) {
yield return new ModelValidatorAdapter(metadata, context, validationService);
}
}
}
private sealed class ModelValidatorAdapter : ModelValidator {
private readonly IValidationService _validationService;
internal ValidationAdapter(ModelMetadata metadata,
ControllerContext controllerContext,
IValidationService validationService)
: base(metadata, controllerContext) {
_validationService = validationService;
}
public override IEnumerable<ModelValidationResult> Validate(object container) {
if (Metadata.Model != null) {
IEnumerable<ValidationFault> validationFaults;
var validatableProperties = Metadata.Properties.Select(metadata => Metadata.ModelType.GetProperty(metadata.PropertyName));
if (!_validationService.TryValidate(Metadata.Model, validatableProperties, out validationFaults)) {
return validationFaults.Select(fault => new ModelValidationResult {
MemberName = fault.PropertyInfo.Name,
Message = fault.FaultedRule.Message
});
}
}
return Enumerable.Empty<ModelValidationResult>();
}
}
}
Nonetheless, I believe this scenario must be present as an option in MVC. At least, the unbound properties list should be given as a parameter of ModelValidatorProvider's GetValidators method!
I think the "old behavior" can be easily recovered by overriding the OnModelUpdating method of the DefaultModelBinder. Please point me in the right direction whether it's not the good way to achieve that :
internal sealed class OldWayModelBinder : DefaultModelBinder {
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext) {
foreach (var validationResult in ModelValidator.GetModelValidator(bindingContext.ModelMetadata, controllerContext).Validate(null)) {
string subPropertyName = CreateSubPropertyName(bindingContext.ModelName, validationResult.MemberName);
if (bindingContext.PropertyFilter(subPropertyName)) {
if (bindingContext.ModelState.IsValidField(subPropertyName)) {
bindingContext.ModelState.AddModelError(subPropertyName, validationResult.Message);
}
}
}
}
}
(however, the fact that the IsValidField method returns true if a given property is faulted is a little bit strange or there is something I don't understand!)
精彩评论