为什么WebApi在模型状态中将空字符串标记为错误

本文关键字:字符串 记为 错误 WebApi 模型 状态 为什么 | 更新日期: 2023-09-27 18:06:02

我有奇怪的行为的Web API, . net 4.5.2。如果可选字符串参数为空,则ModelState没有错误。如果它不为空,也不为空,则不会再次出现错误。但如果它只是一个空字符串,我有模型状态错误。

为什么我得到它和如何禁用它?

假设应用程序服务于localhost:82,我有这些结果:

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."
    ]
  }
}

测试控制器和配置如下。在VS2013中,这减少到最小的默认"Asp.net web应用程序"与"WebApi"。

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是:

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:这个问题有一个变通办法,但它没有回答主要问题:为什么会发生这种情况,以及这种设计决策背后的原因是什么。

为什么WebApi在模型状态中将空字符串标记为错误

我也遇到过同样的问题,最终得出以下结论:

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();
    }
}

问题根源于ModelValidationNode中的一些神奇代码,即使相应的参数具有默认值,它也会为空模型创建模型错误。上面的代码只是用TypeConverterModelBinder替换了简单类型参数的CompositeModelBinder(它调用ModelValidationNode)。

为什么我得到它和如何禁用它?

不知道你为什么得到它。这可能是你禁用它的方式,但在阅读之后,我不认为你真的想要,因为有更简单的解决方案,例如:

使用模型类以一种更简洁的方式解决了这个问题。

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

这就是为什么:

这是一个MVC特性,它将空字符串绑定到null。

我们在我们的应用程序中发现了相同的行为,并使用源代码进行深入调试

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

在正确的方向上搜索是有意义的。这里的方法将空白字符串设置为null,这里的错误被添加到模型状态:

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

IMHO这是一个bug。但谁在乎呢。我们使用了这个变通方法

您试过[DisplayFormat(ConvertEmptyStringToNull = false)]吗?

我们找到了另一个解决方案

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;
    }
}

对于你的情况,它应该是这样的:

[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);
}

下面的代码是这个答案的改编版本

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;
    }
}