How to force MVC to Validate IValidatableObject

前端 未结 2 2116
不思量自难忘°
不思量自难忘° 2020-11-27 03:06

It seems that when MVC validates a Model that it runs through the DataAnnotation attributes (like required, or range) first and if any of those fail it skips running the Val

相关标签:
2条回答
  • 2020-11-27 03:29

    You can manually call Validate() by passing in a new instance of ValidationContext, like so:

    [HttpPost]
    public ActionResult Create(Model model) {
        if (!ModelState.IsValid) {
            var errors = model.Validate(new ValidationContext(model, null, null));
            foreach (var error in errors)                                 
                foreach (var memberName in error.MemberNames)
                    ModelState.AddModelError(memberName, error.ErrorMessage);
    
            return View(post);
        }
    }
    

    A caveat of this approach is that in instances where there are no property-level (DataAnnotation) errors, the validation will be run twice. To avoid that, you could add a property to your model, say a boolean Validated, which you set to true in your Validate() method once it runs and then check before manually calling the method in your controller.

    So in your controller:

    if (!ModelState.IsValid) {
        if (!model.Validated) {
            var validationResults = model.Validate(new ValidationContext(model, null, null));
            foreach (var error in validationResults)
                foreach (var memberName in error.MemberNames)
                    ModelState.AddModelError(memberName, error.ErrorMessage);
        }
    
        return View(post);
    }
    

    And in your model:

    public bool Validated { get; set; }
    
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
        // perform validation
    
        Validated = true;
    }
    
    0 讨论(0)
  • 2020-11-27 03:33

    There's a way to do it without requiring boilerplate code at the top of each controller action.

    You'll need to replace the default model binder with one of your own:

    protected void Application_Start()
    {
        // ...
        ModelBinderProviders.BinderProviders.Clear();
        ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider());
        // ...
    }
    

    Your model binder provider looks like this:

    public class CustomModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(Type modelType)
        {
            return new CustomModelBinder();
        }
    }
    

    Now create a custom model binder that actually forces the validation. This is where the heavy lifting's done:

    public class CustomModelBinder : DefaultModelBinder
    {
        protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            base.OnModelUpdated(controllerContext, bindingContext);
    
            ForceModelValidation(bindingContext);
        }
    
        private static void ForceModelValidation(ModelBindingContext bindingContext)
        {
            var model = bindingContext.Model as IValidatableObject;
            if (model == null) return;
    
            var modelState = bindingContext.ModelState;
    
            var errors = model.Validate(new ValidationContext(model, null, null));
            foreach (var error in errors)
            {
                foreach (var memberName in error.MemberNames)
                {
                    // Only add errors that haven't already been added.
                    // (This can happen if the model's Validate(...) method is called more than once, which will happen when
                    // there are no property-level validation failures.)
                    var memberNameClone = memberName;
                    var idx = modelState.Keys.IndexOf(k => k == memberNameClone);
                    if (idx < 0) continue;
                    if (modelState.Values.ToArray()[idx].Errors.Any()) continue;
    
                    modelState.AddModelError(memberName, error.ErrorMessage);
                }
            }
        }
    }
    

    You'll need an IndexOf extension method, too. This is a cheap implementation but it'll work:

    public static int IndexOf<T>(this IEnumerable<T> source, Func<T, bool> predicate)
    {
        if (source == null) throw new ArgumentNullException("source");
        if (predicate == null) throw new ArgumentNullException("predicate");
    
        var i = 0;
        foreach (var item in source)
        {
            if (predicate(item)) return i;
            i++;
        }
    
        return -1;
    }
    
    0 讨论(0)
提交回复
热议问题