AspNetCore.TestHost - headers 中间件在并行集成测试下引发异常

AspNetCore.TestHost - headers middleware raising exceptions under parallel integration testing

首先,我在 Visual Studio 中创建了一个 out-of-the-box 模板 ASP.NET Core MVC web 应用程序。

然后我添加这个中间件:

public async Task Invoke(HttpContext context)
{
    context.Response.OnStarting(state =>
    {
        var resp = ((HttpContext)state).Response;
        resp.Headers.Add("MyHeader", "header");
        return Task.CompletedTask;
    }, context);

    await _next.Invoke(context );
}

这在浏览器中运行良好,并且在每个页面响应中都按预期显示 header。太好了。

现在我创建一个测试项目,添加 nuget 包 'Microsoft.AspNetCore.TestHost'、'xunit'、'xunit.runners',并放入测试:

    [Fact]
    public void TestHomePageParallel()
    {
        using (var server = CreateServer())
        {
            Parallel.For((long) 0, 10, index =>
            {
                Get(server, "/");
            });
        }
    }


    private TestServer CreateServer()
    {
        Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development");
        var directory = Directory.GetCurrentDirectory();
        var setDir = Path.GetFullPath(
            Path.Combine(directory, @"..\..\..\..\..\WebApplication4")
        );

        var builder = new WebHostBuilder()
            .UseContentRoot(setDir)
            .UseStartup<Startup>();

        return new TestServer(builder);
    }


    public void Get(TestServer server, string url)
    {
        using (var client = server.CreateClient())
        {
            client.BaseAddress = new Uri("http://localhost:5000");
            var req = new HttpRequestMessage();
            req.Method = HttpMethod.Get;
            req.RequestUri = new Uri(url, UriKind.Relative);
            var resp = client.SendAsync(req).Result;
            resp.Dispose();
        }
    }

这将从中间件中的 Resp.Headers.Add() 代码中抛出异常:

One or more errors occurred.
One or more errors occurred.
An item with the same key has already been added.
One or more errors occurred.
An item with the same key has already been added.
One or more errors occurred.
An item with the same key has already been added.
One or more errors occurred.
etc.

所以...为什么会这样?

请注意,如果将测试循环更改为 non-parallel 循环,则不会出现异常。

另请注意,如果您更改中间件代码,使 Response.OnStarting() 在 之后 调用 _next.Invoke() 问题就会消失,即没有记录异常。但是我看到的每个中间件代码添加 headers 的例子都 而不是 这样做,我认为这可能是不安全的 and/or 来不及了以这种方式添加回调:

    public async Task Invoke(HttpContext context)
    {    
        await _next.Invoke(context );

        context.Response.OnStarting(state =>
        {
            var resp = ((HttpContext)state).Response;
            resp.Headers.Add("MyHeader", "header");
            return Task.CompletedTask;
        }, context);
    }

如果你安装了VS2015和.net core工具,你可以自己下载解决方案在这里试试:https://dl.dropboxusercontent.com/u/57858722/HeadersNoWorky.zip

Full stack trace:

Test Name:  Test.Integration.TestHomePageParallel
Test FullName:  Test.Integration.TestHomePageParallel
Test Source:    C:\HeadersNoWorky\Test\Test.cs : line 33
Test Outcome:   Failed
Test Duration:  0:00:04.226

