Search code examples
asp.net-mvcasp.net-web-apimodel-binding

ModelState.IsValid is false when I have a nullable parameter


I reproduced the issue I am having in a brand new MVC Web API project.

This is the default code with a slight modification.

public string Get(int? id, int? something = null)
{
    var isValid = ModelState.IsValid;
    return "value";
}

If you go to http://localhost/api/values/5?something=123 then this works fine, and isValid is true.

If you go to http://localhost/api/values/5?something= then isValid is false.

The issue I am having is that if you provide a null or omitted value for an item that is nullable, the ModelState.IsValid flags a validation error saying "A value is required but was not present in the request."

The ModelState dictionary also looks like this:

enter image description here

with two entries for something, one nullable, which I am not sure if it is significant or not.

Any idea how I can fix this so that the model is valid when nullable parameters are omitted or provided as null? I am using model validation within my web api and it breaks it if every method with a nullable parameter generates model errors.


Solution

  • It appears that the default binding model doesn't fully understand nullable types. As seen in the question, it gives three parameter errors rather than the expected two.

    You can get around this with a custom nullable model binder:

    Model Binder

    public class NullableIntModelBinder : IModelBinder
    {
        public bool BindModel(System.Web.Http.Controllers.HttpActionContext actionContext, ModelBindingContext bindingContext)
        {
            if (bindingContext.ModelType != typeof(int?))
            {
                return false;
            }
    
            ValueProviderResult val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
            if (val == null)
            {
                return false;
            }
    
            string rawvalue = val.RawValue as string;
    
            // Not supplied : /test/5
            if (rawvalue == null)
            {
                bindingContext.Model = null;
                return true;
            }
    
            // Provided but with no value : /test/5?something=
            if (rawvalue == string.Empty)
            {
                bindingContext.Model = null;
                return true;
            }
    
            // Provided with a value : /test/5?something=1
            int result;
            if (int.TryParse(rawvalue, out result))
            {
                bindingContext.Model = result;
                return true;
            }
    
            bindingContext.ModelState.AddModelError(bindingContext.ModelName, "Cannot convert value to int");
            return false;
        }
    }
    

    Usage

    public ModelStateDictionary Get(
        int? id, 
        [ModelBinder(typeof(NullableIntModelBinder))]int? something = null)
    {
        var isValid = ModelState.IsValid;
    
        return ModelState;
    }
    

    Adapted from the asp.net page: http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api for further reading and an alternative method to set it at the class(controller) level rather than per parameter.

    This handles the 3 valid scenarios:

    /test/5
    /test/5?something=
    /test/5?something=2
    

    this first give "something" as null. Anything else (eg ?something=x) gives an error.

    If you change the signature to

    int? somthing
    

    (ie remove = null) then you must explicitly provide the parameter, ie /test/5 will not be a valid route unless you tweak your routes as well.