Web API操作参数间歇性为null

相关问题: Web API ApiController PUT和POST方法间歇性地接收空参数

背景

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

原因似乎是在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 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在Windows 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项目。

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

我还在调查这个的根本原因但到目前为止,我的直觉是,ContinueWith()正在不同的上下文中执行,或者在请求流被处理的那个点或类似的东西上执行(一旦我认为我肯定会更新这一段)。

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

最简单的是只使用task.Result ,但它有一些问题(它显然会导致死锁 ,虽然YMMV)。

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

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

 protected override Task 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 扫除任何线程恶意 ,显然如果你被困在.NET 4.0上,这可能是一个问题。

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