Result StackTrace:  
at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions)
   at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken)
   at System.Threading.Tasks.Parallel.ForWorker64[TLocal](Int64 fromInclusive, Int64 toExclusive, ParallelOptions parallelOptions, Action`1 body, Action`2 bodyWithState, Func`4 bodyWithLocal, Func`1 localInit, Action`1 localFinally)
   at System.Threading.Tasks.Parallel.For(Int64 fromInclusive, Int64 toExclusive, Action`1 body)
   at Test.Integration.TestHomePageParallel() in C:\HeadersNoWorky\Test\Test.cs:line 36
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at Test.Integration.Get(TestServer server, String url) in C:\HeadersNoWorky\Test\Test.cs:line 68
   at Test.Integration.<>c__DisplayClass1_0.<TestHomePageParallel>b__0(Int64 index) in C:\HeadersNoWorky\Test\Test.cs:line 38
   at System.Threading.Tasks.Parallel.<>c__DisplayClass18_0`1.<ForWorker64>b__1()
   at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)
   at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object )
   at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
   at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
   at Microsoft.AspNetCore.Http.HeaderDictionary.Add(String key, StringValues value)
   at WebApplication4.MiddlewareHeader.<>c.<Invoke>b__2_0(Object state) in C:\HeadersNoWorky\WebApplication4\MiddlewareHeader.cs:line 20
   at Microsoft.AspNetCore.TestHost.ResponseFeature.<>c__DisplayClass23_0.<OnStarting>b__0()
   at Microsoft.AspNetCore.TestHost.ResponseFeature.FireOnSendingHeaders()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.GenerateResponse()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.ReturnResponseMessage()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.CompleteResponse()
   at Microsoft.AspNetCore.TestHost.ClientHandler.<>c__DisplayClass3_0.<<SendAsync>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.TestHost.ClientHandler.<SendAsync>d__3.MoveNext()
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at Test.Integration.Get(TestServer server, String url) in C:\HeadersNoWorky\Test\Test.cs:line 68
   at Test.Integration.<>c__DisplayClass1_0.<TestHomePageParallel>b__0(Int64 index) in C:\HeadersNoWorky\Test\Test.cs:line 38
   at System.Threading.Tasks.Parallel.<>c__DisplayClass18_0`1.<ForWorker64>b__1()
   at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)
   at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object )
   at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
   at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
   at Microsoft.AspNetCore.Http.HeaderDictionary.Add(String key, StringValues value)
   at WebApplication4.MiddlewareHeader.<>c.<Invoke>b__2_0(Object state) in C:\HeadersNoWorky\WebApplication4\MiddlewareHeader.cs:line 20
   at Microsoft.AspNetCore.TestHost.ResponseFeature.<>c__DisplayClass23_0.<OnStarting>b__0()
   at Microsoft.AspNetCore.TestHost.ResponseFeature.FireOnSendingHeaders()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.GenerateResponse()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.ReturnResponseMessage()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.CompleteResponse()
   at Microsoft.AspNetCore.TestHost.ClientHandler.<>c__DisplayClass3_0.<<SendAsync>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.TestHost.ClientHandler.<SendAsync>d__3.MoveNext()
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at Test.Integration.Get(TestServer server, String url) in C:\HeadersNoWorky\Test\Test.cs:line 68
   at Test.Integration.<>c__DisplayClass1_0.<TestHomePageParallel>b__0(Int64 index) in C:\HeadersNoWorky\Test\Test.cs:line 38
   at System.Threading.Tasks.Parallel.<>c__DisplayClass18_0`1.<ForWorker64>b__1()
   at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)
   at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object )
   at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
   at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
   at Microsoft.AspNetCore.Http.HeaderDictionary.Add(String key, StringValues value)
   at WebApplication4.MiddlewareHeader.<>c.<Invoke>b__2_0(Object state) in C:\HeadersNoWorky\WebApplication4\MiddlewareHeader.cs:line 20
   at Microsoft.AspNetCore.TestHost.ResponseFeature.<>c__DisplayClass23_0.<OnStarting>b__0()
   at Microsoft.AspNetCore.TestHost.ResponseFeature.FireOnSendingHeaders()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.GenerateResponse()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.ReturnResponseMessage()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.CompleteResponse()
   at Microsoft.AspNetCore.TestHost.ClientHandler.<>c__DisplayClass3_0.<<SendAsync>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.TestHost.ClientHandler.<SendAsync>d__3.MoveNext()
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at Test.Integration.Get(TestServer server, String url) in C:\HeadersNoWorky\Test\Test.cs:line 68
   at Test.Integration.<>c__DisplayClass1_0.<TestHomePageParallel>b__0(Int64 index) in C:\HeadersNoWorky\Test\Test.cs:line 38
   at System.Threading.Tasks.Parallel.<>c__DisplayClass18_0`1.<ForWorker64>b__1()
   at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)
   at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object )
   at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
   at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
   at Microsoft.AspNetCore.Http.HeaderDictionary.Add(String key, StringValues value)
   at WebApplication4.MiddlewareHeader.<>c.<Invoke>b__2_0(Object state) in C:\HeadersNoWorky\WebApplication4\MiddlewareHeader.cs:line 20
   at Microsoft.AspNetCore.TestHost.ResponseFeature.<>c__DisplayClass23_0.<OnStarting>b__0()
   at Microsoft.AspNetCore.TestHost.ResponseFeature.FireOnSendingHeaders()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.GenerateResponse()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.ReturnResponseMessage()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.CompleteResponse()
   at Microsoft.AspNetCore.TestHost.ClientHandler.<>c__DisplayClass3_0.<<SendAsync>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.TestHost.ClientHandler.<SendAsync>d__3.MoveNext()
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at Test.Integration.Get(TestServer server, String url) in C:\HeadersNoWorky\Test\Test.cs:line 68
   at Test.Integration.<>c__DisplayClass1_0.<TestHomePageParallel>b__0(Int64 index) in C:\HeadersNoWorky\Test\Test.cs:line 38
   at System.Threading.Tasks.Parallel.<>c__DisplayClass18_0`1.<ForWorker64>b__1()
   at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)
   at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object )
   at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
   at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
   at Microsoft.AspNetCore.Http.HeaderDictionary.Add(String key, StringValues value)
   at WebApplication4.MiddlewareHeader.<>c.<Invoke>b__2_0(Object state) in C:\HeadersNoWorky\WebApplication4\MiddlewareHeader.cs:line 20
   at Microsoft.AspNetCore.TestHost.ResponseFeature.<>c__DisplayClass23_0.<OnStarting>b__0()
   at Microsoft.AspNetCore.TestHost.ResponseFeature.FireOnSendingHeaders()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.GenerateResponse()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.ReturnResponseMessage()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.CompleteResponse()
   at Microsoft.AspNetCore.TestHost.ClientHandler.<>c__DisplayClass3_0.<<SendAsync>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.TestHost.ClientHandler.<SendAsync>d__3.MoveNext()
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at Test.Integration.Get(TestServer server, String url) in C:\HeadersNoWorky\Test\Test.cs:line 68
   at Test.Integration.<>c__DisplayClass1_0.<TestHomePageParallel>b__0(Int64 index) in C:\HeadersNoWorky\Test\Test.cs:line 38
   at System.Threading.Tasks.Parallel.<>c__DisplayClass18_0`1.<ForWorker64>b__1()
   at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)
   at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object )
   at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
   at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
   at Microsoft.AspNetCore.Http.HeaderDictionary.Add(String key, StringValues value)
   at WebApplication4.MiddlewareHeader.<>c.<Invoke>b__2_0(Object state) in C:\HeadersNoWorky\WebApplication4\MiddlewareHeader.cs:line 20
   at Microsoft.AspNetCore.TestHost.ResponseFeature.<>c__DisplayClass23_0.<OnStarting>b__0()
   at Microsoft.AspNetCore.TestHost.ResponseFeature.FireOnSendingHeaders()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.GenerateResponse()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.ReturnResponseMessage()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.CompleteResponse()
   at Microsoft.AspNetCore.TestHost.ClientHandler.<>c__DisplayClass3_0.<<SendAsync>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.TestHost.ClientHandler.<SendAsync>d__3.MoveNext()
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at Test.Integration.Get(TestServer server, String url) in C:\HeadersNoWorky\Test\Test.cs:line 68
   at Test.Integration.<>c__DisplayClass1_0.<TestHomePageParallel>b__0(Int64 index) in C:\HeadersNoWorky\Test\Test.cs:line 38
   at System.Threading.Tasks.Parallel.<>c__DisplayClass18_0`1.<ForWorker64>b__1()
   at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)
   at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object )
   at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
   at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
   at Microsoft.AspNetCore.Http.HeaderDictionary.Add(String key, StringValues value)
   at WebApplication4.MiddlewareHeader.<>c.<Invoke>b__2_0(Object state) in C:\HeadersNoWorky\WebApplication4\MiddlewareHeader.cs:line 20
   at Microsoft.AspNetCore.TestHost.ResponseFeature.<>c__DisplayClass23_0.<OnStarting>b__0()
   at Microsoft.AspNetCore.TestHost.ResponseFeature.FireOnSendingHeaders()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.GenerateResponse()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.ReturnResponseMessage()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.CompleteResponse()
   at Microsoft.AspNetCore.TestHost.ClientHandler.<>c__DisplayClass3_0.<<SendAsync>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.TestHost.ClientHandler.<SendAsync>d__3.MoveNext()
   at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
   at Test.Integration.Get(TestServer server, String url) in C:\HeadersNoWorky\Test\Test.cs:line 68
   at Test.Integration.<>c__DisplayClass1_0.<TestHomePageParallel>b__0(Int64 index) in C:\HeadersNoWorky\Test\Test.cs:line 38
   at System.Threading.Tasks.Parallel.<>c__DisplayClass18_0`1.<ForWorker64>b__1()
   at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)
   at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object )
   at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
   at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
   at Microsoft.AspNetCore.Http.HeaderDictionary.Add(String key, StringValues value)
   at WebApplication4.MiddlewareHeader.<>c.<Invoke>b__2_0(Object state) in C:\HeadersNoWorky\WebApplication4\MiddlewareHeader.cs:line 20
   at Microsoft.AspNetCore.TestHost.ResponseFeature.<>c__DisplayClass23_0.<OnStarting>b__0()
   at Microsoft.AspNetCore.TestHost.ResponseFeature.FireOnSendingHeaders()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.GenerateResponse()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.ReturnResponseMessage()
   at Microsoft.AspNetCore.TestHost.ClientHandler.RequestState.CompleteResponse()
   at Microsoft.AspNetCore.TestHost.ClientHandler.<>c__DisplayClass3_0.<<SendAsync>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.TestHost.ClientHandler.<SendAsync>d__3.MoveNext()
Result Message: 
One or more errors occurred.
One or more errors occurred.
An item with the same key has already been added.
One or more errors occurred.
An item with the same key has already been added.
One or more errors occurred.
An item with the same key has already been added.
One or more errors occurred.
An item with the same key has already been added.
One or more errors occurred.
An item with the same key has already been added.
One or more errors occurred.
An item with the same key has already been added.
One or more errors occurred.
An item with the same key has already been added.
One or more errors occurred.
An item with the same key has already been added.

更新: 添加一些调试后,我可以看到 Response.OnStarting() 被同一请求多次调用,因此它试图为单个请求更新响应 headers 两次,导致异常.不幸的是,我不明白为什么 Response.OnStarting() 会被调用两次。

TestHost 1.0.0 中的 TestServer 存在 MS 错误

即从这里更改 project.json:

Microsoft.AspNetCore.TestHost": "1.0.0"

对此:

Microsoft.AspNetCore.TestHost": "1.1.0"

修复了问题。

提交请看这里:https://github.com/aspnet/Hosting/issues/852