Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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 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!

like image 634
Kévin Chalet Avatar asked Dec 02 '25 05:12

Kévin Chalet


2 Answers

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

like image 158
Brad Wilson Avatar answered Dec 03 '25 18:12

Brad Wilson


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!

like image 28
Kévin Chalet Avatar answered Dec 03 '25 17:12

Kévin Chalet



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!