Aen Sidhe
Aen Sidhe

Reputation: 1171

Why WebApi marks empty string as error in model state?

I have strange behaviour of Web API, .Net 4.5.2. If optional string parameter is null, ModelState has no error. If it is not null and not empty, no errors again. But if it is just an empty string I have model state error.

Why do I get it and how to disable it?

Assuming app served on localhost:82 I have those results:

Url: http://localhost:82/
Response: "null"

Url: http://localhost:82/?q=1
Response: "1"

Url: http://localhost:82/?q=
Response: {
  "Message": "The request is invalid.",
  "ModelState": {
    "q.String": [
      "A value is required but was not present in the request."
    ]
  }
}

Test controller and config is below. This is reduced to bare minimum default "Asp.net web application" with "WebApi" in VS2013.

namespace Web.Api.Test.Controllers
{
    using System.Web.Http;

    [Route]
    public class HomeController : ApiController
    {
        [Route]
        [HttpGet]
        public IHttpActionResult Search(string q = default(string))
        {
            return this.ModelState.IsValid
                ? this.Ok(q ?? "null")
                : (IHttpActionResult)this.BadRequest(this.ModelState);
        }
    }
}

Startup.cs is:

using Microsoft.Owin;

using WebApplication1;

[assembly: OwinStartup(typeof(Startup))]

namespace WebApplication1
{
    using System.Web.Http;

    using Newtonsoft.Json;

    using Owin;

    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            GlobalConfiguration.Configure(config =>
            {
                config.MapHttpAttributeRoutes();
                config.Formatters.JsonFormatter.SerializerSettings.Formatting = Formatting.Indented;

                config.Formatters.Remove(config.Formatters.XmlFormatter);
            });
        }
    }
}

PS: This question has a workaround, but it does not answer the main question: why does this situation happen and what reasons are behind this design decision.

Upvotes: 9

Views: 7510

Answers (6)

Vadim Nazarenko
Vadim Nazarenko

Reputation: 29

code below is adapted version of this answer

public class WebApiDefaultValueBinder<T> : IModelBinder
{
    public bool BindModel(System.Web.Http.Controllers.HttpActionContext actionContext, ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType != typeof(T))
        {
            return false;
        }

        var val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (val == null)
        {
            return false;
        }

        var rawValue = val.RawValue as string;

        // Not supplied : /test/5
        if (rawValue == null)
        {
            bindingContext.Model = default(T);
            return true;
        }

        // Provided but with no value : /test/5?something=
        if (rawValue == string.Empty)
        {
            bindingContext.Model = default(T);
            return true;
        }

        // Provided with a value : /test/5?something=1
        try
        {
            bindingContext.Model = (T)Convert.ChangeType(val.RawValue, typeof(T));
            return true;
        }
        catch
        {
            //
        }

        bindingContext.ModelState.AddModelError(bindingContext.ModelName, $"Cannot convert value to {typeof(T).Name}");
        return false;
    }
}

Upvotes: 0

mkurdukov
mkurdukov

Reputation: 1

We found another solution

public class EmptyStringToNullModelBinder : Attribute, IModelBinder
{
    public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext)
    {
        ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        bindingContext.Model = string.IsNullOrWhiteSpace(valueResult?.RawValue?.ToString()) ? null : valueResult.RawValue;
        return true;
    }
}

and for your case it would be like this:

[Route]
[HttpGet]
public IHttpActionResult Search([FromUri(BinderType = typeof(EmptyStringToNullModelBinder))]string q = null)
{
    return this.ModelState.IsValid
        ? this.Ok(q ?? "null")
        : (IHttpActionResult)this.BadRequest(this.ModelState);
}

Upvotes: 0

stukselbax
stukselbax

Reputation: 5935

That's why:

This is a MVC feature which binds empty strings to nulls.

We have found the same behavior in our application, and deep dive debugging with source code

git clone https://github.com/ASP-NET-MVC/aspnetwebstack

makes sense to search in the right direction. Here the method which set whitespace strings to null, and here the error is added to model state:

if (parentNode == null && ModelMetadata.Model == null)
{
    string trueModelStateKey = ModelBindingHelper.CreatePropertyModelName(ModelStateKey, ModelMetadata.GetDisplayName());
    modelState.AddModelError(trueModelStateKey, SRResources.Validation_ValueNotFound);
    return;
}

IMHO it is a bug. But who cares. We used this workaround

Upvotes: 1

Gusev Petr
Gusev Petr

Reputation: 101

I have had the same issue, came up with the following eventually:

public class SimpleTypeParameterBindingFactory
{
    private readonly TypeConverterModelBinder converterModelBinder = new TypeConverterModelBinder();
    private readonly IEnumerable<ValueProviderFactory> factories;

    public SimpleTypeParameterBindingFactory(HttpConfiguration configuration)
    {
        factories = configuration.Services.GetValueProviderFactories();
    }

    public HttpParameterBinding BindOrNull(HttpParameterDescriptor descriptor)
    {
        return IsSimpleType(descriptor.ParameterType)
            ? new ModelBinderParameterBinding(descriptor, converterModelBinder, factories)
            : null;
    }

    private static bool IsSimpleType(Type type)
    {
        return TypeDescriptor.GetConverter(type).CanConvertFrom(typeof (string));
    }
}

public class Startup
{
    public void Configure(IAppBuilder appBuilder)
    {
        var configuration = new HttpConfiguration();
        configuration.ParameterBindingRules.Insert(0, new SimpleTypeParameterBindingFactory(configuration).BindOrNull);
        configuration.EnsureInitialized();
    }
}

The problem is rooted in some magic code in ModelValidationNode, which creates model errors for null models even if corresponding parameter has default value. The code above just replaces CompositeModelBinder (which calls ModelValidationNode) with TypeConverterModelBinder for simple type parameters.

Upvotes: 6

weston
weston

Reputation: 54781

Why do I get it and how to disable it?

Don't know why you get it. This maybe how you disable it, but after reading I don't think you want to really as there are simpler solutions, e.g:

Use of a model class solves this in a cleaner way.

public class SearchModel
{
    public string Q { get; set; }
}

public IHttpActionResult Search([FromUri] SearchModel model)
{
    return ModelState.IsValid
        ? Ok(model.Q ?? "null")
        : (IHttpActionResult) BadRequest(ModelState);
}

Upvotes: 4

BNK
BNK

Reputation: 24114

Have you tried [DisplayFormat(ConvertEmptyStringToNull = false)]?

Upvotes: 0

Related Questions