Web API操作参数间歇性为null

时间:2013-08-15 16:08:21

标签: c# asp.net-mvc asp.net-web-api model-binding

相关问题:Web API ApiController PUT and POST methods receive null parameters intermittently

背景

在对现有Web API项目进行负载测试时,由于参数在发布到操作时为null,因此我注意到很多空引用异常。

原因似乎是在dev环境中运行时为日志请求注册的自定义消息处理程序。删除此处理程序可解决此问题。

我理解在Web API中我只能读取一次请求体,并且读取它会始终导致我的参数为null,因为模型绑定将无法进行。出于这个原因,我正在使用带有ContinueWith的ReadAsStringAsync()方法来读取正文。看起来这在~0.2%的请求中表现奇怪(在使用Apache Bench进行本地调试期间)。

代码

在最基本的层面上,我有以下几点:

模型

public class User
{
    public string Name { get; set; }
}

API控制器

public class UsersController : ApiController
{
    [HttpPost]
    public void Foo(User user)
    {
        if (user == null)
        {
            throw new NullReferenceException();
        }
    }
}

消息处理程序

public class TestMessageHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        request.Content.ReadAsStringAsync().ContinueWith((task) =>
        {
            /* do stuff with task.Result */
        });

        return base.SendAsync(request, cancellationToken);
    }
}

...在应用启动期间注册

GlobalConfiguration.Configuration.MessageHandlers.Add(new TestMessageHandler());

我正在使用WebAPI 4.0.30506.0,这是发布时的最新版本。项目中的所有其他MS包也运行最新版本(下面链接的演示项目现已更新,以反映这一点)。

测试

使用Loadster对使用.NET 4.0.30319的Server 2008 R2上的负载平衡IIS 7.5设置进行初始测试。我正在使用Apache Bench在.NET 7上使用.NET 4.5.50709在IIS 7.5上本地复制它。

ab -n 500 -c 25 -p testdata.post -T "application/json" http://localhost/ModelBindingFail/api/users/foo

其中testdata.post包含

{ "Name":"James" }

通过这次测试,我发现500个请求大约有1个失败,所以~0.2%。

后续步骤......

如果您想尝试自己,我已将我的演示项目放在GitHub上,不过除了上面发布的内容之外,它还是一个标准的空Web API项目。

也很乐意尝试任何建议或发布更多信息。谢谢!

1 个答案:

答案 0 :(得分:2)

我还在研究这个的根本原因但到目前为止,我的直觉是,ContinueWith()正在不同的上下文中执行,或者在请求流被处理的位置或类似的位置执行(一次)我想这肯定我会更新这段。)

就修复方面而言,我已经快速测试了三个可以处理500个请求而没有错误的路径。

最简单的方法就是使用task.Result,但这确实存在一些问题(尽管是YMMV,它可以apparently cause deadlocks。)

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
    var result = request.Content.ReadAsStringAsync().Result;
    return base.SendAsync(request, cancellationToken);
}

接下来,你可以确保你正确地链接你的延续,以避免任何关于上下文的歧义,但它是非常难看的(并且我不能100%确定它是否是副作用):

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
    var result = request.Content.ReadAsStringAsync().ContinueWith(task =>
    {
        /* do stuff with task.Result */
    });

    return result.ContinueWith(t => base.SendAsync(request, cancellationToken)).Unwrap();
}

最后,最佳解决方案似乎是使用async / await到sweep away any threading nasties,显然如果您遇到.NET 4.0,这可能是一个问题。

protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
    var content = await request.Content.ReadAsStringAsync();
    Debug.WriteLine(content);
    return await base.SendAsync(request, cancellationToken);
}