suppress ExecutionContext by default in TestServer (#10094)

fixes #7975

There is a 'PreserveExecutionContext' property to turn the old behavior back on. Also I had to modify where IHttpApplication.CreateContext is called since that's what sets the IHttpContextAccessor, which depends on AsyncLocals!
This commit is contained in:
Andrew Stanton-Nurse 2019-05-09 20:47:43 -07:00 committed by GitHub
parent 0b590ff46f
commit d3dc92ff6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 101 additions and 17 deletions

View File

@ -33,6 +33,7 @@ namespace Microsoft.AspNetCore.TestHost
public System.Uri BaseAddress { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public System.Uri BaseAddress { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } public Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
public Microsoft.AspNetCore.Hosting.IWebHost Host { get { throw null; } } public Microsoft.AspNetCore.Hosting.IWebHost Host { get { throw null; } }
public bool PreserveExecutionContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public System.Net.Http.HttpClient CreateClient() { throw null; } public System.Net.Http.HttpClient CreateClient() { throw null; }
public System.Net.Http.HttpMessageHandler CreateHandler() { throw null; } public System.Net.Http.HttpMessageHandler CreateHandler() { throw null; }
public Microsoft.AspNetCore.TestHost.RequestBuilder CreateRequest(string path) { throw null; } public Microsoft.AspNetCore.TestHost.RequestBuilder CreateRequest(string path) { throw null; }

View File

@ -45,6 +45,8 @@ namespace Microsoft.AspNetCore.TestHost
internal bool AllowSynchronousIO { get; set; } internal bool AllowSynchronousIO { get; set; }
internal bool PreserveExecutionContext { get; set; }
/// <summary> /// <summary>
/// This adapts HttpRequestMessages to ASP.NET Core requests, dispatches them through the pipeline, and returns the /// This adapts HttpRequestMessages to ASP.NET Core requests, dispatches them through the pipeline, and returns the
/// associated HttpResponseMessage. /// associated HttpResponseMessage.
@ -61,7 +63,7 @@ namespace Microsoft.AspNetCore.TestHost
throw new ArgumentNullException(nameof(request)); throw new ArgumentNullException(nameof(request));
} }
var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO); var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO, PreserveExecutionContext);
Stream responseBody = null; Stream responseBody = null;
var requestContent = request.Content ?? new StreamContent(Stream.Null); var requestContent = request.Content ?? new StreamContent(Stream.Null);

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Globalization;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server;
@ -14,8 +15,9 @@ namespace Microsoft.AspNetCore.TestHost
internal class HttpContextBuilder : IHttpBodyControlFeature internal class HttpContextBuilder : IHttpBodyControlFeature
{ {
private readonly IHttpApplication<Context> _application; private readonly IHttpApplication<Context> _application;
private readonly bool _preserveExecutionContext;
private readonly HttpContext _httpContext; private readonly HttpContext _httpContext;
private TaskCompletionSource<HttpContext> _responseTcs = new TaskCompletionSource<HttpContext>(TaskCreationOptions.RunContinuationsAsynchronously); private TaskCompletionSource<HttpContext> _responseTcs = new TaskCompletionSource<HttpContext>(TaskCreationOptions.RunContinuationsAsynchronously);
private ResponseStream _responseStream; private ResponseStream _responseStream;
private ResponseFeature _responseFeature = new ResponseFeature(); private ResponseFeature _responseFeature = new ResponseFeature();
@ -23,10 +25,11 @@ namespace Microsoft.AspNetCore.TestHost
private bool _pipelineFinished; private bool _pipelineFinished;
private Context _testContext; private Context _testContext;
internal HttpContextBuilder(IHttpApplication<Context> application, bool allowSynchronousIO) internal HttpContextBuilder(IHttpApplication<Context> application, bool allowSynchronousIO, bool preserveExecutionContext)
{ {
_application = application ?? throw new ArgumentNullException(nameof(application)); _application = application ?? throw new ArgumentNullException(nameof(application));
AllowSynchronousIO = allowSynchronousIO; AllowSynchronousIO = allowSynchronousIO;
_preserveExecutionContext = preserveExecutionContext;
_httpContext = new DefaultHttpContext(); _httpContext = new DefaultHttpContext();
var request = _httpContext.Request; var request = _httpContext.Request;
@ -61,11 +64,14 @@ namespace Microsoft.AspNetCore.TestHost
{ {
var registration = cancellationToken.Register(AbortRequest); var registration = cancellationToken.Register(AbortRequest);
_testContext = _application.CreateContext(_httpContext.Features); // Everything inside this function happens in the SERVER's execution context (unless PreserveExecutionContext is true)
async Task RunRequestAsync()
// Async offload, don't let the test code block the caller.
_ = Task.Factory.StartNew(async () =>
{ {
// This will configure IHttpContextAccessor so it needs to happen INSIDE this function,
// since we are now inside the Server's execution context. If it happens outside this cont
// it will be lost when we abandon the execution context.
_testContext = _application.CreateContext(_httpContext.Features);
try try
{ {
await _application.ProcessRequestAsync(_testContext); await _application.ProcessRequestAsync(_testContext);
@ -81,7 +87,20 @@ namespace Microsoft.AspNetCore.TestHost
{ {
registration.Dispose(); registration.Dispose();
} }
}); }
// Async offload, don't let the test code block the caller.
if (_preserveExecutionContext)
{
_ = Task.Factory.StartNew(RunRequestAsync);
}
else
{
ThreadPool.UnsafeQueueUserWorkItem(_ =>
{
_ = RunRequestAsync();
}, null);
}
return _responseTcs.Task; return _responseTcs.Task;
} }

View File

@ -78,12 +78,14 @@ namespace Microsoft.AspNetCore.TestHost
public IFeatureCollection Features { get; } public IFeatureCollection Features { get; }
/// <summary> /// <summary>
/// Gets or sets a value that controls whether synchronous IO is allowed for the <see cref="HttpContext.Request"/> and <see cref="HttpContext.Response"/> /// Gets or sets a value that controls whether synchronous IO is allowed for the <see cref="HttpContext.Request"/> and <see cref="HttpContext.Response"/>. The default value is <see langword="false" />.
/// </summary> /// </summary>
/// <remarks> public bool AllowSynchronousIO { get; set; }
/// Defaults to false.
/// </remarks> /// <summary>
public bool AllowSynchronousIO { get; set; } = false; /// Gets or sets a value that controls if <see cref="ExecutionContext"/> and <see cref="AsyncLocal{T}"/> values are preserved from the client to the server. The default value is <see langword="false" />.
/// </summary>
public bool PreserveExecutionContext { get; set; }
private IHttpApplication<Context> Application private IHttpApplication<Context> Application
{ {
@ -93,7 +95,7 @@ namespace Microsoft.AspNetCore.TestHost
public HttpMessageHandler CreateHandler() public HttpMessageHandler CreateHandler()
{ {
var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress);
return new ClientHandler(pathBase, Application) { AllowSynchronousIO = AllowSynchronousIO }; return new ClientHandler(pathBase, Application) { AllowSynchronousIO = AllowSynchronousIO, PreserveExecutionContext = PreserveExecutionContext };
} }
public HttpClient CreateClient() public HttpClient CreateClient()
@ -104,7 +106,7 @@ namespace Microsoft.AspNetCore.TestHost
public WebSocketClient CreateWebSocketClient() public WebSocketClient CreateWebSocketClient()
{ {
var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress);
return new WebSocketClient(pathBase, Application) { AllowSynchronousIO = AllowSynchronousIO }; return new WebSocketClient(pathBase, Application) { AllowSynchronousIO = AllowSynchronousIO, PreserveExecutionContext = PreserveExecutionContext };
} }
/// <summary> /// <summary>
@ -128,7 +130,7 @@ namespace Microsoft.AspNetCore.TestHost
throw new ArgumentNullException(nameof(configureContext)); throw new ArgumentNullException(nameof(configureContext));
} }
var builder = new HttpContextBuilder(Application, AllowSynchronousIO); var builder = new HttpContextBuilder(Application, AllowSynchronousIO, PreserveExecutionContext);
builder.Configure(context => builder.Configure(context =>
{ {
var request = context.Request; var request = context.Request;

View File

@ -47,11 +47,12 @@ namespace Microsoft.AspNetCore.TestHost
} }
internal bool AllowSynchronousIO { get; set; } internal bool AllowSynchronousIO { get; set; }
internal bool PreserveExecutionContext { get; set; }
public async Task<WebSocket> ConnectAsync(Uri uri, CancellationToken cancellationToken) public async Task<WebSocket> ConnectAsync(Uri uri, CancellationToken cancellationToken)
{ {
WebSocketFeature webSocketFeature = null; WebSocketFeature webSocketFeature = null;
var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO); var contextBuilder = new HttpContextBuilder(_application, AllowSynchronousIO, PreserveExecutionContext);
contextBuilder.Configure(context => contextBuilder.Configure(context =>
{ {
var request = context.Request; var request = context.Request;

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
@ -424,5 +425,58 @@ namespace Microsoft.AspNetCore.TestHost
// Assert // Assert
var exception = await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await tcs.Task); var exception = await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => await tcs.Task);
} }
[Fact]
public async Task AsyncLocalValueOnClientIsNotPreserved()
{
var asyncLocal = new AsyncLocal<object>();
var value = new object();
asyncLocal.Value = value;
object capturedValue = null;
var builder = new WebHostBuilder()
.Configure(app =>
{
app.Run((context) =>
{
capturedValue = asyncLocal.Value;
return context.Response.WriteAsync("Done");
});
});
var server = new TestServer(builder);
var client = server.CreateClient();
var resp = await client.GetAsync("/");
Assert.NotSame(value, capturedValue);
}
[Fact]
public async Task AsyncLocalValueOnClientIsPreservedIfPreserveExecutionContextIsTrue()
{
var asyncLocal = new AsyncLocal<object>();
var value = new object();
asyncLocal.Value = value;
object capturedValue = null;
var builder = new WebHostBuilder()
.Configure(app =>
{
app.Run((context) =>
{
capturedValue = asyncLocal.Value;
return context.Response.WriteAsync("Done");
});
});
var server = new TestServer(builder)
{
PreserveExecutionContext = true
};
var client = server.CreateClient();
var resp = await client.GetAsync("/");
Assert.Same(value, capturedValue);
}
} }
} }

View File

@ -15,5 +15,10 @@
<ItemGroup> <ItemGroup>
<Reference Include="System.ServiceProcess.ServiceController" /> <Reference Include="System.ServiceProcess.ServiceController" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Compile Update="WebHostService.cs">
<SubType>Component</SubType>
</Compile>
</ItemGroup>
</Project> </Project>