6

I'm building Web Service with Web API 5. I'm implementing custom model binder by extending IModelBinder interface to map complex type as a parameter to action. The binding part is working fine. But Model validation does not occur. ModelState.IsValid is always true.

public class PagingParamsVM
{
        [Range(1, Int32.MaxValue, ErrorMessage = "Page must be at least 1")]
        public int? Page { get; set; }

        [Range(1, Int32.MaxValue, ErrorMessage = "Page size must be at least 1")]
        public int? PageSize { get; set; }
}

public class PaginationModelBinder : IModelBinder
{
        public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
        {
              var model = (PagingParamsVM)bindingContext.Model ?? new PagingParamsVM();
              //model population logic
              .....

              bindingContext.Model = model;
              return true;
        }
}

public IEnumerable<NewsItemVM> Get([ModelBinder(typeof(PaginationModelBinder))]PagingParamsVM pegination)
{
            //Validate(pegination); //if I call this explicitly ModelState.IsValid is set correctly.
            var valid = ModelState.IsValid; //this is always true
}

public class ModelStateValidationActionFilter : ActionFilterAttribute
{
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            var valid = actionContext.ModelState.IsValid //this is always true.
        }
}

If I call Validate() explicitly or use [FromUri] attribute, ModelState.IsValid is set correctly.

public IEnumerable<NewsItemVM> Get([FromUri]PagingParamsVM pegination)
{
            var valid = ModelState.IsValid;
}

Should I implement validation part inside model binder. If so how should I implement?

SajithK
  • 1,014
  • 12
  • 23
  • Possible duplicate of [SO answer](http://stackoverflow.com/questions/8668869/custom-model-binder-not-validating-model). – Mihail Stancescu Dec 05 '16 at 07:49
  • @MihailStancescu I saw this question. It works fine with DataAnnotations. but if I use FluentValidation or similar, It won't work. So seems something is missing here. – SajithK Dec 05 '16 at 08:24

2 Answers2

3

I found an answer. The default validation process can be invoked in custom model binder as follows,

public abstract class PaginationModelBinder : IModelBinder
{
        public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
        {
              var model = (PagingParamsVM)bindingContext.Model ?? new PagingParamsVM();
              //model population logic
              .....

              bindingContext.Model = model;

              //following lines invoke default validation on model
              bindingContext.ValidationNode.ValidateAllProperties = true;
              bindingContext.ValidationNode.Validate(actionContext);

              return true;
        }
}

Thank you guys for your support.

SajithK
  • 1,014
  • 12
  • 23
  • @Sajith I have tried the same, but the validation not triggered for the sub entity used in the model. Any option to invoke validations for the entities used in the main model – Magendran V Mar 27 '18 at 12:01
  • @MagendranV i'm quite curious if you've managed to find a solution since I've hit the same dead end. The DataAnnotations validation works brilliantly for the 1st level properties, but anything any deeper and it doesn't work – Andrei U Apr 30 '18 at 15:50
0

DefaultModelBinder.CreateModel should help you keep model state:

public class PaginationModelBinder : DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
    {
        if(modelType == typeof(PagingParamsVM))
        {
            var page = default(int?);
            var model = bindingContext.Model;
            var valueProvider = bindingContext.ValueProvider;
            var pageValue = valueProvider.GetValue("Page");
            var tmp = default(int);
            if(pageValue != null && int.TryParse(pageValue.AttemptedValue, out tmp))
            {
                page = tmp;
            }

            var pageSize = default(int?);
            var sizeValue = valueProvider.GetValue("PageSize");
            if(sizeValue != null && int.TryParse(sizeValue.AttemptedValue, out tmp))
            {
                pageSize = tmp;
            }
            return new PagingParamsVM { Page = page, PageSize = pageSize };
        }
        return base.CreateModel(controllerContext, bindingContext, modelType);
    }
}

A web api controller that uses the binder can be:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;

public class NewsItemController : ApiController
{
    public IEnumerable<NewsItemVM> Get([ModelBinder(typeof(PaginationModelBinder))]PagingParamsVM pegination)
    {
        //Validate(pegination); //if I call this explicitly ModelState.IsValid is set correctly.
        var valid = ModelState.IsValid; //this is always true
        return Enumerable.Empty<NewsItemVM>();
    }
}
Bob Dust
  • 2,370
  • 1
  • 17
  • 13
  • Web API does not have DefaultModelBinder. DefaultModelBinder comes with MVC. – SajithK Dec 05 '16 at 08:21
  • @sajith, when you develop Web API in a MVC .NET application, `DefaultModelBinder` will come :) – Bob Dust Dec 05 '16 at 08:27
  • Yah I know that. But the thing is that you can not use your PaginationModelBinder : DefaultModelBinder in a controller extended ApiController. – SajithK Dec 05 '16 at 08:36
  • Have you tried your code? I tried this earlier. it gives the following error, Could not create a 'IModelBinder' from 'PaginationModelBinder'. Please ensure it derives from 'IModelBinder' and has a public parameterless constructor. I think the reason is that the model binder must be derived from System.Web.Http.ModelBinding.IModelBinder. But DefaultModelBinder is in System.Web.Mvc namespace. – SajithK Dec 05 '16 at 09:01