Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 337
NibblyPig Avatar asked Sep 30 '15 08:09

NibblyPig


People also ask

Does ModelState IsValid check for NULL?

The ModelState. IsValid property (ApiController) merely checks for a zero validation error count for a passed model while ignoring the nullability of the object instance itself.

What causes false IsValid ModelState?

IsValid is false now. That's because an error exists; ModelState. IsValid is false if any of the properties submitted have any error messages attached to them. What all of this means is that by setting up the validation in this manner, we allow MVC to just work the way it was designed.

How do I know if my ModelState is valid?

Below the Form, the ModelState. IsValid property is checked and if the Model is valid, then the value if the ViewBag object is displayed using Razor syntax in ASP.Net MVC.

What does ModelState IsValid mean?

ModelState. IsValid indicates if it was possible to bind the incoming values from the request to the model correctly and whether any explicitly specified validation rules were broken during the model binding process.


2 Answers

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.

like image 91
freedomn-m Avatar answered Sep 19 '22 00:09

freedomn-m


You'll have to register a custom model-binder for nullable types as the default binder is calling the validator for nullable parameters as well, and the latter considers those empty values as invalid.

The Model Binder:

public class NullableModelBinder<T> : System.Web.Http.ModelBinding.IModelBinder where T : struct
{
    private static readonly TypeConverter converter = TypeDescriptor.GetConverter( typeof( T ) );

    public bool BindModel( HttpActionContext actionContext, System.Web.Http.ModelBinding.ModelBindingContext bindingContext )
    {
        var val = bindingContext.ValueProvider.GetValue( bindingContext.ModelName );

        // Cast value to string but when it fails we must not suppress the validation
        if ( !( val?.RawValue is string rawVal ) ) return false;

        // If the string contains a valid value we can convert it and complete the binding
        if ( converter.IsValid( rawVal ) )
        {
            bindingContext.Model = converter.ConvertFromString( rawVal );
            return true;
        }

        // If the string does contain data it cannot be nullable T and we must not suppress this error
        if ( !string.IsNullOrWhiteSpace( rawVal ) ) return false;

        // String is empty and allowed due to it being a nullable type
        bindingContext.ValidationNode.SuppressValidation = true;
        return false;
    }
}

Registration:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        // ...

        var provider = new SimpleModelBinderProvider(typeof(int?), new NullableModelBinder<int>());
        config.Services.Insert(typeof(ModelBinderProvider), 0, provider);

        // ...
    }
}
like image 38
haim770 Avatar answered Sep 19 '22 00:09

haim770